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)
}
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
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?
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.
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:).
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)
}
}
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.
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 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?
I can't fit the post here, so here's the link to it on stack overflow: https://stackoverflow.com/questions/78419812/reconfigure-custom-content-configuration
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
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.
Please note that for this app to be able show you your location on the map you need to enable it to ask for permission to track the user's location, in the project's targets' info:
Here's also a video that illustrates the lag: https://youtube.com/shorts/DSl-umGxs20?feature=share.
That being said, if you run this SwiftUI app and allow it to track your location, tap the MapUserLocationButton and press the buttons at the bottom, you'll see that the map lags:
import SwiftUI
import MapKit
import CoreLocation
struct ContentView: View {
let currentLocationManager = CurrentUserLocationManager()
let mapLocationsManager = MapLocationsManager()
@State private var mapCameraPosition: MapCameraPosition = .automatic
var body: some View {
Map(
position: $mapCameraPosition
)
.safeAreaInset(edge: .bottom) {
VStack {
if mapLocationsManager.shouldAllowSearches {
Button("First button") {
mapLocationsManager.shouldAllowSearches = false
}
}
Button("Second button") {
mapLocationsManager.shouldAllowSearches = true
}
}
.frame(maxWidth: .infinity)
.padding()
}
.mapControls {
MapUserLocationButton()
}
}
}
@Observable class MapLocationsManager {
var shouldAllowSearches = false
}
// MARK: - Location related code -
class CurrentUserLocationManager: NSObject {
var locationManager: CLLocationManager?
override init() {
super.init()
startIfNecessary()
}
func startIfNecessary() {
if locationManager == nil {
locationManager = .init()
locationManager?.delegate = self
} else {
print(">> \(Self.self).\(#function): method called redundantly: locationManager had already been initialized")
}
}
}; extension CurrentUserLocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
}; extension CurrentUserLocationManager {
private func checkLocationAuthorization() {
guard let locationManager else { return }
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
print("Your location is restricted")
case .denied:
print("Go into setting to change it")
case .authorizedAlways, .authorizedWhenInUse, .authorized:
// locationManager.startUpdatingLocation()
break
@unknown default:
break
}
}
}
I've also tried in a UIKit app (by just embedding ContentView in a view controller), with the same results:
import UIKit
import MapKit
import SwiftUI
import CoreLocation
class ViewController: UIViewController {
let currentLocationManager = CurrentUserLocationManager()
override func viewDidLoad() {
super.viewDidLoad()
currentLocationManager.startIfNecessary()
let hostingController = UIHostingController(
rootView: MapView()
)
addChild(hostingController)
hostingController.view.frame = view.bounds
hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
}
}
extension ViewController { struct MapView: View {
let mapLocationsManager = MapLocationsManager()
@State private var mapCameraPosition: MapCameraPosition = .automatic
var body: some View {
Map(
position: $mapCameraPosition
)
.safeAreaInset(edge: .bottom) {
VStack {
if mapLocationsManager.shouldAllowSearches {
Button("First button") {
mapLocationsManager.shouldAllowSearches = false
}
}
Button("Second button") {
mapLocationsManager.shouldAllowSearches = true
}
}
.frame(maxWidth: .infinity)
.padding()
}
.mapControls {
MapUserLocationButton()
}
}
} }
extension ViewController { @Observable class MapLocationsManager {
var shouldAllowSearches = false
} }
class CurrentUserLocationManager: NSObject {
var locationManager: CLLocationManager?
func startIfNecessary() {
if locationManager == nil {
locationManager = .init()
locationManager?.delegate = self
} else {
print(">> \(Self.self).\(#function): method called redundantly: locationManager had already been initialized")
}
}
}; extension CurrentUserLocationManager: CLLocationManagerDelegate {
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
checkLocationAuthorization()
}
}; extension CurrentUserLocationManager {
private func checkLocationAuthorization() {
guard let locationManager else { return }
switch locationManager.authorizationStatus {
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
case .restricted:
print("Your location is restricted")
case .denied:
print("Go into setting to change it")
case .authorizedAlways, .authorizedWhenInUse, .authorized:
// locationManager.startUpdatingLocation()
break
@unknown default:
break
}
}
}
If you don't center the map in the user's location, you might see an occasiona lag, but it seems to me that this only happens once.
How do I avoid these lags altogether?
Xcode 15.4
If you run the following UIKit app and tap on the button, you can see that it only updates its color if you hold on it for a bit, instead of immediately (as happens in the second app) (iOS 17.5, iPhone 15 Pro simulator, Xcode 15.4).
This app consists of a view controller with a table view with one cell, which has a CheckoutButton instance constrained to its contentView top, bottom, leading and trailing anchors.
The checkout button uses UIButton.Configuration to set its appearance, and update it based on its state.
import UIKit
class ViewController: UIViewController {
let tableView = UITableView()
let checkoutButton = CheckoutButton()
override func viewDidLoad() {
super.viewDidLoad()
// table view setup
view.addSubview(tableView)
tableView.frame = view.bounds
tableView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.contentView.addSubview(checkoutButton)
checkoutButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
checkoutButton.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
checkoutButton.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor),
checkoutButton.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor),
checkoutButton.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)
])
return cell
}
}
class CheckoutButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
var configuration = UIButton.Configuration.plain()
var attributeContainer = AttributeContainer()
attributeContainer.font = .preferredFont(forTextStyle: .headline)
attributeContainer.foregroundColor = .label
configuration.attributedTitle = .init("Checkout", attributes: attributeContainer)
self.configuration = configuration
let configHandler: UIButton.ConfigurationUpdateHandler = { button in
switch button.state {
case .selected, .highlighted:
button.configuration?.background.backgroundColor = .systemCyan
case .disabled:
button.configuration?.background.backgroundColor = .systemGray4
default:
button.configuration?.background.backgroundColor = .systemBlue
}
}
self.configurationUpdateHandler = configHandler
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In this second app, instead, the selection of the button is immediately reflected in its appearance:
import UIKit
class ViewController: UIViewController {
let button = CheckoutButton()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.widthAnchor.constraint(equalToConstant: 300),
button.heightAnchor.constraint(equalToConstant: 44)
])
}
}
class CheckoutButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
var configuration = UIButton.Configuration.plain()
var attributeContainer = AttributeContainer()
attributeContainer.font = .preferredFont(forTextStyle: .headline)
attributeContainer.foregroundColor = .label
configuration.attributedTitle = .init("Checkout", attributes: attributeContainer)
self.configuration = configuration
let configHandler: UIButton.ConfigurationUpdateHandler = { button in
switch button.state {
case .selected, .highlighted:
button.configuration?.background.backgroundColor = .systemCyan
case .disabled:
button.configuration?.background.backgroundColor = .systemGray4
default:
button.configuration?.background.backgroundColor = .systemBlue
}
}
self.configurationUpdateHandler = configHandler
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
This app consists of a view controller with just a button: no table view unlike in the first app.
How do I make the button show its selection as soon as it's tapped, no matter if it's in a table view cell or on its own?
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
Here is a screenshot of the app:
And here follows the code: it's a view controller with a collection view with plain list layout and a diffable data source.
It has 1 section with 2 rows.
The 1st row's content configuration is UIListContentConfiguration.cell(), and it has some text.
The 2nd row has an empty content configuration instead.
As you can see, if you run the app, the separator insets are different for the two rows.
Did I make a mistake?
If this is expected behavior instead, is there an easy fix?
import UIKit
class ViewController: UIViewController {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<String, String>!
var snapshot: NSDiffableDataSourceSnapshot<String, String> {
var snapshot = NSDiffableDataSourceSnapshot<String, String>()
snapshot.appendSections(["main"])
snapshot.appendItems(["one", "two"])
return snapshot
}
override func viewDidLoad() {
super.viewDidLoad()
configureHierarchy()
configureDataSource()
}
func configureHierarchy() {
collectionView = .init(
frame: view.bounds,
collectionViewLayout: createLayout()
)
view.addSubview(collectionView)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
func createLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout { section, layoutEnvironment in
let config = UICollectionLayoutListConfiguration(appearance: .plain)
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
}
}
func configureDataSource() {
let firstCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
var contentConfiguration = UIListContentConfiguration.cell()
contentConfiguration.text = "Hello"
cell.contentConfiguration = contentConfiguration
}
let secondCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
}
let emptyCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
}
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
switch itemIdentifier {
case "one":
collectionView.dequeueConfiguredReusableCell(using: firstCellRegistration, for: indexPath, item: itemIdentifier)
case "two":
collectionView.dequeueConfiguredReusableCell(using: secondCellRegistration, for: indexPath, item: itemIdentifier)
default:
collectionView.dequeueConfiguredReusableCell(using: emptyCellRegistration, for: indexPath, item: itemIdentifier)
}
}
dataSource.apply(self.snapshot, animatingDifferences: false)
}
}
Xcode 15.4, iPhone 15 Pro simulator, iOS 17.5, MacBook Air M1 8GB, macOS 14.5.
Topic:
UI Frameworks
SubTopic:
UIKit