0

My model Keyword has the following implementation:

public struct Keyword: Codable, Equatable, Identifiable {
    
    public var id: String {
        name
    }
    public var name: String
    public var popularity: Int?
    
    public static func == (lhs: Keyword, rhs: Keyword) -> Bool {
        return lhs.name == rhs.name
    }

    [...]

KeywordsView has the following implementation:

@ObservedObject var viewModel = KeywordsViewModel()

var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.keywords) { keyword in
                    KeywordView(keyword: keyword)
                }

                [...]

When popularity is updated (e.g. using a web API), the view never gets updated. It works fine when I remove the static == method. It also works fine if I make this method always return false.

I have an idea why this is happening: SwiftUI probably (implicitly) uses == to figure out whether the view needs to be updated. And because popularity is left out of the equation, SwiftUI never updates the view although popularity is changed.

However, I would like to use the custom == implementation to make use of, amongst others, contains(_:) and compare instances of Keyword in my way.

How can I make this work?

https://developer.apple.com/documentation/swift/equatable

Niels Mouthaan
  • 1,010
  • 8
  • 19
  • Try conforming to identifiable. It's just a guess, you barely posted some code that would allow us to understand the issue (where's the view's code?). – HunterLion Dec 03 '22 at 10:17
  • I believe SwiftUI needs Equatable for knowing when to update a view and since you have defined `==` to only use `keyword` that is what defines equality. Overriding == should be done with some care since it's an essential part of the definition of a custom type IMO. – Joakim Danielson Dec 03 '22 at 10:24
  • @HunterLion I updated the question with additional code. – Niels Mouthaan Dec 03 '22 at 11:00
  • @JoakimDanielson I also share the feeling SwiftUI doesn't work well with custom == implementations. However, how to use Equatable (and its benefits) in that case? An alternative would be to introduce a custom isSameAs function, but that feels a bit ugly. – Niels Mouthaan Dec 03 '22 at 11:02
  • It sounds like your implementation of `==` is semantically different from what you want it to be. You want the UI to update when keywords change with respect to popularity, but your definition of == doesn’t consider keywords to be different if they have same name but different popularity. Make up your mind about what it means for keywords to be equal. – Caleb Dec 03 '22 at 17:52
  • @Caleb, the uniqueness of a `Keyword` instance is based on `name`. Instances of `Keyword` are created locally, and their `popularity` is initially `nil`. Values of `popularity` are retrieved using a web API. Keywords with the same name are considered the same, even when they have different popularities. However, I still need SwiftUI to show updated popularities. In other words: equitability depends on the context. – Niels Mouthaan Dec 04 '22 at 13:52
  • @NielsMouthaan So it seems like you've answered your own question with "depends on context." IOW, in some contexts, two keywords with the same name aren't equal, even though they might be from a user's point of view. In the cases where they should be equal based only on name, perhaps it'd make sense to compare the names instead of the keywords, i.e. use `a.name == b.name` instead of `a == b`? – Caleb Dec 04 '22 at 18:22
  • 1
    @Caleb, I think that makes sense indeed. I've now solved it by introducing a custom method that checks for equatability by comparing names indeed. Thanks. – Niels Mouthaan Dec 05 '22 at 16:13

1 Answers1

1

One option to give you the benefits Equatable, without messing with SwiftUI's use of it would be to add some extensions that match your needs but use the identifier's equality instead. For example:

extension Sequence where Element: Identifiable {
    func contains(id: Element.ID) -> Bool {
        contains(where: { $0.id == id })
    }
}

extension Collection where Element: Identifiable {
    func firstIndex(of id: Element.ID) -> Index? {
        firstIndex(where: { $0.id == id })
    }
}

// keywords.contains(id: "some keyword")
// keywords.firstIndex(of: "some keyword")
dillon-mce
  • 69
  • 5
  • Thanks. However, this does not allow me to use == to compare keywords. – Niels Mouthaan Dec 04 '22 at 13:53
  • That is because your two structs aren't "equal" in the strict sense of equality (Just list "Word" and "word" are not "equal". But they have the same identity. So you can check `keyword1.id == keyword2.id`. It's an extra six characters. Or you could come up with a custom "identity checking" operator `~==` or `<=>` or something. – dillon-mce Dec 04 '22 at 14:42