Post

Replies

Boosts

Views

Created

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
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.)
Topic: UI Frameworks SubTopic: UIKit
1
0
520
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
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
578
Aug ’24
How to scale a SwiftUI view to fit its parent
Code that reproduces the issue import SwiftUI @main struct KeyboardLayoutProblemApp: App { var body: some Scene { WindowGroup { iOSTabView() } } } struct iOSTabView: View { var body: some View { TabView { GameView() .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) .tabItem { Label("Play", systemImage: "gamecontroller.fill") } } } } struct GameView: View { var body: some View { VStack { Text("Play") Spacer() KeyboardView() } .padding() } } struct KeyboardView: View { let firstRowLetters = "qwertyuiop".map { $0 } let secondRowLetters = "asdfghjkl".map { $0 } let thirdRowLetters = "zxcvbnm".map { $0 } var body: some View { VStack { HStack { ForEach(firstRowLetters, id: \.self) { LetterKeyView(character: $0) } } HStack { ForEach(secondRowLetters, id: \.self) { LetterKeyView(character: $0) } } HStack { ForEach(thirdRowLetters, id: \.self) { LetterKeyView(character: $0) } } } .padding() } } struct LetterKeyView: View { let character: Character var width: CGFloat { height*0.8 } @ScaledMetric(relativeTo: .title3) private var height = 35 var body: some View { Button { print("\(character) pressed") } label: { Text(String(character).capitalized) .font(.title3) .frame(width: self.width, height: self.height) .background { RoundedRectangle(cornerRadius: min(width, height)/4, style: .continuous) .stroke(.gray) } } .buttonStyle(PlainButtonStyle()) } } Problem GameView doesn't fit its parent view: Question How do I make GameView be at most as big as its parent view? What I've tried and didn't work GameView() .frame(maxWidth: .infinity, maxHeight: .infinity) GeometryReader { geometry in GameView() .frame(maxWidth: geometry.size.width, maxHeight: geometry.size.height) } GameView() .clipped() GameView() .layoutPriority(1) GameView() .scaledToFit() GameView() .minimumScaleFactor(0.01) GameView() .scaledToFill() .minimumScaleFactor(0.5) I'm not using UIScreen.main.bounds.width because I'm trying to build a multi-platform app.
Topic: UI Frameworks SubTopic: SwiftUI
0
0
89
Jul ’25
Make a grid of `TextField`s in SwiftUI
Code that reproduces the issue import SwiftUI @main struct TextFieldsGridApp: App { @State private var controller = Controller() var body: some Scene { WindowGroup { GridView() .environment(controller) } } } struct GridView: View { @Environment(Controller.self) private var c var body: some View { VStack { ForEach(0..<c.strings.count, id: \.self) { r in HStack { ForEach(0..<4, id: \.self) { c in TextField( "", text: c.strings[r][c] ) .textFieldStyle(.roundedBorder) } } } } .padding() } } #Preview { GridView() .environment(Controller()) } @Observable class Controller { private(set) var strings: [[String]] = Array( repeating: Array(repeating: "A", count: 4), count: 4, ) } Error Cannot convert value of type 'Range<Int>' to expected argument type 'Binding<C>', caused by ForEach(0..<4, id: \.self) { c in. Which I do not get if I say, for example: struct GridView: View { @State private var text = "H" // ... TextField("", text: $text) Environment MacBook Air M1 8GB iOS Sequoia 15.5 Xcode 16.4 Preview: iPhone 16 Pro, iOS 18.5.
Topic: UI Frameworks SubTopic: SwiftUI
1
0
132
Jul ’25
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