4

I've currently hit a wall while trying to develop the search view within my application. I've attempted to place a collection view within a table view cell, as well as use the traditional collection view data source delegates (cell for row at etc.) however either of these implementations seems to be far from optimal in a modern application. Our application is using three models for 'Friends', 'Users', and 'Venues'.

We've successfully got one of our models to populate in the search view using a Diffable Data Source and compositional layout, but when I try to add the others I get the following error:

""*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid parameter not satisfying: section != NSNotFound' terminating with uncaught exception of type NSException""

Heres the SearchViewController

import UIKit

fileprivate typealias DataSource  = UICollectionViewDiffableDataSource<Section, SearchV2VC.DataType>
fileprivate typealias SourceSnapshot = NSDiffableDataSourceSnapshot<Section, SearchV2VC.DataType>

class SearchV2VC: UIViewController {
    
    
    var collectionView: UICollectionView! = nil
    private var dataSource: DataSource!
    var friendData = [FollowingInfo]()
    var userData = [FriendInfo]()
    var venueData = [Venue]()
   
    override func viewDidLoad() {
        super.viewDidLoad()
        
        configureHierarchy()
        configureDataSource()
        
        
    }
   
    private func firstLayoutSection() -> NSCollectionLayoutSection {

        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))

        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets.bottom = 15

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(0.5))

        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
        group.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 2)

        let section = NSCollectionLayoutSection(group: group)

        //section.orthogonalScrollingBehavior = .groupPaging
        section.orthogonalScrollingBehavior = .none



        return section
    }
    private func secondLayoutSection() -> NSCollectionLayoutSection {

        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.5))

        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets.bottom = 15

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.9), heightDimension: .fractionalWidth(0.5))

        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
        group.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 2)

        let section = NSCollectionLayoutSection(group: group)

        //section.orthogonalScrollingBehavior = .groupPaging
        section.orthogonalScrollingBehavior = .none



        return section
    }

    private func thirdLayoutSection() -> NSCollectionLayoutSection {

        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(2))

        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        item.contentInsets.bottom = 15

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.35), heightDimension: .fractionalWidth(0.35))

        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
        group.contentInsets = .init(top: 0, leading: 15, bottom: 0, trailing: 0)

        let section = NSCollectionLayoutSection(group: group)

        navigationItem.title = "You a Bitch"

        section.orthogonalScrollingBehavior = .continuous

        return section
    }
}
extension SearchV2VC {
private func createLayout() -> UICollectionViewCompositionalLayout {

    return UICollectionViewCompositionalLayout { (sectionNumber, env) -> NSCollectionLayoutSection? in

     switch sectionNumber {

     case 0: return self.firstLayoutSection()
     case 1: return self.secondLayoutSection()
     default: return self.thirdLayoutSection()

     }
   }
}
}
    //MARK: - UICollectionViewDataSource Methods


extension SearchV2VC {
    private func configureHierarchy() {
        //collectionView.delegate = self
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        collectionView.delegate = self
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .white
        
        collectionView.register(UserCell.self, forCellWithReuseIdentifier: UserCell.reuseIdentifier)
        collectionView.register(FriendCell.self, forCellWithReuseIdentifier: FriendCell.reuseIdentifier)
        collectionView.register(VenueCell.self, forCellWithReuseIdentifier: VenueCell.reuseIdentifier)
        view.addSubview(collectionView)
        
        
     
    }
}
extension SearchV2VC {
    

    func configureDataSource(){
        
        dataSource = DataSource(collectionView: collectionView, cellProvider: { (collectionView, index, friend) -> UICollectionViewCell in
            
            switch friend{
            case .user(let user):
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: UserCell.reuseIdentifier, for: index) as? UserCell else {fatalError("Couldn't Create New Cell")}
                cell.viewModel = UserCellViewModel(user: user)
                return cell
            
            case .friend(let friend):
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FriendCell.reuseIdentifier, for: index) as? FriendCell else {fatalError("Couldn't Create New Cell")}
                cell.viewModel = FriendCellViewModel(user: friend)
                return cell
            case .venue(let venue):
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: VenueCell.reuseIdentifier, for: index) as? VenueCell else {fatalError("Couldn't Create New Cell")}
                cell.viewModel = VenueCellViewModel(venue: venue)
                return cell
            }
            
            
        })
       
        dataSource.apply(snapshotForCurrentState(), animatingDifferences: false)
        
    }
            
        



func snapshotForCurrentState() -> NSDiffableDataSourceSnapshot<Section, DataType>{
    var snapshot = NSDiffableDataSourceSnapshot<Section, DataType>()
    snapshot.appendSections(Section.allSections)

    let userItems = userData.map { DataType.user($0) }
    snapshot.appendItems(userItems, toSection: Section.users)

    let friendItems = friendData.map { DataType.friend($0) }
    snapshot.appendItems(friendItems, toSection: Section.friends)

    let venueItems = venueData.map { DataType.venue($0) }
    snapshot.appendItems(venueItems, toSection: Section.venue)
    return snapshot
    }
}
extension SearchV2VC: UICollectionViewDelegate  {
    

    enum DataType: Hashable {
        case user(FriendInfo)
        
        case friend(FollowingInfo)
        
        case venue(Venue)
    }
    
     func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
         collectionView.deselectItem(at: indexPath, animated: true)
         guard let user = dataSource.itemIdentifier(for: indexPath) else { return }
        print(user)
     }
    
}

This is what our models look like:

import Foundation
import Firebase
import Mapbox

struct Venue: Hashable {
    
    static func == (lhs: Venue, rhs: Venue) -> Bool {
        lhs.venID == rhs.venID
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(venID)
    }
    
