Swift, iOS15, UIKit, CollectionView header issue - ios

I am testing iOS15 and some new functionalities of UIKit. I've encountered some issues, not sure how to solve them. I did not change that code. This is just a piece of code that worked perfectly with the iOS 14, now after updating my target, it throws an error.
Xcode crashes the moment when my custom header for the UICollectionView of type UICollectionElementKindSectionHeader is being returned for the dataSource. Here is my code:
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Follower>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, followers) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FollowerCell.reuseId, for: indexPath) as! FollowerCell
cell.set(on: followers)
return cell
})
dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) in
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: FollowersCollectionHeaderView.reuseId,
for: indexPath) as! FollowersCollectionHeaderView
header.set(with: self.user)
return header
}
}
The log says:
the view returned from
-collectionView:viewForSupplementaryElementOfKind:atIndexPath: does not match the element kind it is being used for. When asked for a view
of element kind 'FollowersCollectionHeaderView' the data source
dequeued a view registered for the element kind
'UICollectionElementKindSectionHeader'.
I did cast UICollectionElementKindSectionHeader to FollowersCollectionHeaderView, therefore I am not sure what is the issue here.
I've watched WWDC21 what's new in UIKit but haven't seen any mentioning of any change for that particular code.
Any suggestions, what to fix in that code?

Here is the partial solution that I came up with. Apple recommends using object's ID as a reference for the collectionView cells.
enum Section { case main }
var dataSource: UICollectionViewDiffableDataSource<Section, Follower.ID>!
// MARK: - Collection View configurations
fileprivate lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: UIHelper.createCompositionalLayout())
collectionView.delegate = self
collectionView.backgroundColor = .systemBackground
collectionView.register(FollowersCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: FollowersCollectionHeaderView.reuseId)
view.addSubview(collectionView)
return collectionView
}()
fileprivate lazy var snapshot: NSDiffableDataSourceSnapshot<Section, Follower.ID> = {
var snapshot = NSDiffableDataSourceSnapshot<Section, Follower.ID>()
snapshot.appendSections([.main])
let itemIdentifiers = followers.map { $0.id }
snapshot.appendItems(itemIdentifiers, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true)
return snapshot
}()
fileprivate func updateData(with followers: [Follower]) {
snapshot = NSDiffableDataSourceSnapshot<Section, Follower.ID>()
snapshot.appendSections([.main])
let itemIdentifiers = followers.map { $0.id }
snapshot.appendItems(itemIdentifiers, toSection: .main)
dataSource.apply(snapshot, animatingDifferences: true)
}
fileprivate func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<FollowerCell, Follower.ID> { [weak self]
cell, indexPath, followerID in
guard let self = self else { return }
let followerArray = self.followers.filter { $0.id == followerID }
if let follower = followerArray.first {
cell.set(on: follower)
}
}
dataSource = UICollectionViewDiffableDataSource<Section, Follower.ID>(collectionView: collectionView) {
collectionView, indexPath, itemIdentifier in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration,
for: indexPath,
item: itemIdentifier)
}
let headerRegistration = createSectionHeaderRegistration()
dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath)
}
}
fileprivate func createSectionHeaderRegistration() -> UICollectionView.SupplementaryRegistration<FollowersCollectionHeaderView> {
return UICollectionView.SupplementaryRegistration<FollowersCollectionHeaderView>(
elementKind: FollowersCollectionHeaderView.reuseId) { [weak self] supplementaryView, elementKind, indexPath in
guard let self = self else { return }
supplementaryView.set(with: self.user)
}
}

Related

How to call Async Task inside view did load for Collection View?

