Post

Replies

Boosts

Views

Activity

Reply to UITextView scrolling indicator cut off at top
Better solution: NSLayoutConstraint.activate([ textView.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8), textView.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8), textView.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: cell.directionalLayoutMargins.leading), textView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -cell.directionalLayoutMargins.trailing), // minimum height to show the full vertical scroll indicator textView.heightAnchor.constraint(equalToConstant: 41.0), ]) Credit: https://stackoverflow.com/a/78935224/22697469. Again, I can't unaccept the previously accepted answer.
Aug ’24
Reply to UITextView scrolling indicator cut off at top
Make the cell taller. Updated cell registration: let textView = UITextView() textView.font = .systemFont(ofSize: UIFont.labelFontSize) cell.contentView.addSubview(textView) textView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ textView.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor), textView.heightAnchor.constraint(equalToConstant: textView.font!.pointSize * 4 - 16), textView.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: cell.directionalLayoutMargins.leading), textView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -cell.directionalLayoutMargins.trailing), cell.contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: textView.font!.pointSize * 4) ])
Aug ’24
Reply to Reconfigure UICollectionView section
@DTS Engineer Thank you for your reply, but, given that the body of the mentioned function is as follows, your answer is not pertinent to my question, since the item identifiers of the section are not reconfigured: let mountains = mountainsController.filteredMountains(with: filter).sorted { $0.name < $1.name } var snapshot = NSDiffableDataSourceSnapshot<Section, MountainsController.Mountain>() snapshot.appendSections([.main]) snapshot.appendItems(mountains) dataSource.apply(snapshot, animatingDifferences: true) By using the word "reconfigure", I hoped I had made clear that I am looking for a method like reconfigureItems(withIdentifiers:) of NSDiffableDataSourceSnapshot but for NSDiffableDataSourceSectionSnapshot.
Aug ’24
Reply to Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
@Frameworks Engineer Thank you for your reply. I highly encourage you to read my updated question on stack overflow, as I suggested doing in a previous reply. In particular, the question has been made explicit and is also now clearly introduced by a header. Could you please answer the question by modifying the provided code? Playing around with configurationUpdateHandler didn't do it for me. Problem If you tap on the row, it doesn't look like it gets selected: the line backgroundConfiguration.backgroundColor = .systemBlue breaks the cell's default background color transformer. Question Given that my goal is to have my cell manifest its selection exactly like usual (meaning exactly as it would without the line backgroundConfiguration.backgroundColor = .systemBlue), that the details of how a cell usually does so are likely not public, that I would like to set a custom background color for my cell and that I would want to configure its appearance using configurations, since I seem to understand that that is the way to go from iOS 14 onwards, does anybody know how to achieve my goal by resetting something to whatever it was before I said backgroundConfiguration.backgroundColor = .systemBlue? What I've tried and didn't work: Setting the collection view's delegate and specifying that you can select any row Setting the color transformer to .grayscale Setting the cell's backgroundConfiguration to UIBackgroundConfiguration.listGroupedCell().updated(for: cell.configurationState) Setting the color transformer to cell.defaultBackgroundConfiguration().backgroundColorTransformer Using collection view controllers (and setting collectionView.clearsSelectionOnViewWillAppear to false) Setting the cell's automaticallyUpdatesBackgroundConfiguration to false and then back to true Putting the cell's configuration code inside a configurationUpdateHandler Combinations of the approaches above Setting the color transformer to UIBackgroundConfiguration.listGroupedCell().backgroundColorTransformer and cell.backgroundConfiguration?.backgroundColorTransformer (they're both nil) Workaround 1: use a custom color transformer var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColorTransformer = .init { _ in if cell.configurationState.isSelected || cell.configurationState.isHighlighted || cell.configurationState.isFocused { .systemRed } else { .systemBlue } } cell.backgroundConfiguration = backgroundConfiguration Workaround 2: don't use a background configuration You can set the cell's selectedBackgroundView, like so: let v = UIView() v.backgroundColor = .systemBlue cell.selectedBackgroundView = v You won't be able to use custom background content configurations though and might want to use background views instead: var contentConfiguration = UIListContentConfiguration.cell() contentConfiguration.text = "Hello" cell.contentConfiguration = contentConfiguration let v = UIView() v.backgroundColor = .systemBlue cell.backgroundView = v let bv = UIView() bv.backgroundColor = .systemRed cell.selectedBackgroundView = bv Consideration on the workarounds Both workarounds seem to also not break this code, which deselects cells on viewWillAppear(_:) and was taken and slightly adapted from Apple's Modern Collection Views project (e.g. EmojiExplorerViewController.swift): override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) deselectSelectedItems(animated: animated) } func deselectSelectedItems(animated: Bool) { if let indexPath = collectionView.indexPathsForSelectedItems?.first { if let coordinator = transitionCoordinator { coordinator.animate(alongsideTransition: { [weak self] context in self?.collectionView.deselectItem(at: indexPath, animated: true) }) { [weak self] (context) in if context.isCancelled { self?.collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) } } } else { collectionView.deselectItem(at: indexPath, animated: animated) } } } (Collection view controllers don't sport all of that logic out of the box, even though their clearsSelectionOnViewWillAppear property is true by default.)
Aug ’24
Reply to Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
Here follows a more up to date version of the post, improved for clarity. Since it looks like I can no longer edit nor delete my original question on this forum and I don't want to make a duplicate, it seems appropriate to me to link to a forum that lets you: https://stackoverflow.com/questions/78830896/setting-the-backgroundcolor-property-of-uibackgroundconfiguration-breaks-the-d
Aug ’24
Reply to Can I omit `ObservableObject` conformance?
@DTS Engineer Hello, Thank you for your reply. I feel like the following is a better answer, since it gives some explanation: "You only need the ObservableObject conformance if you need a SwiftUI view to be auto-updated when an @Published property of your object changes. You also need to store the object as @ObservedObject or @StateObject to get the auto-updating behaviour and these property wrappers actually require the ObservableObject conformance. As you're using UIKit and hence don't actually get any automatic view updates, you don't need the ObservableObject conformance." Credit: https://stackoverflow.com/a/78812534/22697469. As per your suggestion to use the @Observable macro, unfortunately, I can't target just iOS 17 or above.
Jul ’24
Reply to Handle keyboard layout in iOS 15+ UIKit app with collection view using the modern approach
@DTS Engineer I see, but it looks like this approach is no longer recommended: I can't find "The Keys to a Better Text Input Experience", the WWDC session that is mentioned in "Your guide to keyboard layout". Considering that it is impossible that Apple does not explain you how to comprehensively (as explained later) handle the keyboard being laid out if your app uses a collection view controller and that I can't find such documentation, do you happen to have a link to it? In particular, your solution does not take into account the fact that the keyboard frame might change, the fact that the text view might have input accessory views, that the view controller might be embedded in a collection view controller, that the app scene might be floating on an iPad or that there have been changes in the timing with which the keyboard is shown in iOS 17 with respect to earlier iOS versions. All of these cases and more are handled by the up to date API.
Jul ’24
Reply to How do I make a map with selectable `MKMapItem`s in iOS 17?
Ok you may as well display markers like this: ForEach(mapItems, id: \.self) { Marker(item: $0) } where mapItems is an array of type [MKMapItem]. Complete code: import SwiftUI import MapKit struct MapView: View { @State private var mapCameraPosition: MapCameraPosition = .automatic @State private var mapSelection: MKMapItem? let mapItems = [MKMapItem( placemark: .init( coordinate: .init( latitude: 37, longitude: -122 ) ) )] var body: some View { Map( position: $mapCameraPosition, selection: $mapSelection ) { ForEach(mapItems, id: \.self) { Marker(item: $0) } } .onChange(of: mapSelection) { _, newSelection in print("onChange") if let _ = newSelection { print("selected") } } } }
Jun ’24
Reply to UICollectionViewListCell not resizing
Use UIListContentConfiguration: import UIKit class ViewController: UIViewController { var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<String, String>! override func viewDidLoad() { super.viewDidLoad() configureHierarchy() configureDataSource() } func configureHierarchy() { collectionView = .init(frame: .zero, collectionViewLayout: createLayout()) view.addSubview(collectionView) collectionView.frame = view.bounds } func createLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { section, layoutEnvironment in let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in var contentConfig = CustomListContentConfiguration() contentConfig.placeholder = "Placeholder" cell.contentConfiguration = contentConfig } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } var snapshot = NSDiffableDataSourceSnapshot<String, String>() snapshot.appendSections(["main"]) snapshot.appendItems(["demo"]) dataSource.apply(snapshot, animatingDifferences: false) } } class CustomListContentConfiguration: UIContentConfiguration { var placeholder: String? func makeContentView() -> UIView & UIContentView { return CustomListContentView(configuration: self) } func updated(for state: UIConfigurationState) -> Self { // Not handling state changes in this example, so just return self return self } } class CustomListContentView: UIView, UIContentView { var configuration: UIContentConfiguration init(configuration: UIContentConfiguration) { self.configuration = configuration super.init(frame: .zero) configureSubviews() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configureSubviews() { guard let config = configuration as? CustomListContentConfiguration else { return } let leadingView = UIView() leadingView.backgroundColor = .systemRed let textField = UITextField() textField.placeholder = config.placeholder textField.font = .systemFont(ofSize: 100) addSubview(leadingView) addSubview(textField) leadingView.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ leadingView.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor), leadingView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), leadingView.widthAnchor.constraint(equalTo: layoutMarginsGuide.heightAnchor), leadingView.heightAnchor.constraint(equalTo: layoutMarginsGuide.heightAnchor), textField.topAnchor.constraint(equalTo: topAnchor), textField.bottomAnchor.constraint(equalTo: bottomAnchor), textField.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 16), textField.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 44) ]) } }
Apr ’24
Reply to UISearchBar resigns first responder when typing in UICollectionView
If you use cell registrations instead of supplementary registrations, the keyboard doesn't dismiss for any reason and you can also get rid of the dispatch blocks: import UIKit class ViewController: UIViewController { let words = ["foo", "bar"] var filteredWords = ["foo", "bar"] { didSet { dataSource.apply(self.snapshot) } } var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<String, String>! var snapshot: NSDiffableDataSourceSnapshot<String, String> { var snapshot = NSDiffableDataSourceSnapshot<String, String>() snapshot.appendSections(["main"]) snapshot.appendItems(["search bar id"]) snapshot.appendItems(filteredWords) return snapshot } override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = .init(title: "Apply", style: .plain, target: self, action: #selector(apply)) configureHierarchy() configureDataSource() } @objc func apply() { dataSource.apply(self.snapshot) } func configureHierarchy() { collectionView = .init(frame: .zero, collectionViewLayout: createLayout()) view.addSubview(collectionView) collectionView.frame = view.bounds collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } func createLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { section, layoutEnvironment in var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) config.headerMode = .firstItemInSection return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { _, _, _ in } let searchBarCellRegistration = UICollectionView.CellRegistration<SearchBarCell, String>{ cell, indexPath, itemIdentifier in cell.searchBar.delegate = self } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in if indexPath.row == 0 { collectionView.dequeueConfiguredReusableCell(using: searchBarCellRegistration, for: indexPath, item: itemIdentifier) } else { collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } } dataSource.apply(self.snapshot, animatingDifferences: false) } } extension ViewController: UISearchBarDelegate { func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { filteredWords = words.filter { $0.hasPrefix(searchText) } } func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { filteredWords = words } } class SearchBarCell: UICollectionViewListCell { let searchBar = UISearchBar() override init(frame: CGRect) { super.init(frame: frame) contentView.addSubview(searchBar) searchBar.pinToSuperview() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension UIView { func pin( to object: CanBePinnedTo, top: CGFloat = 0, bottom: CGFloat = 0, leading: CGFloat = 0, trailing: CGFloat = 0 ) { self.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ self.topAnchor.constraint(equalTo: object.topAnchor, constant: top), self.bottomAnchor.constraint(equalTo: object.bottomAnchor, constant: bottom), self.leadingAnchor.constraint(equalTo: object.leadingAnchor, constant: leading), self.trailingAnchor.constraint(equalTo: object.trailingAnchor, constant: trailing), ]) } func pinToSuperview( top: CGFloat = 0, bottom: CGFloat = 0, leading: CGFloat = 0, trailing: CGFloat = 0, file: StaticString = #file, line: UInt = #line ) { guard let superview = self.superview else { print(">> \(#function) failed in file: \(String.localFilePath(from: file)), at line: \(line): could not find \(Self.self).superView.") return } self.pin(to: superview, top: top, bottom: bottom, leading: leading, trailing: trailing) } func pinToSuperview(constant c: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { self.pinToSuperview(top: c, bottom: -c, leading: c, trailing: -c, file: file, line: line) } } @MainActor protocol CanBePinnedTo { var topAnchor: NSLayoutYAxisAnchor { get } var bottomAnchor: NSLayoutYAxisAnchor { get } var leadingAnchor: NSLayoutXAxisAnchor { get } var trailingAnchor: NSLayoutXAxisAnchor { get } } extension UIView: CanBePinnedTo { } extension UILayoutGuide: CanBePinnedTo { } extension String { static func localFilePath(from fullFilePath: StaticString = #file) -> Self { URL(fileURLWithPath: "\(fullFilePath)").lastPathComponent } } Basically don't use supplementary views unless absolutely necessary: it also seems that there's no declarative way to update them without losing animations (Apple's guided project "Modern Collection Views" doesn't seem to provide a solution, nor do these posts: https://stackoverflow.com/questions/78311570/how-do-i-update-a-collection-view-supplementary-view-without-giving-up-on-animat, https://forums.developer.apple.com/forums/thread/749847).
Apr ’24