0

From the tutorial https://developer.apple.com/tutorials/swiftui/handling-user-input, it has the star to indicate "isFavourite".

I have changed the star to a button, if the user tapped on it, and then it will toggle the isFavourite value.

struct LandmarkRow: View {

    @State var landmark: Landmark
    @State var isChecked: Bool = false

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)

            Spacer()

            Button(action: {
                self.isChecked.toggle()
                landmark.isFavourite.toggle() // tried to modify the landmark value
            }, label: {
                if self.isChecked {
                    Image(systemName: "checkmark.square.fill")
                        .imageScale(.large)
                } else {
                    Image(systemName: "square")
                        .imageScale(.large)
                }
            })
        }
    }
}

However, I found an issue here.

Back to the LandmarkList view, I would like to count how many are landmarks are selected. However, when I use a for loop to loop through the landmarks, I found that the isFavourite value is not modified.

struct LandmarkList: View {
    var body: some View {
        NavigationView {
            VStack {

            Text("\(getCount(landmarks)") // fail here, it shows 0 forever
            List(landmarks) { landmark in
                NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
            }
            .navigationTitle("Landmarks")
        }
    }
}

I have a function getCount in the LandmarkList:

func getCount(landmarks: [Landmark]) -> Int {
    var count: Int = 0
    for landmark in landmarks {
        if landmark.isFavourite {
           count += 1
        }
    }
    return count
}

Is that because of the @State? Or what have been wrong here?

pawello2222
  • 46,897
  • 22
  • 145
  • 209
user6539552
  • 1,331
  • 2
  • 19
  • 39
  • 1
    You need to pass a Binding to your LandmarkRow as you are only updating the favorite value in the row, and never in the actual data that supplies the row – Andrew Jan 20 '21 at 16:23
  • A good tip is to always declare `@State` properties as `private`. You shouldn't access `landmark` as a `@State` from outside the `LandmarkRow` view. – pawello2222 Jan 20 '21 at 16:24
  • Does this answer your question https://stackoverflow.com/a/59316596/12299030? – Asperi Jan 20 '21 at 16:46

1 Answers1

0

As I said in my comment above you aren't actually updating the global state of the landmarks, only the state that exists in the row, You would need to using a binding or access the value in the ModelData that is held in the environment.

If you look at the LandmarkDetail page there is a solution there.

You can add the FavoriteButton, the environmentObject for the ModelData, and a computed property to calculate the index of the Landmark you are using to the LandmarkRow. This basically the same solution as used in the LandmarkDetail.

This gives the following:

struct LandmarkRow: View {
    // Access the modelData from the environment
    @EnvironmentObject var modelData: ModelData
    var landmark: Landmark

    // We need to get the index so that we can update the landmark in the ModelData
    var landmarkIndex: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }

    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)

            Spacer()
            // Use the favorite button that already exists in the project
            FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                .buttonStyle(PlainButtonStyle()) 
            // As LandmarkRow is used in a NavigationLink we need to set the 
            // buttonStyle so that we can use it otherwise we will just navigate.
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var landmarks = ModelData().landmarks

    static var previews: some View {
        Group {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
        }
        .previewLayout(.fixed(width: 300, height: 70))
        .environmentObject(ModelData()) // Add the environmentObject so the previews will work
    }
}

Then if you want to see how many favourites you have you can do the following in your LandmarkList

var body: some View {
    NavigationView {
        List {
            Text("Favorites: \(modelData.landmarks.filter({$0.isFavorite}).count)")
            Toggle(isOn: $showFavoritesOnly) {
                Text("Favorites only")
            }

            ForEach(filteredLandmarks) { landmark in
                NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                    LandmarkRow(landmark: landmark)
                }
            }
        }
        .navigationTitle("Landmarks")
    }
}

These changes give the following result:

enter image description here

Tested in Xcode 12.3 with iOS 14.3

Andrew
  • 26,706
  • 9
  • 85
  • 101