fankibiber
fankibiber

Reputation: 809

How to modify Boolean value using @StateObject?

I was trying to use @StateObject to recreate the functionality of Landmarks app, which Apple released as tutorial last year. I have the following code. It's a basic app that you can see JSON objects' isFavorite value true or false. In Apple's example it's possible to change the isFavorite value by tapping on a button, and then persist the change.

Still, I do not fully understand Apple's example here. How it is possible to persist the value without rewriting the JSON itself? Isn't it a bad example?

import SwiftUI
import Combine
import Foundation

struct Item: Codable, Identifiable, Equatable {
    var id: Int
    var name: String
    var isFavorite: Bool
}

final class UserData: ObservableObject {
    @Published var items = Bundle.main.decode([Item].self, from: "data.json")
    @Published var showFavorites = false
}

struct ContentView: View {
    @State var itemID = Item.ID()
    @StateObject var userData = UserData()

    var body: some View {
        NavigationView {
            VStack {
                Toggle(isOn: $userData.showFavorites) {
                    Text("Show Favorites Only")
                }
                List {
                    ForEach(userData.items) { item in
                        if !userData.showFavorites || item.isFavorite {
                            NavigationLink(destination: ContentDetail(itemID: item.id - 1)) {
                                ContentRow(item: item)
                            }
                        }
                    }
                }
            }
        }
    }
}

struct ContentRow: View {
    var item: Item

    var body: some View {
        HStack {
            Text(item.name)
            Spacer()
            if item.isFavorite {
                Image(systemName: "star.fill")
                    .imageScale(.medium)
                    .foregroundColor(.yellow)
            }
        }
    }
}

struct ContentDetail: View {
    @State var itemID = Item.ID()
    @StateObject var userData = UserData()

    var body: some View {
        VStack {
            Button {
                userData.items[itemID].isFavorite.toggle()
            } label: {
                if userData.items[itemID].isFavorite {
                    Image(systemName: "star.fill")
                        .foregroundColor(Color.yellow)
                } else {
                    Image(systemName: "star")
                        .foregroundColor(Color.gray)
                }
            }
            Text(userData.items[itemID].name)
        }
    }
}

extension Bundle {
    func decode<T: Decodable>(_ type: T.Type, from file: String, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T {
        guard let url = self.url(forResource: file, withExtension: nil) else {
            fatalError("Failed to locate \(file) in bundle.")
        }

        guard let data = try? Data(contentsOf: url) else {
            fatalError("Failed to load \(file) from bundle.")
        }

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = dateDecodingStrategy
        decoder.keyDecodingStrategy = keyDecodingStrategy

        do {
            return try decoder.decode(T.self, from: data)
        } catch DecodingError.keyNotFound(let key, let context) {
            fatalError("Failed to decode \(file) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)")
        } catch DecodingError.typeMismatch(_, let context) {
            fatalError("Failed to decode \(file) from bundle due to type mismatch – \(context.debugDescription)")
        } catch DecodingError.valueNotFound(let type, let context) {
            fatalError("Failed to decode \(file) from bundle due to missing \(type) value – \(context.debugDescription)")
        } catch DecodingError.dataCorrupted(_) {
            fatalError("Failed to decode \(file) from bundle because it appears to be invalid JSON")
        } catch {
            fatalError("Failed to decode \(file) from bundle: \(error.localizedDescription)")
        }
    }
}

Upvotes: 1

Views: 507

Answers (1)

pawello2222
pawello2222

Reputation: 54436

It looks like you're creating two instances of UserData.

struct ContentView: View {
    ...
    @StateObject var userData = UserData()
    ...
}

struct ContentDetail: View {
    ...
    @StateObject var userData = UserData()
    ...
}

Using a @StateObject doesn't mean that its instance it will be the same for all your views. A @StateObject will persist when SwiftUI decides to invalidate/redraw your View, but it doesn't mean the same object will be available globally.

In your example in the ContentView you're accessing (and making changes) to one instance of UserData and in the ContentDetail you're using another instance. This also means you're needlessly decoding your JSON twice.

If you want the UserData to be available globally in your current environment you may try an @EnvironmentObject.

Alternatively you can pass UserData to your DetailView:

NavigationLink(destination: ContentDetail(itemID: item.id - 1, userData: userData))
...

struct ContentDetail: View {
    ...
    @ObservedObject var userData: UserData // <- declare only
    ...
}

Note that you don't need a @StateObject in your detail view. You can safely use an @ObservedObject as it's passed to your detail view (and not created in it). See this question for more details.

Upvotes: 1

Related Questions