Post

Replies

Boosts

Views

Activity

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.
Topic: UI Frameworks SubTopic: UIKit
4
0
1.3k
Jul ’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?
Topic: UI Frameworks SubTopic: UIKit
4
0
590
Aug ’24
SwiftUI previews not loading
Hello, I am a UIKit developer and I would like to try out SwiftUI. Unfortunately, my previews don't load. My situation is like the one described in this blog post: https://forums.developer.apple.com/forums/thread/704036. Unfortunately I can't update Xcode like that developer did. What I've tried: quitting and restarting Xcode, restarting my computer, resetting the simulator, deleting the derived data folder, creating new projects without storage options, test bundles or source control, editing the content view of the initial Hello World file. To be clear, I've just started learning about SwiftUI, just yesterday evening, and the previews have never loaded. Is the problem solvable? If so, how?
3
0
1.3k
Feb ’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?
Topic: UI Frameworks SubTopic: UIKit
3
0
660
Jul ’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
Topic: UI Frameworks SubTopic: UIKit
3
0
488
Aug ’24
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.
Topic: UI Frameworks SubTopic: UIKit Tags:
3
0
577
Aug ’24
How do I inject reference types in a table view cell and reload the row of the said cell without causing a memory leak?
For instance, executing the following code, in which a stepper is injected in a table view cell and the cell is reloaded when the user changes the stepper's value, causes the memory usage to grow pretty quickly (I stopped the simulation at 1GB) when you tap on the stepper. Also the CPU usage jumps straight at 99%, and the UI freezes. Note: I'd like to know exactly what I asked, not how to make a table view cell with a stepper in general. I know that calling reloadData() or reconfigureRows(at:) doesn't cause any of the mentioned issues. Also please don't reply with questions like "Have you tried to use weak references?". The code is short: please reply with a working solution if you can. class ViewController: UIViewController { let tableView = UITableView() let stepper = UIStepper() override func viewDidLoad() { super.viewDidLoad() view.addSubview(tableView) tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") tableView.dataSource = self stepper.addTarget(self, action: #selector(stepperValueChanged), for: .valueChanged) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() tableView.frame = view.bounds } @objc private func stepperValueChanged() { tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic) } } 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.accessoryView = stepper var configuration = cell.defaultContentConfiguration() configuration.text = "\(stepper.value)" cell.contentConfiguration = configuration return cell } }
Topic: UI Frameworks SubTopic: UIKit Tags:
2
0
561
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
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
737
Jul ’24
View a history of project states in Xcode 26
I used to be able to do that by clicking on main, as the documentation says, whereas now nothing happens. I believe this is Xcode 26 Beta 3. MacBook Air M1 8GB, macOS 15.5. I need to get to 220 characters, so I may as well say I've also tried double clicking etc. Does anyone kindly have any suggestions?
2
0
187
Jul ’25
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