Better solution:
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),
// minimum height to show the full vertical scroll indicator
textView.heightAnchor.constraint(equalToConstant: 41.0),
])
Credit: https://stackoverflow.com/a/78935224/22697469.
Again, I can't unaccept the previously accepted answer.
Post
Replies
Boosts
Views
Activity
There's probably a better solution though.
I can't unaccept my answer nor edit it in general though.
Make the cell taller.
Updated cell registration:
let textView = UITextView()
textView.font = .systemFont(ofSize: UIFont.labelFontSize)
cell.contentView.addSubview(textView)
textView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
textView.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor),
textView.heightAnchor.constraint(equalToConstant: textView.font!.pointSize * 4 - 16),
textView.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: cell.directionalLayoutMargins.leading),
textView.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -cell.directionalLayoutMargins.trailing),
cell.contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: textView.font!.pointSize * 4)
])
@DTS Engineer Thank you for your reply, but, given that the body of the mentioned function is as follows, your answer is not pertinent to my question, since the item identifiers of the section are not reconfigured:
let mountains = mountainsController.filteredMountains(with: filter).sorted { $0.name < $1.name }
var snapshot = NSDiffableDataSourceSnapshot<Section, MountainsController.Mountain>()
snapshot.appendSections([.main])
snapshot.appendItems(mountains)
dataSource.apply(snapshot, animatingDifferences: true)
By using the word "reconfigure", I hoped I had made clear that I am looking for a method like reconfigureItems(withIdentifiers:) of NSDiffableDataSourceSnapshot but for NSDiffableDataSourceSectionSnapshot.
@Frameworks Engineer Thank you for your reply.
I highly encourage you to read my updated question on stack overflow, as I suggested doing in a previous reply.
In particular, the question has been made explicit and is also now clearly introduced by a header.
Could you please answer the question by modifying the provided code?
Playing around with configurationUpdateHandler didn't do it for me.
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.)
Here follows a more up to date version of the post, improved for clarity.
Since it looks like I can no longer edit nor delete my original question on this forum and I don't want to make a duplicate, it seems appropriate to me to link to a forum that lets you: https://stackoverflow.com/questions/78830896/setting-the-backgroundcolor-property-of-uibackgroundconfiguration-breaks-the-d
PS I've also tried and didn't work cell.defaultBackgroundConfiguration().backgroundColorTransformer.
And using collection view controllers (and setting clearsSelectionOnViewWillAppear to false).
@DTS Engineer Hello,
Thank you for your reply.
I feel like the following is a better answer, since it gives some explanation:
"You only need the ObservableObject conformance if you need a SwiftUI view to be auto-updated when an @Published property of your object changes. You also need to store the object as @ObservedObject or @StateObject to get the auto-updating behaviour and these property wrappers actually require the ObservableObject conformance.
As you're using UIKit and hence don't actually get any automatic view updates, you don't need the ObservableObject conformance."
Credit: https://stackoverflow.com/a/78812534/22697469.
As per your suggestion to use the @Observable macro, unfortunately, I can't target just iOS 17 or above.
@DTS Engineer I see, but it looks like this approach is no longer recommended: I can't find "The Keys to a Better Text Input Experience", the WWDC session that is mentioned in "Your guide to keyboard layout".
Considering that it is impossible that Apple does not explain you how to comprehensively (as explained later) handle the keyboard being laid out if your app uses a collection view controller and that I can't find such documentation, do you happen to have a link to it?
In particular, your solution does not take into account the fact that the keyboard frame might change, the fact that the text view might have input accessory views, that the view controller might be embedded in a collection view controller, that the app scene might be floating on an iPad or that there have been changes in the timing with which the keyboard is shown in iOS 17 with respect to earlier iOS versions.
All of these cases and more are handled by the up to date API.
I can't delete this reply
Ok you may as well display markers like this:
ForEach(mapItems, id: \.self) {
Marker(item: $0)
}
where mapItems is an array of type [MKMapItem].
Complete code:
import SwiftUI
import MapKit
struct MapView: View {
@State private var mapCameraPosition: MapCameraPosition = .automatic
@State private var mapSelection: MKMapItem?
let mapItems = [MKMapItem(
placemark: .init(
coordinate: .init(
latitude: 37,
longitude: -122
)
)
)]
var body: some View {
Map(
position: $mapCameraPosition,
selection: $mapSelection
) {
ForEach(mapItems, id: \.self) {
Marker(item: $0)
}
}
.onChange(of: mapSelection) { _, newSelection in
print("onChange")
if let _ = newSelection {
print("selected")
}
}
}
}
Change
var configuration: UIContentConfiguration
to
var configuration: UIContentConfiguration {
didSet {
configureSubviews()
}
}
and remove the subviews in the content view initialization:
subviews.forEach {
$0.removeFromSuperview()
}
Use UIListContentConfiguration:
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
var contentConfig = CustomListContentConfiguration()
contentConfig.placeholder = "Placeholder"
cell.contentConfiguration = contentConfig
}
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)
}
}
class CustomListContentConfiguration: UIContentConfiguration {
var placeholder: String?
func makeContentView() -> UIView & UIContentView {
return CustomListContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Self {
// Not handling state changes in this example, so just return self
return self
}
}
class CustomListContentView: UIView, UIContentView {
var configuration: UIContentConfiguration
init(configuration: UIContentConfiguration) {
self.configuration = configuration
super.init(frame: .zero)
configureSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureSubviews() {
guard let config = configuration as? CustomListContentConfiguration else { return }
let leadingView = UIView()
leadingView.backgroundColor = .systemRed
let textField = UITextField()
textField.placeholder = config.placeholder
textField.font = .systemFont(ofSize: 100)
addSubview(leadingView)
addSubview(textField)
leadingView.translatesAutoresizingMaskIntoConstraints = false
textField.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
leadingView.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor),
leadingView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
leadingView.widthAnchor.constraint(equalTo: layoutMarginsGuide.heightAnchor),
leadingView.heightAnchor.constraint(equalTo: layoutMarginsGuide.heightAnchor),
textField.topAnchor.constraint(equalTo: topAnchor),
textField.bottomAnchor.constraint(equalTo: bottomAnchor),
textField.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 16),
textField.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
])
}
}
If you use cell registrations instead of supplementary registrations, the keyboard doesn't dismiss for any reason and you can also get rid of the dispatch blocks:
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(["search bar id"])
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 = .firstItemInSection
return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
}
}
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { _, _, _ in }
let searchBarCellRegistration = UICollectionView.CellRegistration<SearchBarCell, String>{ cell, indexPath, itemIdentifier in
cell.searchBar.delegate = self
}
dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
if indexPath.row == 0 {
collectionView.dequeueConfiguredReusableCell(using: searchBarCellRegistration, for: indexPath, item: itemIdentifier)
} else {
collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
}
}
dataSource.apply(self.snapshot, animatingDifferences: false)
}
}
extension ViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
filteredWords = words.filter { $0.hasPrefix(searchText) }
}
func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
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
}
}
Basically don't use supplementary views unless absolutely necessary: it also seems that there's no declarative way to update them without losing animations (Apple's guided project "Modern Collection Views" doesn't seem to provide a solution, nor do these posts: https://stackoverflow.com/questions/78311570/how-do-i-update-a-collection-view-supplementary-view-without-giving-up-on-animat, https://forums.developer.apple.com/forums/thread/749847).
I can't edit the post here anymore so go check out the stackoverflow.com post which is up to date: https://stackoverflow.com/questions/78373006/handle-keyboard-layout-in-ios-15-uikit-app-with-collection-view-using-the-moder