Post

Replies

Boosts

Views

Activity

UITextView scrolling indicator cut off at top
Sample app A collection view controller with list layout, 1 section and 1 row. The cell's content view contains a text view. class ViewController: UICollectionViewController { var snapshot: NSDiffableDataSourceSnapshot<Section, String> { var snapshot = NSDiffableDataSourceSnapshot<Section, String>() snapshot.appendSections([.main]) snapshot.appendItems(["one", "two"], toSection: .main) return snapshot } var dataSource: UICollectionViewDiffableDataSource<Section, String>? enum Section { case main } init() { super.init(collectionViewLayout: .init()) collectionView.collectionViewLayout = createLayout() configureDataSource() // more likely and automatically avoid unpleasant animations on iOS 15 by configuring the data source in the init rather than in view did load } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in let textView = UITextView() textView.font = .systemFont(ofSize: UIFont.labelFontSize) cell.contentView.addSubview(textView) textView.translatesAutoresizingMaskIntoConstraints = false 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) ]) } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } dataSource?.apply(self.snapshot, animatingDifferences: false) } func createLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { section, layoutEnvironment in let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } } Question 1 Can anybody edit the provided sample code so that the text view's vertical indicator inset is not cut off at the top? Question 2 It seems to me that Apple has successfully implemented text views inside table view cells: can anybody provide Apple documentation as per how to do so? What I've tried and didn't work textView.verticalScrollIndicatorInsets.top = 30 // does nothing Adding the text view to a custom view and the view to the cell's content view textView.contentInset = .zero textView.scrollIndicatorInsets = .zero textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 Centering the text view vertically and constraining its height to that of the content view with an 8 points constant to leave some padding Constraining the top and bottom anchors of the text view to the cell's layout margins guide's top and bottom anchors Constraint I need the text view to have some padding from the top and bottom of the cell for aesthetic reasons.
3
0
363
Aug ’24
Reconfigure UICollectionView section
Problem When I reconfigure a collection view snapshot and apply it to my complex production collection view with list layout, the UI updates slowly, unless I don't animate the differences, even though I'm only reconfiguring the specific item identifiers of the cells that should be updated. I thought that maybe I could speed up the animations of the UI updates by applying the snapshot to a specific section (dataSource?.apply(sectionSnapshot, to: .main)) instead of to the whole collection view (dataSource?.apply(wholeSnapshot)). The problem is that I can't reconfigure the item identifiers of a single section snapshot to then apply the updated snapshot. Question Given the following sample app, can anybody edit reconfigureMainSection() so that, when invoked, the .main section cell registrations are called, consequently invoking print("Reconfiguring")? Sample app This collection view with list layout and diffable data source has 1 section and 2 rows. You can tap the right bar button item to call reconfigureMainSection(). class ViewController: UICollectionViewController { var snapshot: NSDiffableDataSourceSnapshot<Section, String> { var snapshot = NSDiffableDataSourceSnapshot<Section, String>() snapshot.appendSections([.main]) snapshot.appendItems(["one", "two"], toSection: .main) return snapshot } var dataSource: UICollectionViewDiffableDataSource<Section, String>? enum Section { case main } init() { super.init(collectionViewLayout: .init()) collectionView.collectionViewLayout = createLayout() configureDataSource() } override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = .init( title: "Reconfigure", style: .plain, target: self, action: #selector(reconfigureMainSection) ) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in print("Reconfiguring") } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } dataSource?.apply(self.snapshot, animatingDifferences: false) } func createLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { section, layoutEnvironment in let config = UICollectionLayoutListConfiguration(appearance: .plain) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } @objc func reconfigureMainSection() { var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<String>() sectionSnapshot.append(snapshot.itemIdentifiers(inSection: .main)) // reconfigure the section snapshot dataSource?.apply(sectionSnapshot, to: .main) } } Environment MacBook Air M1 8GB macOS Sonoma 14.5 Xcode 15.4 iPhone 15 Pro simulator on iOS 17.5
3
0
335
Aug ’24
Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
Sample app The following is a UIKit app that displays a collection view with list layout and diffable data source (one section, one row). 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 collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } 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 backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColor = .systemBlue cell.backgroundConfiguration = backgroundConfiguration } 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) } } 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.)
1
0
356
Aug ’24
Setting the `backgroundColor` property of UIBackgroundConfiguration breaks the default UIConfigurationColorTransformer
The following is a UIKit app that displays a collection view with list layout and diffable data source (one section, one row). 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 collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } 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 backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColor = .systemBlue cell.backgroundConfiguration = backgroundConfiguration } 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) } } If you tap on the row, it seems like selection doesn't happen: giving the cell a blue background broke its default background color transformer. Here's 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 backgroundConfiguration to UIBackgroundConfiguration.listGroupedCell().updated(for: cell.configurationState) Combinations of the approaches above Setting the color transformer to UIBackgroundConfiguration.listGroupedCell().backgroundColorTransformer and cell.backgroundConfiguration?.backgroundColorTransformer (they're both nil) Setting the cell's backgroundColor directly I also considered using a custom color transformer: var backgroundConfiguration = UIBackgroundConfiguration.listGroupedCell() backgroundConfiguration.backgroundColorTransformer = UIConfigurationColorTransformer { _ in if cell.configurationState.isSelected || cell.configurationState.isHighlighted { .systemBlue.withAlphaComponent(0.7) } else { .systemBlue } } cell.backgroundConfiguration = backgroundConfiguration However, if you push a view controller when you select the row, the row gets deselected, which is unfortunate, giving that I like to deselect rows in viewWillAppear(_:) to keep users more oriented. There might be ways to circumvent this, but my custom color transformer might still differ from the default one in some other ways. So how do I assign the default one to my cell?
4
0
424
Aug ’24
Can I omit `ObservableObject` conformance?
Apple's documentation pretty much only says this about ObservableObject: "A type of object with a publisher that emits before the object has changed. By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.". And this sample seems to behave the same way, with or without conformance to the protocol by Contact: import UIKit import Combine class ViewController: UIViewController { let john = Contact(name: "John Appleseed", age: 24) private var cancellables: Set<AnyCancellable> = [] override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. john.$age.sink { age in print("View controller's john's age is now \(age)") } .store(in: &cancellables) print(john.haveBirthday()) } } class Contact { @Published var name: String @Published var age: Int init(name: String, age: Int) { self.name = name self.age = age } func haveBirthday() -> Int { age += 1 return age } } Can I therefore omit conformance to ObservableObject every time I don't need the objectWillChange publisher?
2
0
378
Jul ’24
Programmatic UICollectionViewController
I'm making a UIKit app with no storyboard. This is my scene delegate: import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) window.makeKeyAndVisible() window.rootViewController = UINavigationController(rootViewController: ViewController()) self.window = window } } I've noticed that if I subclass ViewController to UICollectionViewController, the app crashes with message "Thread 1: "UICollectionView must be initialized with a non-nil layout parameter"": import UIKit class ViewController: UICollectionViewController { } It looks like I necessarily need to override the initializer: import UIKit class ViewController: UICollectionViewController { init() { super.init(collectionViewLayout: .init()) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } I would indeed like to pass the final collection view layout in super.init(collectionViewLayout:), but defining the trailing actions before that isn't possible since self hasn't been initialized yet. So this is what I'm stuck with: import UIKit class ViewController: UICollectionViewController { init() { super.init(collectionViewLayout: .init()) var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) var layout = UICollectionViewCompositionalLayout.list(using: configuration) configuration.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath -> UISwipeActionsConfiguration? in // access a property of self return .init(actions: [.init(style: .destructive, title: "Hello", handler: { _,_,_ in print("Handled") })]) } layout = UICollectionViewCompositionalLayout.list(using: configuration) collectionView.collectionViewLayout = layout } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } Is this all valid?
3
0
372
Jul ’24
UICollectionView has weird separator insets in landscape mode
Here is a screenshot of the app: And here follows the code: it's a view controller with a collection view with plain list layout and a diffable data source. It has 1 section with 2 rows. The 1st row's content configuration is UIListContentConfiguration.cell(), and it has some text. The 2nd row has an empty content configuration instead. As you can see, if you run the app, the separator insets are different for the two rows. Did I make a mistake? If this is expected behavior instead, is there an easy fix? import UIKit class ViewController: UIViewController { var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<String, String>! var snapshot: NSDiffableDataSourceSnapshot<String, String> { var snapshot = NSDiffableDataSourceSnapshot<String, String>() snapshot.appendSections(["main"]) snapshot.appendItems(["one", "two"]) return snapshot } override func viewDidLoad() { super.viewDidLoad() configureHierarchy() configureDataSource() } func configureHierarchy() { collectionView = .init( frame: view.bounds, collectionViewLayout: createLayout() ) view.addSubview(collectionView) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } func createLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { section, layoutEnvironment in let config = UICollectionLayoutListConfiguration(appearance: .plain) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } func configureDataSource() { let firstCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in var contentConfiguration = UIListContentConfiguration.cell() contentConfiguration.text = "Hello" cell.contentConfiguration = contentConfiguration } let secondCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in } let emptyCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in switch itemIdentifier { case "one": collectionView.dequeueConfiguredReusableCell(using: firstCellRegistration, for: indexPath, item: itemIdentifier) case "two": collectionView.dequeueConfiguredReusableCell(using: secondCellRegistration, for: indexPath, item: itemIdentifier) default: collectionView.dequeueConfiguredReusableCell(using: emptyCellRegistration, for: indexPath, item: itemIdentifier) } } dataSource.apply(self.snapshot, animatingDifferences: false) } } Xcode 15.4, iPhone 15 Pro simulator, iOS 17.5, MacBook Air M1 8GB, macOS 14.5.
1
0
411
Jun ’24
Handle keyboard layout in iOS 15+ UIKit app with collection view using the modern approach
The following is a UIKit app that uses a collection view with list layout and a diffable data source. It displays one section that has 10 empty cells and then a final cell whose content view contains a text view, that is pinned to the content view's layout margins guide. The text view's scrolling is set to false, so that the line collectionView.selfSizingInvalidation = .enabledIncludingConstraints will succeed at making the text view's cell resize automatically and animatedly as the text changes. import UIKit class ViewController: UIViewController { var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<String, Int>! let textView: UITextView = { let tv = UITextView() tv.text = "Text" tv.isScrollEnabled = false return tv }() override func viewDidLoad() { super.viewDidLoad() configureHierarchy() configureDataSource() if #available(iOS 16.0, *) { collectionView.selfSizingInvalidation = .enabledIncludingConstraints } } func configureHierarchy() { collectionView = .init(frame: .zero, collectionViewLayout: createLayout()) view.addSubview(collectionView) collectionView.frame = view.bounds collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } func createLayout() -> UICollectionViewLayout { let configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return UICollectionViewCompositionalLayout.list(using: configuration) } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { _, _, _ in } let textViewCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { [weak self] cell, _, _ in guard let self else { return } cell.contentView.addSubview(textView) textView.pin(to: cell.contentView.layoutMarginsGuide) } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in if indexPath.row == 10 { collectionView.dequeueConfiguredReusableCell(using: textViewCellRegistration, for: indexPath, item: itemIdentifier) } else { collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } } var snapshot = NSDiffableDataSourceSnapshot<String, Int>() snapshot.appendSections(["section"]) snapshot.appendItems(Array(0...10)) dataSource.apply(snapshot) } } 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), ]) } } @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 { } How do I make the UI move to accomodate the keyboard once you tap on the text view and also when the text view changes size, by activating the view.keyboardLayoutGuide.topAnchor constraint, as shown in the WWDC21 video "Your guide to keyboard layout"? My code does not resize the text view on iOS 15, only on iOS 16+, so clearly the solution may as well allow the UI to adjust to changes to the text view frame on iOS 16+ only. Recommended, modern, approach: Not recommended, old, approach: Here's what I've tried and didn’t work on the Xcode 15.3 iPhone 15 Pro simulator with iOS 17.4 and on my iPhone SE with iOS 15.8: view.keyboardLayoutGuide.topAnchor.constraint(equalTo: textView.bottomAnchor).isActive = true in the text view cell registration view.keyboardLayoutGuide.topAnchor.constraint(equalTo: collectionView.bottomAnchor).isActive = true in viewDidLoad() pinning the bottom of the collection view to the top of the keyboard: func configureHierarchy() { collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout()) view.addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } To be more specific, I only tried this last approach on the simulator, and it moved the UI seemingly twice as much as it should have, and the tab bar of my tab bar controller was black, which discouraged me although there might be a proper (non-workaround) solution for iOS 17 only (i.e. saying view.keyboardLayoutGuide.usesBottomSafeArea = false). The first 2 approaches just didn't work instead. Setting the constraints priority to .defaultHigh doesn't do it.
4
0
815
Jun ’24
UIButton.ConfigurationUpdateHandler slow in UITableView cell [UIButton.Configuration, UIButtonConfiguration]
If you run the following UIKit app and tap on the button, you can see that it only updates its color if you hold on it for a bit, instead of immediately (as happens in the second app) (iOS 17.5, iPhone 15 Pro simulator, Xcode 15.4). This app consists of a view controller with a table view with one cell, which has a CheckoutButton instance constrained to its contentView top, bottom, leading and trailing anchors. The checkout button uses UIButton.Configuration to set its appearance, and update it based on its state. import UIKit class ViewController: UIViewController { let tableView = UITableView() let checkoutButton = CheckoutButton() override func viewDidLoad() { super.viewDidLoad() // table view setup view.addSubview(tableView) tableView.frame = view.bounds tableView.autoresizingMask = [.flexibleHeight, .flexibleWidth] tableView.dataSource = self tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") } } extension ViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) cell.contentView.addSubview(checkoutButton) checkoutButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ checkoutButton.topAnchor.constraint(equalTo: cell.contentView.topAnchor), checkoutButton.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), checkoutButton.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), checkoutButton.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor) ]) return cell } } class CheckoutButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) var configuration = UIButton.Configuration.plain() var attributeContainer = AttributeContainer() attributeContainer.font = .preferredFont(forTextStyle: .headline) attributeContainer.foregroundColor = .label configuration.attributedTitle = .init("Checkout", attributes: attributeContainer) self.configuration = configuration let configHandler: UIButton.ConfigurationUpdateHandler = { button in switch button.state { case .selected, .highlighted: button.configuration?.background.backgroundColor = .systemCyan case .disabled: button.configuration?.background.backgroundColor = .systemGray4 default: button.configuration?.background.backgroundColor = .systemBlue } } self.configurationUpdateHandler = configHandler } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } In this second app, instead, the selection of the button is immediately reflected in its appearance: import UIKit class ViewController: UIViewController { let button = CheckoutButton() override func viewDidLoad() { super.viewDidLoad() view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: view.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.centerYAnchor), button.widthAnchor.constraint(equalToConstant: 300), button.heightAnchor.constraint(equalToConstant: 44) ]) } } class CheckoutButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) var configuration = UIButton.Configuration.plain() var attributeContainer = AttributeContainer() attributeContainer.font = .preferredFont(forTextStyle: .headline) attributeContainer.foregroundColor = .label configuration.attributedTitle = .init("Checkout", attributes: attributeContainer) self.configuration = configuration let configHandler: UIButton.ConfigurationUpdateHandler = { button in switch button.state { case .selected, .highlighted: button.configuration?.background.backgroundColor = .systemCyan case .disabled: button.configuration?.background.backgroundColor = .systemGray4 default: button.configuration?.background.backgroundColor = .systemBlue } } self.configurationUpdateHandler = configHandler } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } This app consists of a view controller with just a button: no table view unlike in the first app. How do I make the button show its selection as soon as it's tapped, no matter if it's in a table view cell or on its own?
1
0
516
Jun ’24
SwiftUI map lags when it's centered in the user's location and state changes
Please note that for this app to be able show you your location on the map you need to enable it to ask for permission to track the user's location, in the project's targets' info: Here's also a video that illustrates the lag: https://youtube.com/shorts/DSl-umGxs20?feature=share. That being said, if you run this SwiftUI app and allow it to track your location, tap the MapUserLocationButton and press the buttons at the bottom, you'll see that the map lags: import SwiftUI import MapKit import CoreLocation struct ContentView: View { let currentLocationManager = CurrentUserLocationManager() let mapLocationsManager = MapLocationsManager() @State private var mapCameraPosition: MapCameraPosition = .automatic var body: some View { Map( position: $mapCameraPosition ) .safeAreaInset(edge: .bottom) { VStack { if mapLocationsManager.shouldAllowSearches { Button("First button") { mapLocationsManager.shouldAllowSearches = false } } Button("Second button") { mapLocationsManager.shouldAllowSearches = true } } .frame(maxWidth: .infinity) .padding() } .mapControls { MapUserLocationButton() } } } @Observable class MapLocationsManager { var shouldAllowSearches = false } // MARK: - Location related code - class CurrentUserLocationManager: NSObject { var locationManager: CLLocationManager? override init() { super.init() startIfNecessary() } func startIfNecessary() { if locationManager == nil { locationManager = .init() locationManager?.delegate = self } else { print(">> \(Self.self).\(#function): method called redundantly: locationManager had already been initialized") } } }; extension CurrentUserLocationManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { checkLocationAuthorization() } }; extension CurrentUserLocationManager { private func checkLocationAuthorization() { guard let locationManager else { return } switch locationManager.authorizationStatus { case .notDetermined: locationManager.requestWhenInUseAuthorization() case .restricted: print("Your location is restricted") case .denied: print("Go into setting to change it") case .authorizedAlways, .authorizedWhenInUse, .authorized: // locationManager.startUpdatingLocation() break @unknown default: break } } } I've also tried in a UIKit app (by just embedding ContentView in a view controller), with the same results: import UIKit import MapKit import SwiftUI import CoreLocation class ViewController: UIViewController { let currentLocationManager = CurrentUserLocationManager() override func viewDidLoad() { super.viewDidLoad() currentLocationManager.startIfNecessary() let hostingController = UIHostingController( rootView: MapView() ) addChild(hostingController) hostingController.view.frame = view.bounds hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(hostingController.view) hostingController.didMove(toParent: self) } } extension ViewController { struct MapView: View { let mapLocationsManager = MapLocationsManager() @State private var mapCameraPosition: MapCameraPosition = .automatic var body: some View { Map( position: $mapCameraPosition ) .safeAreaInset(edge: .bottom) { VStack { if mapLocationsManager.shouldAllowSearches { Button("First button") { mapLocationsManager.shouldAllowSearches = false } } Button("Second button") { mapLocationsManager.shouldAllowSearches = true } } .frame(maxWidth: .infinity) .padding() } .mapControls { MapUserLocationButton() } } } } extension ViewController { @Observable class MapLocationsManager { var shouldAllowSearches = false } } class CurrentUserLocationManager: NSObject { var locationManager: CLLocationManager? func startIfNecessary() { if locationManager == nil { locationManager = .init() locationManager?.delegate = self } else { print(">> \(Self.self).\(#function): method called redundantly: locationManager had already been initialized") } } }; extension CurrentUserLocationManager: CLLocationManagerDelegate { func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { checkLocationAuthorization() } }; extension CurrentUserLocationManager { private func checkLocationAuthorization() { guard let locationManager else { return } switch locationManager.authorizationStatus { case .notDetermined: locationManager.requestWhenInUseAuthorization() case .restricted: print("Your location is restricted") case .denied: print("Go into setting to change it") case .authorizedAlways, .authorizedWhenInUse, .authorized: // locationManager.startUpdatingLocation() break @unknown default: break } } } If you don't center the map in the user's location, you might see an occasiona lag, but it seems to me that this only happens once. How do I avoid these lags altogether? Xcode 15.4
1
0
555
Jun ’24
How do I make a map with selectable `MKMapItem`s in iOS 17?
I want to display an Apple map in my UIKit app, but if I try to do so with UIKit I get concurrency-related warnings (Xcode 15.4 with strict concurrency warnings enabled) that I couldn't find a proper solution to. So I opted for SwiftUI, and for the iOS 17 approach, specifically, (WWDC23). The following code displays a map with a marker. If you look at the Map initializer, you can see that a map camera position and a map selection are specified. If you run the app, however, you can't actually select the marker, unless you uncomment .tag(mapItem). import SwiftUI import MapKit struct MapView: View { @State private var mapCameraPosition: MapCameraPosition = .automatic @State private var mapSelection: MKMapItem? let mapItem = MKMapItem( placemark: .init( coordinate: .init( latitude: 37, longitude: -122 ) ) ) var body: some View { Map( position: $mapCameraPosition, selection: $mapSelection ) { Marker(item: mapItem) // .tag(mapItem) } .onChange(of: mapSelection) { _, newSelection in print("onChange") // never prints if let _ = newSelection { print("selected") // never prints } } } } If you give another tag, like 1, the code once again doesn't work. Is that really the way to go (that is, is my code with fine once you uncomment .tag(mapItem))? If not, how do I make a map with selectable MKMapItems in iOS 17? iOS 17.5, iPhone 15 Pro simulator, Xcode 15.4, macOS 14.5, MacBook Air M1 8GB.
1
0
631
Jun ’24
"Attempted to dequeue a cell for a different registration or reuse identifier than the existing cell when reconfiguring an item, which is not allowed"
Searching for my very title (including the quotes) generated no results on Google. If you run this UIKit app and tap the right bar button item: class ViewController: UIViewController { var boolean = false { didSet { var snapshot = self.snapshot snapshot.reconfigureItems(snapshot.itemIdentifiers) // if you comment this out the app doesn't crash dataSource.apply(snapshot) } } var snapshot: NSDiffableDataSourceSnapshot<String, String> { var snapshot = NSDiffableDataSourceSnapshot<String, String>() snapshot.appendSections(["main"]) snapshot.appendItems(boolean ? ["one"] : ["one", "two"]) return snapshot } 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 navigationItem.rightBarButtonItem = .init( title: "Toggle boolean", style: .plain, target: self, action: #selector(toggleBoolean) ) } @objc func toggleBoolean() { boolean.toggle() } func createLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { section, layoutEnvironment in let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } func configureDataSource() { let cellRegistration1 = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in } let cellRegistration2 = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in } dataSource = .init(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in if indexPath.row == 0 && boolean { collectionView.dequeueConfiguredReusableCell(using: cellRegistration1, for: indexPath, item: itemIdentifier) } else { collectionView.dequeueConfiguredReusableCell(using: cellRegistration2, for: indexPath, item: itemIdentifier) } } dataSource.apply(self.snapshot, animatingDifferences: false) } } it crashes with that error message. The app uses a collection view with list layout and a diffable data source. It has one section, which should show one row if boolean is true, two if it's false. When boolean changes, the collection view should also reconfigure its items (in my real app, that's needed to update the shown information). If you comment out snapshot.reconfigureItems(snapshot.itemIdentifiers), the app no longer crashes. What's the correct way of reconfiguring the items of a diffable data source then? iOS 17.5, iPhone 15 Pro simulator, Xcode 15.4, macOS 17.5, MacBook Air M1 8GB.
1
0
1.1k
Jun ’24
UICollectionViewListCell not resizing
Please run the following UIKit app. It uses a collection view with compositional layout (list layout) and a diffable data source. The collection view has one section with one row. The cell contains a text field that is pinned to its contentView. 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 let textField = UITextField() textField.placeholder = "Placeholder" textField.font = .systemFont(ofSize: 100) cell.contentView.addSubview(textField) textField.pinToSuperview() } 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) } } 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 } } Unfortunately, as soon as I insert a leading view in the cell: let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in let contentView = cell.contentView let leadingView = UIView() leadingView.backgroundColor = .systemRed let textField = UITextField() textField.placeholder = "Placeholder" textField.font = .systemFont(ofSize: 100) contentView.addSubview(leadingView) contentView.addSubview(textField) leadingView.translatesAutoresizingMaskIntoConstraints = false textField.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ leadingView.centerYAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerYAnchor), leadingView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor), leadingView.widthAnchor.constraint(equalTo: contentView.layoutMarginsGuide.heightAnchor), leadingView.heightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.heightAnchor), textField.topAnchor.constraint(equalTo: contentView.topAnchor), textField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), textField.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 16), textField.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor), textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 44) ]) } the cell does not self-size, and in particular it does not accomodate the text field: What would be the best way to make the cell resize automatically?
1
0
509
Apr ’24
How do I center a UIImageView vertically in a UICollectionViewListCell as a custom accessory?
Please run the following UIKit app. It uses a collection view with compositional layout (list layout) and a diffable data source. It has one section with one row. The cell has an image view as a leading accessory. Unfortunately, as soon as I set an image for the image view, the accessory is no longer centered: 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 let iv = UIImageView() iv.backgroundColor = .systemRed // iv.image = .init(systemName: "camera") iv.contentMode = .scaleAspectFit iv.frame.size = .init( width: 40, height: 40 ) cell.accessories = [.customView(configuration: .init( customView: iv, placement: .leading(), reservedLayoutWidth: .actual, maintainsFixedSize: true ))] } 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) } } This seems like a bug but then if I set the image view's size to 100x100, even without giving it an image, the cell doesn't resize, which makes me think I'm making a mistake.
0
0
378
Apr ’24