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)")
}
}
}