0

I'm learning SwiftUI. I've started by doing Apple SwiftUI tutorials. In one of them they provided @EnviromentalObjects to make changes in their model, they were changing isFavorite bool state. But they did not use @ObservedObject with UserDefaults and when I'm reloading my app, all changes are dissapiring. I've learned how to use UserDefaults for values in view and now I can set on my list of objects if I want to show only isFavorive objects or all of them and when I reload my app, this toggle button is saving my changes.

But I want to do the same with editing my Model.

There is my Model:

import SwiftUI
import CoreLocation

struct City: Hashable, Codable, Identifiable {
var id: Int
var country: String
var name: String
var description: String
fileprivate var imageName: String
fileprivate var coordinates: Coordinates
var state: String
var people: Int
var area: Int
var continent: Continent
var isFavorite: Bool
var isFeatured: Bool


var locationCoordinate: CLLocationCoordinate2D {
    CLLocationCoordinate2D(
        latitude: coordinates.latitude,
        longitude: coordinates.longitude)
}

enum Continent: String, CaseIterable, Codable, Hashable {
    case europe = "Europe"
    case asia = "Asia"
    case america = "America"
}
}

And there is file where I load data from JSON file (copied from apple tutorial)

import UIKit
import SwiftUI
import CoreLocation

var CityData: [City] = load("CityData.json")

func load<T: Decodable>(_ filename: String) -> T {
let data: Data

guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
}

do {
    data = try Data(contentsOf: file)
} catch {
    fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}

do {
    let decoder = JSONDecoder()
    return try decoder.decode(T.self, from: data)
} catch {
    fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}


final class ImageStore {
typealias _ImageDictionary = [String: CGImage]
fileprivate var images: _ImageDictionary = [:]

fileprivate static var scale = 2

static var shared = ImageStore()

func image(name: String) -> Image {
    let index = _guaranteeImage(name: name)

    return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(verbatim: name))
}

static func loadImage(name: String) -> CGImage {
    guard
        let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
        let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
        let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
    else {
        fatalError("Couldn't load image \(name).jpg from main bundle.")
    }
    return image
}

fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
    if let index = images.index(forKey: name) { return index }

    images[name] = ImageStore.loadImage(name: name)
    return images.index(forKey: name)!
}
}

And here you can see how CityData is used:

**%% in UserData.swift file %%**

final class UserData: ObservableObject {
@Published var city = CityData
}

**%And in my view %%**

struct CityList: View {
@EnvironmentObject private var userData: UserData
@ObservedObject var SettingsVM = SettingsViewModel()

var body: some View {
    NavigationView {
        List {
            Toggle(isOn: $SettingsVM.showFavoritesOnly) {
                Text("Show Favorites Only")
            }

            ForEach(userData.city) { City in
                if !self.SettingsVM.showFavoritesOnly || City.isFavorite {
                    NavigationLink(
                        destination: ContentView(city: City)
                            .environmentObject(self.userData)
                    )
                    {
                        CityRow(city: City)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Capitals"))
    }
}
}

%% And how I'm changing "isFavorite" value

   Button(action: {
                self.userData.city[self.cityIndex].isFavorite.toggle();
                print(String(self.city.isFavorite));

            }) {
                if self.userData.city[self.cityIndex].isFavorite {
                    Image(systemName: "star.fill")
                        .foregroundColor(Color.yellow)
                } else {
                    Image(systemName: "star")
                        .foregroundColor(Color.gray)
                }
            }

I was trying to embend CityData in UserDefault.standard.object, but It didn't work. I'm definitly doing something wrong. Can you help me?

My app with City List and isFavorite Toggle

Maciekx7
  • 135
  • 1
  • 8

1 Answers1

0

SwiftUI's @Published is not smart enough for what you want to do.

You are using @Published together with an array. But you don't assign to the array later, you just access an element of it and modify that. @Published never sees the change since no assignment to the published variable took place. So no re-render gets triggered.

Call self.userData.objectWillChange.send() before you change the array element to trigger a re-render.

Fabian
  • 5,040
  • 2
  • 23
  • 35
  • I've used ObjectWillChange in Button just before ` self.userData.city[self.cityIndex].isFavorite.toggle(); ` But it doesn't work. Am I doing it right? – Maciekx7 Dec 01 '19 at 23:30
  • It should at first glance, but if it does not then it doesn't.I can't be sure why without a complete example to execute. – Fabian Dec 02 '19 at 00:12