So I am calling this into a colleciton view. This example is for SwiftUI but I am creationg a collection view with a list layout.
Example: https://hygraph.com/blog/swift-with-hygraph
I have done all the other setup which you can find in the gist but I keep getting these errors:
[Common] Snapshot request 0x60000348db30 complete with error: <NSError: 0x600003489020; domain: FBSSceneSnapshotErrorDomain; code: 4; reason: "an unrelated condition or state was not satisfied">
My Gist: https://github.com/ImranRazak1/HygraphSwift
View Controller
import UIKit
class ViewController: UIViewController {
enum Section {
case main
}
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section,Product>?
var products: [Product] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
navigationItem.title = "Products"
view.backgroundColor = .white
//Uncomment when needed
configureHierarchy()
configureDataSource()
collectionView.register(ListCell.self, forCellWithReuseIdentifier: "ListCell")
}
func loadProducts() async {
self.products = await APIService().listProducts()
}
func configure<T: SelfConfiguringCell>(with product: Product, for indexPath: IndexPath) -> T {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ListCell", for: indexPath) as? T else {
fatalError("Unable to dequeue Cell")
}
cell.configure(with: product)
return cell
}
}
extension ViewController {
private func createLayout() -> UICollectionViewLayout {
let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
return UICollectionViewCompositionalLayout.list(using: config)
}
}
extension ViewController {
private func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
collectionView.delegate = self
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Product> { (cell, indexPath, product) in
var content = cell.defaultContentConfiguration()
content.text = "\(product.name)"
cell.contentConfiguration = content
}
dataSource = UICollectionViewDiffableDataSource<Section, Product>(collectionView: collectionView) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Product) -> UICollectionViewCell? in
return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
}
//inital data
var snapshot = NSDiffableDataSourceSnapshot<Section, Product>()
snapshot.appendSections([.main])
//Problem loading this information
snapshot.appendItems(products, toSection: .main)
dataSource?.apply(snapshot, animatingDifferences: false)
Task {
do {
await self.loadProducts()
snapshot.appendItems(products, toSection: .main)
await dataSource?.apply(snapshot, animatingDifferences: false)
}
}
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
}
}
Best,
Imran

RxSwift DisposeBag and UITableViewCell

I have a UITableViewCell, and it contain a UICollectionView.
I put data for collection view in dataSource's tableview.
class HomeTableViewCell: UITableViewCell {
var disposeBag = DisposeBag()
#IBOutlet weak var collectionItems: UICollectionView!
var listItem: PublishSubject<HomeResultModel> = PublishSubject<HomeResultModel>()
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfMenuData>(configureCell: {(_, _, _, _) in
fatalError()
})
override func awakeFromNib() {
super.awakeFromNib()
setup()
setupBinding()
}
override func prepareForReuse() {
disposeBag = DisposeBag()
}
func setup() {
collectionItems.register(UINib(nibName: MenuCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: MenuCollectionViewCell.identifier)
collectionItems.register(UINib(nibName: RecipeCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: RecipeCollectionViewCell.identifier)
collectionItems.rx.setDelegate(self).disposed(by: disposeBag)
collectionItems.rx.modelAndIndexSelected(HomeListModel.self).subscribe { (model, index) in
if let recipesID = model.recipesID {
tlog(tag: self.TAG, sLog: "recipesID : \(recipesID)")
self.recipeIdSelected.onNext("\(recipesID)")
}
}.disposed(by: disposeBag)
dataSource.configureCell = { (dataSource, collectionView, indexPath, item) -> UICollectionViewCell in
let cellType = dataSource.sectionModels.first?.type ?? .recipeCell
if cellType == .menuCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MenuCollectionViewCell.identifier, for: indexPath) as! MenuCollectionViewCell
cell.showInfo(item)
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecipeCollectionViewCell.identifier, for: indexPath) as! RecipeCollectionViewCell
cell.showInfo(item.recipesName ?? "", imageLink: item.imageThumb ?? "")
return cell
}
}
}
func setupBinding() {
listItem.map { model -> [SectionOfMenuData] in
let modelID = try JSONEncoder().encode(model.id)
let modelResultID = String.init(data: modelID, encoding: .utf8)!
return [SectionOfMenuData(type: modelResultID == "0" ? .menuCell : .recipeCell, id: modelResultID, items: model.list)]
}.bind(to: collectionItems.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
}
I pass data for tableviewcell with code:
let dataSource = RxTableViewSectionedReloadDataSource<SectionOfHome> { dataSource, tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.identifier, for: indexPath) as! HomeTableViewCell
cell.listItem.onNext(item)
return cell
}
First show it ok, but when scroll tableview, I reset DisposeBag in:
func prepareForReuse{
disposeBag = DisposeBag()
}
and so tableview show blank.
How wrong was I in this regard?
Your subscription to listItems is getting disposed when the cell is reused and is never recreated. You should run the one-time tasks in awakeFromNib and move individual cell specific bindings to a function that you can call after resetting the disposeBag in prepareForReuse.

