Reputation: 809
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
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