Post

Replies

Boosts

Views

Activity

SwiftUI selectable stepper in view that presents modally
Note: I'd like the solution to work for iOS 15 as well. With the following implementation, tapping on the stepper from iPhone (iOS 15.8 (physical device) as well as iOS 17.2 (simulator and canvas)) presents ModalView, instead of changing the stepper's value as one would expect. It's a somewhat real-life example but still basic, as I felt that having a view with just a stepper would have made the problem unrealistically easy. struct CategoryView: View { @State private var modalIsPresented = false @State private var stepperValue = 0 var body: some View { List { StepperRow(value: self.$stepperValue) .onTapGesture { modalIsPresented = true } } .sheet(isPresented: $modalIsPresented) { modalIsPresented = false } content: { ModalView() } } } struct StepperRow: View { @Binding var value: Int var body: some View { VStack(alignment: .leading) { Stepper( "\(value) Name of the article", value: $value, in: 0...Int.max ) Text("Item description, which could be long and I'd like to go under the stepper.") .font(.caption) } } } What doesn't work: setting the stepper's style to .plain or BorderlessButtonStyle(), as might work for a button. The following code is a working solution, though it's ugly. struct CategoryView: View { @State private var stepperValue = 0 var body: some View { List { StepperRow(value: self.$stepperValue) } } } struct StepperRow: View { @Binding var value: Int @State private var modalIsPresented = false var body: some View { ZStack(alignment: .leading) { VStack(alignment: .leading) { HStack { Text("\(value) Name of the article") Spacer() Stepper( "", value: $value, in: 0...Int.max ) .labelsHidden() .hidden() } Text("Item description, which could be long and I'd like to go under the stepper.") .font(.caption) } .onTapGesture { modalIsPresented = true } VStack(alignment: .leading) { HStack { Text("\(value) Name of the article") .hidden() Spacer() Stepper( "", value: $value, in: 0...Int.max ) .labelsHidden() } Text("Item description, which could be long and I'd like to go under the stepper.") .font(.caption) .hidden() } } .sheet(isPresented: $modalIsPresented) { modalIsPresented = false } content: { ModalView() } } } Basically I've put the stepper above the view to which I've added the onTapGesture recognizer, but to do so I had to duplicate the view code, so that everything laid out correctly, and hide the appropriate subviews, so that VoiceOver would ignore the duplicates, and also because it felt right. Can anyone come up with a better solution?
1
0
713
Mar ’24
UIStepper disclosure indicator
Steppers overlap with the disclosure indicator if you try to add them to a UICollectionViewListCell using: cell.accessories = [.disclosureIndicator(), .customView(configuration: .init(customView: UIStepper(), placement: .trailing()))]. What's the correct way to add a stepper to the accessories of a cell then? Example that you can run: class GridViewController: UIViewController { enum Section { case main } var dataSource: UICollectionViewDiffableDataSource<Section, Int>! = nil var collectionView: UICollectionView! = nil override func viewDidLoad() { super.viewDidLoad() navigationItem.title = "VC" configureHierarchy() configureDataSource() } } extension GridViewController { private func createLayout() -> UICollectionViewLayout { let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return UICollectionViewCompositionalLayout.list(using: config) } } extension GridViewController { private func configureHierarchy() { collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.backgroundColor = .black view.addSubview(collectionView) } private func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, identifier) in cell.accessories = [.disclosureIndicator(), .customView(configuration: .init(customView: UIStepper(), placement: .trailing()))] } dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier) } var snapshot = NSDiffableDataSourceSnapshot<Section, Int>() snapshot.appendSections([.main]) snapshot.appendItems([1]) dataSource.apply(snapshot, animatingDifferences: false) } }
Topic: UI Frameworks SubTopic: UIKit Tags:
1
0
587
Mar ’24
Can't display the simplest UIToolbar
I thought I could easily display a toolbar in UIKit, but I was wrong, or at least I can't do so without getting "Unable to simultaneously satisfy constraints." console messages. Here is my code: import UIKit class ViewController: UIViewController { let toolbar = UIToolbar() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground toolbar.items = [ UIBarButtonItem(title: "Title", style: .plain, target: nil, action: nil), UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) ] view.addSubview(toolbar) toolbar.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), toolbar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), toolbar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), toolbar.heightAnchor.constraint(equalToConstant: 44) ]) } } And here is the console log: Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) ( "<NSAutoresizingMaskLayoutConstraint:0x600002107b10 h=--& v=--& _UIToolbarContentView:0x104008e40.height == 0 (active)>", "<NSLayoutConstraint:0x600002122e90 V:|-(0)-[_UIButtonBarStackView:0x10250d8b0] (active, names: '|':_UIToolbarContentView:0x104008e40 )>", "<NSLayoutConstraint:0x600002121ef0 _UIButtonBarStackView:0x10250d8b0.bottom == _UIToolbarContentView:0x104008e40.bottom (active)>", "<NSLayoutConstraint:0x600002107a70 UIButtonLabel:0x10250f280.centerY == _UIModernBarButton:0x1027059c0'Title'.centerY + 1.5 (active)>", "<NSLayoutConstraint:0x60000210ea30 'TB_Baseline_Baseline' _UIModernBarButton:0x1027059c0'Title'.lastBaseline == UILayoutGuide:0x600003b0ca80'UIViewLayoutMarginsGuide'.bottom (active)>", "<NSLayoutConstraint:0x60000210ea80 'TB_Top_Top' V:|-(>=0)-[_UIModernBarButton:0x1027059c0'Title'] (active, names: '|':_UIButtonBarButton:0x102607120 )>", "<NSLayoutConstraint:0x60000210e8f0 'UIButtonBar.maximumAlignmentSize' _UIButtonBarButton:0x102607120.height == UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide'.height (active)>", "<NSLayoutConstraint:0x60000212c960 'UIView-bottomMargin-guide-constraint' V:[UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide']-(0)-| (active, names: '|':_UIButtonBarStackView:0x10250d8b0 )>", "<NSLayoutConstraint:0x60000210ec60 'UIView-bottomMargin-guide-constraint' V:[UILayoutGuide:0x600003b0ca80'UIViewLayoutMarginsGuide']-(11)-| (active, names: '|':_UIButtonBarButton:0x102607120 )>", "<NSLayoutConstraint:0x60000212d6d0 'UIView-topMargin-guide-constraint' V:|-(0)-[UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide'] (active, names: '|':_UIButtonBarStackView:0x10250d8b0 )>" ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x600002107a70 UIButtonLabel:0x10250f280.centerY == _UIModernBarButton:0x1027059c0'Title'.centerY + 1.5 (active)> Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful. Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) ( "<NSAutoresizingMaskLayoutConstraint:0x600002106710 h=--& v=--& _UIToolbarContentView:0x104008e40.width == 0 (active)>", "<NSLayoutConstraint:0x600002120b40 H:|-(0)-[_UIButtonBarStackView:0x10250d8b0] (active, names: '|':_UIToolbarContentView:0x104008e40 )>", "<NSLayoutConstraint:0x600002122e40 H:[_UIButtonBarStackView:0x10250d8b0]-(0)-| (active, names: '|':_UIToolbarContentView:0x104008e40 )>", "<NSLayoutConstraint:0x60000210eda0 'TB_Leading_Leading' H:|-(16)-[_UIModernBarButton:0x1027059c0'Title'] (active, names: '|':_UIButtonBarButton:0x102607120 )>", "<NSLayoutConstraint:0x60000210eb70 'TB_Trailing_Trailing' H:[_UIModernBarButton:0x1027059c0'Title']-(16)-| (active, names: '|':_UIButtonBarButton:0x102607120 )>", "<NSLayoutConstraint:0x60000210e580 'UISV-canvas-connection' UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide'.leading == _UIButtonBarButton:0x102607120.leading (active)>", "<NSLayoutConstraint:0x60000210e5d0 'UISV-canvas-connection' UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide'.trailing == UIView:0x10400e480.trailing (active)>", "<NSLayoutConstraint:0x60000210e9e0 'UISV-spacing' H:[_UIButtonBarButton:0x102607120]-(0)-[UIView:0x10400e480] (active)>", "<NSLayoutConstraint:0x60000212c820 'UIView-leftMargin-guide-constraint' H:|-(0)-[UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide'](LTR) (active, names: '|':_UIButtonBarStackView:0x10250d8b0 )>", "<NSLayoutConstraint:0x60000212ca00 'UIView-rightMargin-guide-constraint' H:[UILayoutGuide:0x600003b00380'UIViewLayoutMarginsGuide']-(0)-|(LTR) (active, names: '|':_UIButtonBarStackView:0x10250d8b0 )>" ) Will attempt to recover by breaking constraint <NSLayoutConstraint:0x60000210eb70 'TB_Trailing_Trailing' H:[_UIModernBarButton:0x1027059c0'Title']-(16)-| (active, names: '|':_UIButtonBarButton:0x102607120 )> Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful. I tried giving the toolbar a frame rather than constraints, and also to not give it an explicit height. The only thing that works is to comment out UIBarButtonItem(title: "Title", style: .plain, target: nil, action: nil), which isn't really a solution. What am I doing wrong?
Topic: UI Frameworks SubTopic: UIKit Tags:
1
0
720
Mar ’24
How do I resize a UICollectionViewListCell containing a UITextView?
This is a simple collection view with compositional layout and diffable data source. It displays one cell, of type UICollectionViewListCell, whose contentView has a text view as a subview. import UIKit class ViewController: UIViewController { var collectionView: UICollectionView! let textView = UITextView() var dataSource: UICollectionViewDiffableDataSource<Section, Int>! enum Section: CaseIterable { case first } override func viewDidLoad() { super.viewDidLoad() configureHierarchy() configureDataSource() } private func configureHierarchy() { collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) view.addSubview(collectionView) collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] textView.delegate = self } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { [weak self] cell, indexPath, itemIdentifier in guard let self else { return } cell.contentView.addSubview(textView) textView.pinToSuperviewMargins() } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } var snapshot = NSDiffableDataSourceSnapshot<Section, Int>() snapshot.appendSections(Section.allCases) snapshot.appendItems([1], toSection: .first) dataSource.apply(snapshot) } func createLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { section, layoutEnvironment in var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } } extension ViewController: UITextViewDelegate { func textViewDidChange(_ textView: UITextView) { // Do something here? } } The pinToSuperviewMargins method sets the top, bottom, leading and trailing constraints of the view on which it's called to its superview's and its translatesAutoResizingMaskIntoConstraints property to false: extension UIView { func pinToSuperviewMargins( top: CGFloat = 0, bottom: CGFloat = 0, leading: CGFloat = 0, trailing: CGFloat = 0, file: StaticString = #file, line: UInt = #line ) { guard let superview = self.superview else { let localFilePath = URL(fileURLWithPath: "\(file)").lastPathComponent print(">> \(#function) failed in file: \(localFilePath), at line: \(line): could not find \(Self.self).superView.") return } self.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ self.topAnchor.constraint(equalTo: superview.topAnchor, constant: top), self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: bottom), self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: leading), self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: trailing), ]) } func pinToSuperviewMargins(constant c: CGFloat = 0, file: StaticString = #file, line: UInt = #line) { self.pinToSuperviewMargins(top: c, bottom: c, leading: c, trailing: c, file: file, line: line) } } I tried calling collectionView.setNeedsLayout() in textViewDidChange(_:) but it doesn't work. I used to accomplish cell resizing with tableView.beginUpdates(); tableView.endUpdates() when dealing with table views.
Topic: UI Frameworks SubTopic: UIKit Tags:
2
0
1.8k
Apr ’24
Swift 5.10: Cannot access property '*' with a non-sendable type '*' from non-isolated deinit; this is an error in Swift 6
The following Swift UIKit code produces the warning "Cannot access property 'authController' with a non-sendable type 'AuthController' from non-isolated deinit; this is an error in Swift 6": import UIKit class AuthFormNavC: UINavigationController { let authController: AuthController init(authController: AuthController) { self.authController = authController super.init(rootViewController: ConsentVC()) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { authController.signInAnonymouslyIfNecessary() } } Swift 5.10, Xcode 15.3 with complete strict concurrency checking. What is the workaround? Please don't ask me why I'm doing what I'm doing or anything unrelated to the question. If you're wondering why I want to call authController.signInAnonymouslyIfNecessary() when the navigation controller is denitialized, my goal is to call it when the navigation controller is dismissed (or popped), and I think that the deinitializer of a view controller is the only method that is called if and only if the view controller is being dismissed (or popped) in my case. I tried observing variables like isViewLoaded in the past using KVO but I couldn't get it to work passing any combination of options in observe(_:options:changeHandler:).
1
0
1.2k
Apr ’24
Update collection view supplementary view content
If you run the following UIKit app and tap the view controller's right bar button item, the footerText property will change. How should I update the collection view's footer to display the updated footerText? class ViewController: UIViewController { var collectionView: UICollectionView! var footerText = "Initial footer text" var dataSource: UICollectionViewDiffableDataSource<Section, String>! var snapshot: NSDiffableDataSourceSnapshot<Section, String> { var snapshot = NSDiffableDataSourceSnapshot<Section, String>() snapshot.appendSections(Section.allCases) snapshot.appendItems(["A", "a"], toSection: .first) return snapshot } enum Section: CaseIterable { case first } override func viewDidLoad() { super.viewDidLoad() configureHierarchy() configureDataSource() } func configureHierarchy() { navigationItem.rightBarButtonItem = .init(title: "Change footer text", style: .plain, target: self, action: #selector(changeFooterText)) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) view.addSubview(collectionView) collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] } @objc func changeFooterText() { footerText = "Secondary footer text" } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in var contentConfiguration = UIListContentConfiguration.cell() contentConfiguration.text = itemIdentifier cell.contentConfiguration = contentConfiguration } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } configureSupplementaryViewProvider() dataSource.apply(self.snapshot) } func configureSupplementaryViewProvider() { let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { headerView, elementKind, indexPath in var contentConfiguration = UIListContentConfiguration.cell() contentConfiguration.text = "Header \(indexPath.section)" headerView.contentConfiguration = contentConfiguration } let footerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionFooter) { [weak self] headerView, elementKind, indexPath in guard let self else { return } var contentConfiguration = UIListContentConfiguration.cell() contentConfiguration.text = self.footerText headerView.contentConfiguration = contentConfiguration } dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in if kind == UICollectionView.elementKindSectionHeader { collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath) } else if kind == UICollectionView.elementKindSectionFooter { collectionView.dequeueConfiguredReusableSupplementary(using: footerRegistration, for: indexPath) } else { nil } } } func createLayout() -> UICollectionViewLayout { UICollectionViewCompositionalLayout { section, layoutEnvironment in var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) config.headerMode = .supplementary config.footerMode = .supplementary return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) } } } What I've tried to do in footerText's didSet: Reconfiguring the supplementary view provider: var footerText = "Initial footer text" { didSet { configureSupplementaryViewProvider() } } Also re-applying the snapshot: var footerText = "Initial footer text" { didSet { configureSupplementaryViewProvider() dataSource.apply(self.snapshot) } } Also re-configuring the items: var footerText = "Initial footer text" { didSet { configureSupplementaryViewProvider() dataSource.apply(self.snapshot, animatingDifferences: true) var snapshot = dataSource.snapshot() snapshot.reconfigureItems(snapshot.itemIdentifiers) dataSource.apply(snapshot, animatingDifferences: false) } }
Topic: UI Frameworks SubTopic: UIKit Tags:
1
0
1.2k
Apr ’24
Lags in UICollectionView accessories when animatedly applying a snapshot unless .disclosureIndicator() is also an accessory
Please run the following UIKit app. It displays a collection view with compositional layout (list layout) and diffable data source. import UIKit class ViewController: UIViewController { var bool = false { didSet { var snapshot = dataSource.snapshot() snapshot.reconfigureItems(snapshot.itemIdentifiers) dataSource.apply(snapshot, animatingDifferences: true) } } var collectionView: UICollectionView! var dataSource: UICollectionViewDiffableDataSource<String, String>! var snapshot: NSDiffableDataSourceSnapshot<String, String> { var snapshot = NSDiffableDataSourceSnapshot<String, String>() snapshot.appendSections(["section"]) snapshot.appendItems(["id"]) 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 { let configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) return UICollectionViewCompositionalLayout.list(using: configuration) } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { [weak self] cell, indexPath, itemIdentifier in guard let self else { return } let _switch = UISwitch() cell.accessories = [ .customView(configuration: .init( customView: _switch, placement: .trailing()) ), // .disclosureIndicator() ] _switch.isOn = bool _switch.addTarget(self, action: #selector(toggleBool), for: .valueChanged) } dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier) } dataSource.apply(self.snapshot, animatingDifferences: false) } @objc func toggleBool() { bool.toggle() } } When you tap on the switch, it lags. If you uncomment .disclosureIndicator() and tap on the switch, it doesn't lag. How do I make it so that the switch doesn't lag without having a disclosure indicator in the cell? Note: while it would solve the issue, I would prefer not to declare the switch at the class level, as I don't want to declare all my controls, which could be quite a lot, at the view controller level in my real app. Edit: declaring the switch at the configureDataSource() level also fixes it, but it would still be inconvenient to declare many switches, say of a list with n elements, at that level.
Topic: UI Frameworks SubTopic: UIKit Tags:
1
0
562
Apr ’24
How to align UITextView text to the title of a table view section
The following UIKit swift app uses a table view with 2 sections. The first section displays a custom cell with a text view, which was added to the cell’s contentView and anchored to the ladder’s layoutMarginsGuide’s top, bottom, leading and trailing anchors. The second section displays a custom cell that is like the former but with a text field instead of a text view. Both sections have titles defined in the tableView(_:cellForRowAt:) method. If you run the app, you will see that the text view’s text is not vertically aligned to it’s section’s title, whereas the text field’s is. How do I align the text view’s text as well? import UIKit class ViewController: UIViewController { let tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() view.addSubview(tableView) tableView.frame = view.bounds tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] tableView.dataSource = self tableView.register(TextViewCell.self, forCellReuseIdentifier: TextViewCell.identifier) tableView.register(TextFieldCell.self, forCellReuseIdentifier: TextFieldCell.identifier) } } extension ViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { 2 } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.section == 0 { tableView.dequeueReusableCell(withIdentifier: TextViewCell.identifier, for: indexPath) } else { tableView.dequeueReusableCell(withIdentifier: TextFieldCell.identifier, for: indexPath) } } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { "Title \(section)" } } extension UITableViewCell { static var identifier: String { "\(Self.self)" } } class TextViewCell: UITableViewCell { let textView: UITextView = { let tv = UITextView() tv.text = "Text view" tv.font = .preferredFont(forTextStyle: .title2) tv.backgroundColor = .systemRed tv.isScrollEnabled = false return tv }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(textView) textView.pin(to: contentView.layoutMarginsGuide) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } class TextFieldCell: UITableViewCell { let textField: UITextField = { let tf = UITextField() tf.text = "Text field" tf.backgroundColor = .systemBlue return tf }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(textField) textField.pin(to: contentView.layoutMarginsGuide) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } Here's how the pin(to:) function is defined in case you're wondering: import UIKit 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 { }
Topic: UI Frameworks SubTopic: UIKit Tags:
1
0
834
Apr ’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?
Topic: UI Frameworks SubTopic: UIKit Tags:
1
0
707
Apr ’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.
Topic: UI Frameworks SubTopic: UIKit
1
0
1.6k
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
930
Jun ’24