The following is a UIKit app with a collection view with one section, whose supplementary view sports a search bar.
When you type, it often resigns first responder.
import UIKit
class ViewController: UIViewController {
let words = ["foo", "bar"]
var filteredWords = ["foo", "bar"] {
didSet {
dataSource.apply(self.snapshot)
}
}
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<String, String>!
var snapshot: NSDiffableDataSourceSnapshot<String, String> {
var snapshot = NSDiffableDataSourceSnapshot<String, String>()
snapshot.appendSections(["main"])
snapshot.appendItems(filteredWords)
return snapshot
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = .init(title: "Apply", style: .plain, target: self, action: #selector(apply))
configureHierarchy()
configureDataSource()
}
@objc func apply() {
dataSource.apply(self.snapshot)
}
func configureHierarchy() {
collectionView = .init(frame: .zero, collectionViewLayout: createLayout())
view.addSubview(collectionView)
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
func createLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { section, layoutEnvironment in
var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
config.headerMode = .supplementary
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
}
}
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { _, _, _ in }
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
let searchbarHeaderRegistration = UICollectionView.SupplementaryRegistration<SearchBarCell>(elementKind: UICollectionView.elementKindSectionHeader) { cell, elementKind, indexPath in
cell.searchBar.delegate = self
}
dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
collectionView.dequeueConfiguredReusableSupplementary(using: searchbarHeaderRegistration, for: indexPath)
}
dataSource.apply(self.snapshot, animatingDifferences: false)
}
}
extension ViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
filteredWords = words.filter { $0.hasPrefix(searchText) }
}
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
filteredWords = words
}
}
}
class SearchBarCell: UICollectionViewListCell {
let searchBar = UISearchBar()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(searchBar)
searchBar.pinToSuperview()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension UIView {
func pin(
to object: CanBePinnedTo,
top: CGFloat = 0,
bottom: CGFloat = 0,
leading: CGFloat = 0,
trailing: CGFloat = 0
) {
self.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.topAnchor.constraint(equalTo: object.topAnchor, constant: top),
self.bottomAnchor.constraint(equalTo: object.bottomAnchor, constant: bottom),
self.leadingAnchor.constraint(equalTo: object.leadingAnchor, constant: leading),
self.trailingAnchor.constraint(equalTo: object.trailingAnchor, constant: trailing),
])
}
func pinToSuperview(
top: CGFloat = 0,
bottom: CGFloat = 0,
leading: CGFloat = 0,
trailing: CGFloat = 0,
file: StaticString = #file,
line: UInt = #line
) {
guard let superview = self.superview else {
print(">> \(#function) failed in file: \(String.localFilePath(from: file)), at line: \(line): could not find \(Self.self).superView.")
return
}
self.pin(to: superview, top: top, bottom: bottom, leading: leading, trailing: trailing)
}
func pinToSuperview(constant c: CGFloat = 0, file: StaticString = #file, line: UInt = #line) {
self.pinToSuperview(top: c, bottom: -c, leading: c, trailing: -c, file: file, line: line)
}
}
@MainActor
protocol CanBePinnedTo {
var topAnchor: NSLayoutYAxisAnchor { get }
var bottomAnchor: NSLayoutYAxisAnchor { get }
var leadingAnchor: NSLayoutXAxisAnchor { get }
var trailingAnchor: NSLayoutXAxisAnchor { get }
}
extension UIView: CanBePinnedTo { }
extension UILayoutGuide: CanBePinnedTo { }
extension String {
static func localFilePath(from fullFilePath: StaticString = #file) -> Self {
URL(fileURLWithPath: "\(fullFilePath)").lastPathComponent
}
}
By the way if you remove the dispatch blocks, the app simply crashes complaining that you're trying to apply datasource snapshots from both the main queue and other queues.
Xcode 15.3 iOS 17.4 simulator, MacBook Air M1 8GB macOS Sonoma 14.4.1
Post
Replies
Boosts
Views
Activity
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 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 allow the UI to adjust to text view changes on iOS 16+ only.
Recommended, modern, approach:
Not recommended, old, approach:
I've tried to say view.keyboardLayoutGuide.topAnchor.constraint(equalTo: textView.bottomAnchor).isActive = true in the text view cell registration, as well as view.keyboardLayoutGuide.topAnchor.constraint(equalTo: collectionView.bottomAnchor).isActive = true in viewDidLoad(), but both of these approaches fail (Xcode 15.3 iPhone 15 Pro simulator with iOS 17.4, physical iPhone SE on iOS 15.8).
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 { }
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.
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)
}
}
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:).
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.
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?
I can't override the show(_:sender:) of UIViewController or else my app freezes when the method is called (Xcode 15.3 simulator, iOS 17.4, macOS Sonoma 14.3.1, MacBook Air M1 8GB).
Is there any workaround?
override func show(_ vc: UIViewController, sender: Any?) {
super.show(vc, sender: sender)
}
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)
}
}
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
}
}
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?
The location of my derived data folder differs between Xcode and Finder.
How do I change Xcode's path to match my Finder path?
MacOS Sonoma 14.2.1, MacBook Air M1, 8GB
Xcode 15.2
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?
In the following code, test 1 (test_postNotification) fails while test 2 (test_notificationsArePostedOnTheMainQueue) passes.
What concerns me, though, is that if I substitute the lines "let result = XCTWaiter.wait(for: [expectation], timeout: 0); XCTAssertEqual(result, .timedOut)" of test 2 with "wait(for: [expectation], timeout: 0.1)", then test number 1 passes.
I have cleaned the build folder and restarted Xcode and my computer, but the issue persists.
This concerns me because I would have said that the tests of the NotificationPosterTests class were isolated, but apparently they are not, since changing test 2 makes test 1 go from failing to passing.
Is this expected behavior?
import Foundation
import XCTest
extension Notification.Name {
static let menuPostRequest = Notification.Name("menuPostRequest")
static let editingOrderError = Notification.Name("editingOrderError")
}
class NotificationPoster {
let notificationCenter: NotificationCenter
init(notificationCenter: NotificationCenter = .default) {
self.notificationCenter = notificationCenter
}
func postNotification(_ notification: Notification) {
let _notificationCenter = notificationCenter // you can't use optional chaining nor conditional unwrapping on self to reference self.notificationCenter in the dispatch block because self is nil when self.postNotification(_:) is called
DispatchQueue.main.async {
_notificationCenter.post(notification)
}
}
}
final class NotificationPosterTests: XCTestCase {
private var sut: NotificationPoster!
private var notificationCenter: NotificationCenter!
override func setUp() {
super.setUp()
notificationCenter = NotificationCenter()
sut = NotificationPoster(notificationCenter: notificationCenter)
}
override func tearDown() {
notificationCenter = nil
sut = nil
super.tearDown()
}
func test_postNotification() {
let notification = Notification(name: .menuPostRequest)
let expectation = XCTNSNotificationExpectation(
name: notification.name,
object: notification.object,
notificationCenter: notificationCenter
)
sut.postNotification(notification)
wait(for: [expectation], timeout: 0.1) // don't make it 0.01
}
func test_notificationsArePostedOnTheMainQueue() {
let notification = Notification(name: .editingOrderError)
let expectation = XCTNSNotificationExpectation(
name: notification.name,
object: notification.object,
notificationCenter: notificationCenter
)
sut.postNotification(notification)
let result = XCTWaiter.wait(for: [expectation], timeout: 0)
XCTAssertEqual(result, .timedOut)
}
}