Using UICollectionLayoutListConfiguration with storyboards

Can you use the new UICollectionLayoutListConfiguration API in iOS 14 with collection views in storyboards?
I have a UICollectionViewController in a storyboard, which I configure with a custom list layout below:
var config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView.setCollectionViewLayout(layout, animated: false)
collectionView.dataSource = dataSource
(In storyboards collection views can only be set with flow or custom layouts)
This uses a standard diffable data source:
return UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView, indexPath, item) in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "SomeCell",
for: indexPath
) as? SomeCell else {
fatalError("Couldn't dequeue cell \(reuseIdentifier)")
}
cell.setItem(item)
return cell
}
However I get some really weird behaviour, such as IBOutlets being nil when rotating the screen, despite everything working fine before rotation.
I've found no good way to debug what is going on here, the stack trace looks correct and the cell's class is initilised and this is running on the main thread.
I have replicated each and every step of your code and the end result is as expected for me.
Here's end result in portrait mode
Here's end result in landscape mode
What I intended to do was to show a list of numbers in the collection view, here are the necessary files for your reference:
The UICollectionViewController
import UIKit
private let reuseIdentifier = "SomeCell"
enum Section {
case main
}
class SampleCollectionViewController: UICollectionViewController {
var dataSource: UICollectionViewDiffableDataSource<Section, Int>!
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.setCollectionViewLayout(getLayout(), animated: false)
configureDataSource()
}
func getLayout() -> UICollectionViewCompositionalLayout {
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
return layout
}
func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: self.collectionView, cellProvider: { (collectionView, indexPath, number) -> UICollectionViewListCell? in
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as? SomeCell else {
fatalError("Cannot create cell")
}
cell.label.text = number.description
return cell
})
var initialSnapshot = NSDiffableDataSourceSnapshot<Section, Int>()
initialSnapshot.appendSections([.main])
initialSnapshot.appendItems(Array(1...100), toSection: .main)
dataSource.apply(initialSnapshot, animatingDifferences: false)
}
}
The UICollectionViewListCell
import UIKit
class SomeCell: UICollectionViewListCell {
#IBOutlet weak var label: UILabel!
override func updateConstraints() {
super.updateConstraints()
label.textColor = .blue
}
}
The storyboard setup:
I did all this following standard steps nothing fancy here
Important Note: One potential place where I found your implementation different was that you have dequeued a cell withReuseIdentifier as "SomeCell" but the cell class you have shown throwing error is TextCell if you could provide more specifics on the same then it would be helpful to narrow down the error you are facing.
Here is simplified demo. Much of based on somehow replicated your code.
Tested with Xcode 12 / iOS 14
struct SomeItem: Hashable {
let id = UUID()
var value: String
}
class SomeCell: UICollectionViewCell {
#IBOutlet weak var label: UILabel!
var item: SomeItem!
func setItem(_ item: SomeItem) {
self.item = item
label.text = item.value
}
override func awakeFromNib() { // called when all outlets connected
super.awakeFromNib()
label.textColor = .blue
}
}
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
let config = UICollectionLayoutListConfiguration(appearance: .plain)
let layout = UICollectionViewCompositionalLayout.list(using: config)
collectionView.setCollectionViewLayout(layout, animated: false)
collectionView.dataSource = dataSource
}
lazy var dataSource = {
UICollectionViewDiffableDataSource<Int, SomeItem>(collectionView: self.collectionView) { (collectionView, indexPath, item) in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "SomeCell",
for: indexPath
) as? SomeCell else {
fatalError("Couldn't dequeue cell")
}
cell.setItem(item)
return cell
}
}()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
var snapshot = NSDiffableDataSourceSnapshot<Int, SomeItem>()
snapshot.appendSections([0])
snapshot.appendItems([SomeItem(value: "Jan"), SomeItem(value: "Fab"), SomeItem(value: "Mar")])
dataSource.apply(snapshot)
}
}
Nothing special in storyboard - only applied custom classes for view controller and cell (label is centred for simplicity) and made custom collection view layout:

