Search code examples
iosswiftmodel-view-controlleruicollectionviewrelated-content

Using a Data Model to 'relate' to other models (create relationships)


I have a data model that stores specific extreme-sports information. This data is displayed in a tableView, where the cells (extreme sport tricks) can be tapped to view all the details in a Details VC. Part of these details is an optional 'related' section, which can be defined as part of the model.

Example - Table Cell may read "Kickflip" (a skateboarding trick). I can associate "Ollie" and "360-flip" (two more skateboarding tricks) to the Kickflip model's related section.

I want these two related terms (Ollie and 360-flip) clickable so that instead of the user reading 'Kickflip' details, they can read the definition of either Ollie or 360-flip.

My question here is how can I get the 'related' items to refresh the data to the specific term.

Here is my model:

struct ExtremeSportsData {
    static func getAllSportsTerms() -> [ExtremeSportsTermsWithSectionHeaders] {
        return [
            ExtremeSportsTermsWithSectionHeaders(sectionName: "A", sectionObjects: [
                ExtremeSportsTermsModel(term: "Acid Drop", definition: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", relatedTerms: nil),
                ExtremeSportsTermsModel(term: "Alpha Flip", definition: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", relatedTerms: nil),
                ExtremeSportsTermsModel(term: "Axle Stall", definition: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", relatedTerms: ["Ollie"])
                ]),
            ExtremeSportsTermsWithSectionHeaders(sectionName: "O", sectionObjects: [
                ExtremeSportsTermsModel(term: "Ollie", definition: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", relatedTerms: ["Shuvit"]),
                ExtremeSportsTermsModel(term: "Overturn", definition: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", relatedTerms: ["Acid Drop", "Alpha Flip"])
                ]),
            ExtremeSportsTermsWithSectionHeaders(sectionName: "S", sectionObjects: [
                ExtremeSportsTermsModel(term: "Shuvit", definition: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", relatedTerms: ["Ollie", "Axle Stall", "Overturn"])
                ])
        ]
    }
}

Once a cell is tapped, the information on the DetailVC gets displayed in a Collection View. I want the related terms (if there are any!) to update the data to that specific term. As you can see in my data above - if the user taps on Ollie, they will have Shuvit as a related term. That can be clicked to read the Shuvit definition & information.

My thought process for the UI was to use Contains(String), however this solution does not work IF there is a typo in the data.. (and my code below is not perfect)

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let specificRelatedTerm = receivedSportTerm!.relatedTerms![indexPath.item]
    if receivedSportTerm!.term.contains(specificRelatedTerm) {
        print("Yes - Related")
    }
    else {
        print("No - Not related")
    }
}

How can I set up "relationships" between data, so that I can tap buttons in a UI to update information based on the relationships?

I will be happy to clarify any of the above... Something is telling me I did not word this as good as I could have, but any help is appreciated thank you.

UI


Solution

  • I think some sort of class like Repository or Holder will be good for this option. Basic idea - you have Repository that contains all ExtremeSportsTermsModels and related items for each ExtremeSportsTermsModel and funcs that can manupilate with data.

    final class RelatedFlipsRepository {
    
        enum Action {
            case add
            case remove
        }
    
        private var relations: [String: Set<String>] = [:]
        var flips: [ExtremeSportsTermsModel] = []
    
        init(flips: [ExtremeSportsTermsModel]) {
            self.flips = flips
        }
    
        // MARK: - Public
    
        func addFlip(_ flip: ExtremeSportsTermsModel, relatedTo related: [ExtremeSportsTermsModel] = []) {
            if !flips.contains(where: { $0.term == flip.term }) {
                flips.append(flip)
            }
    
            configureRelated(related, flip: flip, action: .add)
        }
    
        func getRelated(to flip: ExtremeSportsTermsModel) -> [ExtremeSportsTermsModel] {
            guard let values = relations[flip.term], !values.isEmpty else { return [] }
            return flips.filter({ values.contains($0.term) })
        }
    
        func configureRelated(_ related: [ExtremeSportsTermsModel], flip: ExtremeSportsTermsModel, action: Action) {
            switch action {
            case .add:
                addRelated(related, to: flip)
                related.forEach({ addRelated([flip], to: $0) })
    
            case .remove:
                removeRelated(related, from: flip)
                related.forEach({ removeRelated([flip], from: $0) })
            }
    
        }
    
        // MARK: - Private
    
        private func addRelated(_ related: [ExtremeSportsTermsModel], to flip: ExtremeSportsTermsModel) {
            if !related.isEmpty {
                let relatedValues = related.map({ $0.term })
                if let values = relations[flip.term] {
                    relations[flip.term] = values.union(relatedValues)
                } else {
                    relations[flip.term] = Set(relatedValues)
                }
            }
        }
    
        private func removeRelated(_ related: [ExtremeSportsTermsModel], from flip: ExtremeSportsTermsModel) {
            guard let values = relations[flip.term], !related.isEmpty else { return }
            relations[flip.term] = values.subtracting(related.map({ $0.term }))
        }
    
    }
    

    Usage:

    let ollie = ExtremeSportsTermsModel(term: "Ollie")
    let shuvit = ExtremeSportsTermsModel(term: "Shuvit")
    let acidDrop = ExtremeSportsTermsModel(term: "Acid Drop")
    
    let repository = RelatedFlipsRepository(flips: [ollie, shuvit, acidDrop])
    
    repository.configureRelated([acidDrop, shuvit], flip: ollie, action: .add)
    print(repository.getRelated(to: ollie)) // [Shuvit, Acid Drop]
    print(repository.getRelated(to: acidDrop)) // [Ollie]
    
    repository.configureRelated([acidDrop], flip: ollie, action: .remove)
    print(repository.getRelated(to: ollie)) // [Shuvit]
    print(repository.getRelated(to: acidDrop)) // []  
    

    As you can see Repository have two properties relations and flips (nobody said that you have to keep all data in one property, right?). relations has type [String: Set<String>] that keeps ExtremeSportsTermsModel's term values as the "primary keys" and it's not really good. In most cases there is an let id: Int value or something close to it, maybe you should think about it too.

    This workflow can help you to take care about related data in both directions, but if you want more specific solution you can use Graph data structure, but it's not quite easy to implement and support.

    P.S. Copy/paste file.


    didSelectItemAt problem.

    I think you strategy should be like 1) grab tapped relatedItem, 2) set the relatedItem to item value by which ViewController can reload yourself, 3) and call appropriate func to reload UI.

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        let relatedItem = repository.getRelated(to: receivedSportItem!)[indexPath.row]
        // set `relatedItem` to you `item`
        // call `reloadUI` func
    }