    var venID: String?
    var latitude: Double?
    var longitude: Double?
    var name: String?
    var description: String?
    var image: String?
    var address: String?

}

struct FriendInfo: Hashable {
    
    static func == (lhs: FriendInfo, rhs: FriendInfo) -> Bool {
        lhs.userID == rhs.userID
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(userID)
    }
    
    var displayName: String?
    var userID: String?
    var userName: String?
    var providerID: String?
    var profileImageURL: String?
    var isFriend = false
    var stats: UserStats?
    var isCurrentUser: Bool { return Auth.auth().currentUser?.uid == providerID }
}

struct FollowingInfo: Hashable {
    
    static func == (lhs: FollowingInfo, rhs: FollowingInfo) -> Bool {
        lhs.userID == rhs.userID
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(userID)
    }
    
    var displayName: String?
    var userID: String?
    var profileURL: String?
    var userName: String?
}

Lastly here is the Custom Section Class we are using instead of the usual 'enum section' located in the controller, and this is most likely where the problem lies:

import UIKit

struct Section: Hashable {
    var id = UUID()
    
    var title: String
    var data: [Any]
    
    init(title: String, data: [Any]) {
        self.title = title
        self.data = data
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    static func == (lhs: Section, rhs: Section) -> Bool {
        lhs.id == rhs.id
    }
    
}

extension Section {
    static var allSections: [Section] = [
        Section(title: "Users", data: [FriendInfo]()),
        Section(title: "Friends", data: [FollowingInfo]()),
        Section(title: "Venue", data: [Venue]())
    ]
}

extension Section {
    static var users: Section = 
    Section(title: "Users", data: [FriendInfo]())
}

extension Section {
    static var friends: Section =
    Section(title: "Friends", data: [FollowingInfo]())
}

extension Section {
    static var venue: Section =
    Section(title: "Venue", data: [Venue]())
}

Lastly, I should mention we are setting these 3 models with completely different Data via an API call to Firebase. (The data is operating as expected everywhere else in the application) The collection view cells are utilizing a view model hence the 'cell.viewModel = ...' seen in the view controller.

I've been trying to get this functioning as expected for several days now. I have watched every video from Apple, thoroughly inspected all the example projects, and visited nearly every link google has to offer on the topic.

This is my first stack question so I apologize if I missed any crucial information. Surely someone has implemented something like this design, I am open to any ways of achieving this. Thanks!

3 Answers3

7

I actually found your post while trying to figure this out for myself, and just got it to work. You're definitely on the right track by using enums. I'm sorry I can't tell you exactly what that error means, but here is the gist of what worked for me. I have only two different models (Event and Filter), but obviously it could be expanded to work with 3+.

typealias EventsDataSource = UICollectionViewDiffableDataSource<Section, Item>
typealias EventsSnapshot = NSDiffableDataSourceSnapshot<Section, Item>

struct Filter {
    let text: String
}

struct Event {
    let description: String
    let startTime: Date
    let endTime: Date
}

enum Item: Hashable {
    case filter(Filter)
    case event(Event)
}

struct Section: Hashable {
    let items: [Item]
    let title: String?
}

Donny Wals has a particularly good Diffable Datasources article.

(And if you're looking to integrate Core Data into your Diffable Datasource, Julian Schiavo's article is immensely helpful too.)

Kevin Kelly
  • 164
  • 1
  • 12
  • My view controller is only responsible for the layout specific to our UI. In my "controller" class I handle the diffable data stuff. One thing I noticed looking at your code again is you create the DataType enum to define your different datasource models, but then you use a generic Any type in your Section struct. Use your enum! So your Section struct would look just like mine, replacing Item with DataType. I'm also curious how you're populating `data` in Section? I see you initialize `allSections` with a couple sections with title, but with empty arrays for `data`. – Kevin Kelly Jul 14 '21 at 16:03
1

You get this error Invalid parameter not satisfying: section != NSNotFound when you add ItemIdentifier instances to a SectionIdentifier instance which is not yet added to the snapshot. That's the reason for the message - you have not added the section to the data source and UIKit does not know what indexPath.section is for the added items.

Thus don't forget to do snapshot.appendSections([...]) before doing snapshot.appendItems([...], toSection:...)

Aleksandar Vacić
  • 4,433
  • 35
  • 35
0

For me the best way to use multiple models is to cast them to a hashable.

Models:

struct ModelA: Hashable {
    let title: String
}

struct ModelB: Hashable {
    let image: UIImage
}

enum Section {
    case a
    case b
}

UICollectionViewDiffableDataSource:

let dataSource = UICollectionViewDiffableDataSource<Section, AnyHashable>(collectionView: collectionView) { collectionView, indexPath, item in
    switch item {
    case let model as ModelA:
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellA", for: indexPath) as! CellA
        cell.configure(with: model)
        return cell
    case let model as ModelB:
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellB", for: indexPath) as! CellB
        cell.configure(with: model)
        return cell
    default:
        fatalError("Unknown item type")
    }
}

Fill UICollectionViewDiffableDataSource with data:

var snapshot = NSDiffableDataSourceSnapshot<Section, AnyHashable>()
snapshot.appendSections([.a, .b])
snapshot.appendItems([ModelA(title: "Item 1"), ModelA(title: "Item 2")], toSection: .a)
snapshot.appendItems([ModelB(image: UIImage(named: "image1")), ModelB(image: UIImage(named: "image2"))], toSection: .b)
dataSource.apply(snapshot, animatingDifferences: false)

UICollectionViewDelegate methods:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
    switch item {
    case let model as ModelA:
        print("Selected item A with title: \(model.title)")
    case let model as ModelB:
        print("Selected item B with image: \(model.image)")
    default:
        break
    }
}
Dewerro
  • 371
  • 3
  • 12