Crash number of items in section 0 when there are only 0 sections in the collection view

I want to learn using UICollectionViewDiffableDataSource using Pinterest Layout, but when I try to running my simulator. it crash and give me a message
request for number of items in section 0 when there are only 0 sections in the collection view
I did the Pinterest layout using raywenderlich tutorial , when I stumble in google the problem cause is the Pinterest layout. but before I use the diffableDataSource it works fine, but after using diffableDataSource it crash. where do I do wrong? can you help me, this is my code for diffableDataSource
// This is in my PhotoViewController
enum Section {
case main
}
let layout = PinterestLayout()
var dataSource: UICollectionViewDiffableDataSource<Section, PhotoItem>!
var photos: [PhotoItem] = []
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Photos"
navigationController?.navigationBar.prefersLargeTitles = true
setupCollectionView()
getPhoto()
configureDataSource()
}
private func getPhoto() {
NetworkManager.shared.getPhotos(matching: "office", page: 1) { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let photos):
self.photos = photos
self.updateData()
case .failure(let error):
print(error)
}
}
}
private func configureDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, PhotoItem>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, photoItems) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withClass: PhotoCell.self, for: indexPath)
cell.set(photoItems)
return cell
})
}
private func updateData() {
var snapshot = NSDiffableDataSourceSnapshot<Section, PhotoItem>()
snapshot.appendSections([.main])
snapshot.appendItems(photos)
DispatchQueue.main.async { self.dataSource.apply(snapshot, animatingDifferences: true) }
}
private func setupCollectionView() {
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.register(cellWithClass: PhotoCell.self)
collectionView.backgroundColor = .systemBackground
if let layout = collectionView.collectionViewLayout as? PinterestLayout {
layout.delegate = self
}
view.addSubview(collectionView)
collectionView.fillToSuperview()
}
extension PhotosViewController: PinterestDelegate {
func collectionView(_ collectionView: UICollectionView, heightForPhotoAtIndexPath indexPath: IndexPath) -> CGFloat {
return 200
}
}
You have to assign your NSDiffableDataSource object to your CollectionView DataSource:
collectionView.dataSource = dataSource

UITableViewDiffabledatasource NSFetchedResultController: how to update in case of change

I'm setting up a tableView with the new UITableViewDiffabledatasource and an NSFetchedResultController.
Insertion and deletion are correctly being handle out of the box. But when an item is updated (as in one of it's property is updated) and the cell displaying that item should be updated, it does not get updated.
How can I got about making sure the UITableViewDiffabledatasource sees that change and fires a refresh of the cell?
Adding the code I'm using:
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = dataSource
}
func makeDataSource() -> UITableViewDiffableDataSource<String, NSManagedObjectID> {
let reuseIdentifier = String(describing: RegisterTableCell.self)
let dataSource = UITableViewDiffableDataSource<String, NSManagedObjectID>(
tableView: tableView,
cellProvider: { tableView, indexPath, objectID in
let cartItem = try! self.container.viewContext.existingObject(with: objectID) as! CartItem
let cell = tableView.dequeueReusableCell(
withIdentifier: reuseIdentifier,
for: indexPath
) as! RegisterTableCell
cell.count.text = String(format: "%#", cartItem.count ?? "0")
cell.label.text = cartItem.name
cell.price.text = self.priceFormatter.string(from: NSNumber(value: cartItem.totalPrice()))
return cell
}
)
dataSource.defaultRowAnimation = .left
return dataSource
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
updateUI()
let diffableSnapshot = snapshot as NSDiffableDataSourceSnapshot<String,NSManagedObjectID>
dataSource.apply(diffableSnapshot, animatingDifferences: true, completion: nil)
}

Resources