0

I'm currently developing an application using SwiftUI.

If I run the code below, the display result will be A, But I want to make it the display result B.

What kind of code can this be achieved?

enter image description here


Here is the code:

struct TestView:View {

    let fruits = ["apple","apple","apple","orange","orange","banana"]

    @State var tmpFruits = [""]

    var body: some View{

        List(fruits, id: \.self){fruit in
            if tmpFruits.contains(fruit) == false{

                Button(action: {
                }, label: {
                    Text(fruit)
                })
                .onAppear(){
                    tmpFruits.append(fruit)
                }
            }
        }
    }
}

Swift 5.0

SwiftUI 2.0

Tio
  • 944
  • 3
  • 15
  • 35
  • Where does the fruits array come from? The best solution might be to remove the duplicates before it is assigned to the `fruits` property (if the source is from outside the view) – Joakim Danielson Aug 12 '21 at 15:50
  • @Joakim Danielson, The `fruits array` is created by the data retrieved by the API connection. – Tio Aug 12 '21 at 15:53
  • 4
    Then I would suggest you remove the duplicates after the data has been retrieved and before it is passed to the view. This way it will be done only once, if you do it like in your code or like in some of the answers below it will be done every time the view is redrawn – Joakim Danielson Aug 12 '21 at 15:56
  • @Joakim Danielson, I got it! Thank you for the information. – Tio Aug 12 '21 at 16:57

3 Answers3

2

You need to have a List with unique and constant IDs. Currently, the ID is the string for each element in fruits. The problem with that is since there are repeated items, you are violating the unique ID rule.

A way to solve this is filtering fruits first, hence unique IDs. It also greatly simplifies the view code.

This uses Unique from apple/swift-algorithms for .uniqued(). Remember to import Algorithms.

Code:

import Algorithms

struct TestView: View {
    private let fruits = ["apple", "apple", "apple", "orange", "orange", "banana"]

    var body: some View {
        List(Array(fruits.uniqued()), id: \.self) { fruit in
            Button(fruit) {
                print("action")
            }
        }
    }
}
George
  • 25,988
  • 10
  • 79
  • 133
  • 2
    `Set.insert` returns a tuple that tells you if the item was new upon insertion. You can use this rather than a separate `contains` check. See https://stackoverflow.com/a/55684308/3141234 Even better, check out https://github.com/apple/swift-algorithms/blob/main/Guides/Unique.md – Alexander Aug 12 '21 at 15:48
  • @Alexander Wow, that's much better! – George Aug 12 '21 at 16:00
  • 1
    In theory this is nice. In practice it's too expensive to deduplicate the array every time the view is being rendered. – vadian Aug 12 '21 at 16:10
  • @vadian Yeah - I'm assuming that fruits is actually a `@State` (and constant just for the example). So each time the body is rendered the array is remade, which I guess is fine when that will only really happen when `fruits` changes. If performance is critical, there will be better ways but for a small array I think this is sufficient. Learned a new word though (deduplicate)! – George Aug 12 '21 at 16:13
1

Ideally, you should use the official swift-algorithms package to gain access to a uniqued() function on sequences, which will do exactly what you're looking for.

If you're not using SPM or don't want to pull in that dependancy (though I really do encourage it, it's a first party Apple library, it's open source, and has really great stuff!), you can use a hand-rolled extension, like the community has been doing for years. Something like this would work for your purposes

public extension Sequence where Element: Hashable {

    /// Return the sequence with all duplicates removed.
    ///
    /// i.e. `[ 1, 2, 3, 1, 2 ].uniqued() == [ 1, 2, 3 ]`
    ///
    /// - note: Taken from stackoverflow.com/a/46354989/3141234, as 
    ///         per @Alexander's comment.
    func uniqued() -> [Element] {
        var seen = Set<Element>()
        return self.filter { seen.insert($0).inserted }
    }
}

Once that's in place, this is as simple as:

struct TestView: View {
    private let fruits = ["apple", "apple", "apple", "orange", "orange", "banana"]

    var body: some View {
        List(fruits.uniqued(), id: \.self) { fruit in
            Button(fruit) {
                print("action")
            }
        }
    }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • Btw without `Array(...)` around `fruits.uniqued()` I get a compiler error for it not conforming to `RandomAccessCollection`. – George Aug 12 '21 at 16:04
  • 1
    @George True for the Swift-Algs implementation, but the one I show here returns a concrete `Array` directly. It's less flexible, but useful in cases like this. – Alexander Aug 12 '21 at 16:10
0

Try with this

List {
    ForEach(fruits, id: \.self) { fruit in
        if !tmpFruits.contains(fruit) {
            Button(action: {

            }, label: {
                Text(fruit)
            })
            .onAppear() {
                tmpFruits.append(fruit)
                print(tmpFruits)
            }
        }
    }
}
Alexander
  • 59,041
  • 12
  • 98
  • 151
  • 1
    Hey Gabriele, welcome to the site! I have one issue with this approach: this is coupling view code (iterating `fruits` and displaying it) with "business logic" (de-duplicating entries of the `fruits` array). I think these should be split, with something like `let uniqueFruits = fruits.uniqued()` (using a [`uniqued()` from swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/Unique.md)) – Alexander Aug 12 '21 at 15:47
  • Hi thanks! Yeah I was looking on that and it looks interesting. I would try this for sure. – gabriele cusimano Aug 12 '21 at 15:55