Been away from the swift-ing for a good 3 years now.
Getting back into it now and trying to learn Combine and SwiftUI.
Making a test Workout app.
Add an exercise, record reps and weights for 3 sets.
Save data.
I'm having issues moving some data around from views to data store.
I think I'm confusing all the different property wrappers.
Summary at the bottom after code.
App:
#main
struct TestApp: App {
#StateObject private var store = ExerciseStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}
}
Views:
struct ContentView: View {
#EnvironmentObject var store: ExerciseStore
var body: some View {
List {
ForEach($store.completedExercises) { $exercise in
ExerciseView(exercise: $exercise)
}
}
}
}
struct ExerciseView: View {
#Binding var exercise: CompletedExercise
var body: some View {
VStack {
Text(exercise.exercise.name)
SetView(set: $exercise.sets[0])
SetView(set: $exercise.sets[1])
SetView(set: $exercise.sets[2])
}
}
}
struct SetView: View {
#Binding var set: ExerciseSet
var body: some View {
HStack {
TextField(
"Reps",
value: $set.reps,
formatter: NumberFormatter()
)
TextField(
"Weight",
value: $set.weight,
formatter: NumberFormatter()
)
}
}
}
Store:
class ExerciseStore: ObservableObject {
#Published var completedExercises: [CompletedExercise] = [CompletedExercise(Exercise())]
init() {
if let data = UserDefaults.standard.data(forKey: "CompletedExercise") {
if let decoded = try? JSONDecoder().decode([CompletedExercise].self, from: data) {
completedExercises = decoded
return
}
}
}
func save() {
if let encoded = try? JSONEncoder().encode(completedExercises) {
UserDefaults.standard.set(encoded, forKey: "CompletedExercise")
}
}
}
Models:
class CompletedExercise: Codable, Identifiable, ObservableObject {
var id = UUID().uuidString
var exercise: Exercise
#Published var sets = [
ExerciseSet(),
ExerciseSet(),
ExerciseSet()
]
init(exercise: Exercise) {
self.exercise = exercise
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
exercise = try container.decode(Exercise.self, forKey: .exercise)
sets = try container.decode([ExerciseSet].self, forKey: .sets)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(exercise, forKey: .exercise)
try container.encode(sets, forKey: .sets)
}
}
private enum CodingKeys: CodingKey {
case id, exercise, sets
}
struct Exercise: Codable, Identifiable {
var id = -1
var name = "Bench Press"
}
class ExerciseSet: Codable, ObservableObject {
#Published var reps: Int?
#Published var weight: Int?
init() {}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
reps = try container.decodeIfPresent(Int.self, forKey: .reps)
weight = try container.decodeIfPresent(Int.self, forKey: .weight)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(reps, forKey: .reps)
try container.encode(weight, forKey: .weight)
}
}
private enum CodingKeys: CodingKey {
case reps, weight
}
Thats more or less the current code.
I've added a bunch of print statements in the save function in ExerciseStore to see what gets saved.
No matter what I've tried, I can't get the reps/weight via the SetView text fields to persist through the ExerciseStore and get saved.
I've played around with #Binding and such as well but can't get it working.
What am I missing/messing up with the new SwiftUI data flows.
You're code is great, but the values in your ExerciseSet & CompletedExercise aren't marked with #Published.
In order to access the ObservableObject capabilities you need to publish those values enabling your Views to listen, and your class to bind to the changes made.
Also substitute ObservedObject with StateObject, same case for EnvironmentObject.
There are a couple of things I note about your code. First you declare the store as an #EnvironmentObject but you don't show the code where you set up the EnvironmentKey and EnvironmentValues to use it. It may be that you chose to leave that code out, but if you don't have it, it will be worth looking at. See the docs for EnvironmentKey, I think it explains both. https://developer.apple.com/documentation/swiftui/environmentkey.
#EnvironmentObject just declares that you want to take a property from the environment and observe it. You have to put the property in the environment (usually from a top-level view) using the environmentObject View modifier (https://developer.apple.com/documentation/swiftui/image/environmentobject(_:)/). You don't show that code either. You may have put it on your app (or whichever view instantiates ContentView).
Secondly you have #ObservableObjects but no #Published properties on those objects. #Published is how Combine knows which properties you want notifications about.
Thirdly you use #ObservedObject in a lot of your views. #ObservedObject declares that "someone else is going to own an Observable object, and they are going to give it to this view at runtime and this view is going to watch it". At some point, however, someone needs to own the object. It will be the "source of truth" that gets shared using #ObservedObject. The view that wants to own the object should declare that ownership using #StateObject. It can then pass the observed object to children and the children will use #ObservedObject to watch the object owned by the parent. If you have something that's not a observable object you want a view to own, you declare ownership using the #State modifier. Then if you want to share that state to children and let them modify it, you will use a Binding.
It's best to just have one class which is the store and make all the other model types as structs. That way things will update properly. You can flatten the model by using the IDs to cross reference, e.g.
class ExerciseStore: ObservableObject {
#Published var exercises = [Excercise]
#Published var sets
#Published var completedIDs
You can see an example of this in the Fruta sample project where there is a favourite smoothies array that is IDs and not duplicates of the smoothie structs.
Related
try to save user setting, but UserDefaults is not working, Xcode 12.3, swiftui 2.0, when I am reload my app, my setting not updating for new value)
class PrayerTimeViewModel: ObservableObject {
#Published var lm = LocationManager()
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults.standard.set(method.params, forKey: "method")
self.getPrayerTime()
}
}
func getPrayerTime() {
let cal = Calendar(identifier: Calendar.Identifier.gregorian)
let date = cal.dateComponents([.year, .month, .day], from: Date())
let coordinates = Coordinates(latitude: lm.location?.latitude ?? 0.0, longitude: lm.location?.longitude ?? 0.0)
var par = method.params
par.madhab = mashab
self.times = PrayerTimes(coordinates: coordinates, date: date, calculationParameters: par)
}
and view.. update with AppStorage
struct MethodView: View {
#ObservedObject var model: PrayerTimeViewModel
#Environment(\.presentationMode) var presentationMode
#AppStorage("method", store: UserDefaults(suiteName: "method")) var method: CalculationMethod = .dubai
var body: some View {
List(CalculationMethod.allCases, id: \.self) { item in
Button(action: {
self.model.objectWillChange.send()
self.presentationMode.wrappedValue.dismiss()
self.model.method = item
method = item
}) {
HStack {
Text("\(item.rawValue)")
if model.method == item {
Image(systemName: "checkmark")
.foregroundColor(.black)
}
}
}
}
}
}
You have two issues.
First, as I mentioned in my comment above that you are using two different suites for UserDefaults. This means that you are storing and retrieving from two different locations. Either use UserDefaults.standard or use the one with your chosen suite UserDefaults(suitName: "method") - you don't have to use a suite unless you plan on sharing your defaults with other extensions then it would be prudent to do so.
Secondly you are storing the wrong item in UserDefaults. You are storing a computed property params rather than the actual enum value. When you try to retrieve the value it fails as it is not getting what it expects and uses the default value that you have set.
Here is a simple example that shows what you could do. There is a simple enum that has a raw value (String) and conforms to Codable, it also has a computed property. This matches your enum.
I have added an initialiser to my ObservableObject. This serves the purpose to populate my published Place from UserDefaults when the Race object is constructed.
Then in my ContentView I update the place depending on a button press. This updates the UI and it updates the value in UserDefaults.
This should be enough for you to understand how it works.
enum Place: String, Codable {
case first
case second
case third
case notPlaced
var someComputedProperty: String {
"Value stored: \(self.rawValue)"
}
}
class Race: ObservableObject {
#Published var place: Place = .notPlaced {
didSet {
// Store the rawValue of the enum into UserDefaults
// We can store the actual enum but that requires more code
UserDefaults.standard.setValue(place.rawValue, forKey: "method")
// Using a custom suite
// UserDefaults(suiteName: "method").setValue(place.rawValue, forKey: "method")
}
}
init() {
// Load the value from UserDefaults if it exists
if let rawValue = UserDefaults.standard.string(forKey: "method") {
// We need to nil-coalesce here as this is a failable initializer
self.place = Place(rawValue: rawValue) ?? .notPlaced
}
// Using a custom suite
// if let rawValue = UserDefaults(suiteName: "method")?.string(forKey: "method") {
// self.place = Place(rawValue: rawValue) ?? .notPlaced
// }
}
}
struct ContentView: View {
#StateObject var race: Race = Race()
var body: some View {
VStack(spacing: 20) {
Text(race.place.someComputedProperty)
.padding(.bottom, 20)
Button("Set First") {
race.place = .first
}
Button("Set Second") {
race.place = .second
}
Button("Set Third") {
race.place = .third
}
}
}
}
Addendum:
Because the enum conforms to Codable it would be possible to use AppStorage to read and write the property. However, that won't update the value in your ObservableObject so they could easily get out of sync. It is best to have one place where you control a value. In this case your ObservableObject should be the source of truth, and all updates (reading and writing to UserDefaults) should take place through there.
You write in one UserDefaults domain but read from the different. Assuming your intention is to use suite only UserDefaults, you should change one in model, like
#Published var method: CalculationMethod = .dubai {
didSet {
UserDefaults(suiteName: "method").set(method.params, forKey: "method")
self.getPrayerTime()
}
}
or if you want to use standard then just use AppStorage with default constructor, like
// use UserDefaults.standard by default
#AppStorage("method") var method: CalculationMethod = .dubai
I have the following data model:
class MyImage: : Identifiable, Equatable, ObservableObject {
let id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D?
}
class MyImageCollection : ObservableObject {
#Published var images: [MyImage] = []
#Published var selection: Set<UUID> = []
}
extension Array where Element == MyImage {
func haveCoordinates() -> Array<MyImage> {
return filter { (image) -> Bool in
return image.coordinate != nil
}
}
}
I use the collection in the views as follows:
# Top View
#StateObject var imageModel: MyImageCollection = MyImageCollection()
# Dependend Views
#ObservedObject var imageModel: MyImageCollection
So in my SwiftUI, whenever I add a new instance of MyImage via imageCollection.images.append(anImage) everything works perfectly and any View is updated accordingly, also any View using imageCollection.haveCoordinates() is updated. But I also want to have any views updated, when I change a property of an image like imageCollection.images[0].coordinate = someCoordinate. That does not work currently.
Any hints?
Your subviews need to directly observe your 'MyImage' class to be updated accordingly. Pass in your 'MyImage' instances directly into an observed object variable. Here's what that may look like...
ForEach(collection.images) { myImage in
YourSubView(image: myImage)
}
Where the image parameter is passed to an observed object property in your subview.
Switch the #ObservedObject to #EnvironmentObject in the DependantView and initialize DependentView().environmentObject(imageModel). Apple Documentation.This connects the two instances.
Also, If you want to Observe each MyImage you have to create an MyImageView that observes it directly
struct MyImage: View{
#ObservedObject var myImage: MyImage
//Rest of code
}
Thanks for your replies, very appreciated. I was trying to create a Subview with a reference to the object as ObservableObject too, but failed because I was using MapAnnotation and that is initialized with the coordinate property. Need to figure that out in detail what the difference is here, the documentation from Apple does not help very much to see any difference at a glance. I'm using some kind of workaround now by "invalidating" the MyImage instance with setting a new UUID:
class MyImage: : Identifiable, Equatable, ObservableObject {
var id = UUID()
var path: String
#Published var coordinate: CLLocationCoordinate2D? {
didSet {
id = UUID()
}
}
}
I am tinkering around with URLSession and was essentially building a super simple app to load a JSON file into ContentView sort of like a friends list from Facebook and wanted some clarity on not any errors I'm having, but rather, the internal workings of Swift's Codable protocol. Here are some code and explanations:
struct User: Identifiable, Codable {
struct Friend : Identifiable, Codable {
var name : String
var id : String
}
var id : String
var isActive: Bool
var name : String
var age: Int
var company : String
var address : String
var about : String
var registered : String
var friends : [Friend]
var checkIsActive: String {
return self.isActive ? "🟢" :"🔴"
}
}
So to summarize above, I have a User struct which contains a bunch of properties that conform to Codable.
class UsersArrayClass: ObservableObject {
#Published var userArray = [User]()
}
However, I have another class, UsersArrayClass which creates a #Published var userArray of User struct objects. This class conforms to the #ObservableObject protocol, but of course, when I try to make it conform to Codable, it does not like that likely because of the #Published property wrapper being applied on the array itself... This is what is essentially confusing me though if the User struct has Codable conformance, why doesn't userArray which contains User objects automatically conform to Codable as well?
I was thinking that maybe loading all of this up into a Core Data model would solve my problems, but I still can't move on unless I understand what I'm missing out on here, so thanks in advance for any input.
It is hacky, but we can via extension add Codable conformance to Published despite lacking access to Published internals.
extension Published: Codable where Value: Codable {
public func encode(to encoder: Encoder) throws {
guard
let storageValue =
Mirror(reflecting: self).descendant("storage")
.map(Mirror.init)?.children.first?.value,
let value =
storageValue as? Value
??
(storageValue as? Publisher).map(Mirror.init)?
.descendant("subject", "currentValue")
as? Value
else { fatalError("Failed to encode") }
try value.encode(to: encoder)
}
public init(from decoder: Decoder) throws {
self.init(initialValue: try .init(from: decoder))
}
}
Quick check:
class User: ObservableObject, Codable {
#Published var name = "Paul"
}
struct ContentView: View {
#ObservedObject var user = User()
var body: some View {
let data = try? JSONEncoder().encode(user)
let dataFromStr = """
{
"name": "Smith"
}
"""
.data(using: .utf8)
let decoded = try! JSONDecoder().decode(User.self, from: dataFromStr!)
return
VStack{
Text(verbatim: String(data: data!, encoding: .utf8) ?? "encoding failed")
Text(decoded.name)
}
}
}
/*
Cannot automatically synthesize 'Encodable' because 'Published<[User]>'
does not conform to 'Encodable' #Published var userArray = [User]()
*/
// Published declaration
#propertyWrapper struct Published<Value> { ... }
Published don't conform to Codable or any common protocol in Foundation currently
Trying to make Published conform to Codeable resulting error below:
/*
Implementation of 'Decodable' cannot be
automatically synthesized in an extension in a different file to the type
*/
extension Published: Codable where Value: Codable {}
I've set up a mvvm architecture. I've got a model, a bunch of views and for each view one single store. To illustrate my problem, consider the following:
In my model, there exists a user object user and two Views (A and B) with two Stores (Store A, Store B) which both use the user object. View A and View B are not dependent on each other (both have different stores which do not share the user object) but are both able to edit the state of the user object. Obviously, you need to propagate somehow the changes from one store to the other. In order to do so, I've built a hierarchy of stores with one root store who maintains the entire "app state" (all states of shared objects like user). Now, Store A and B only maintain references on root stores objects instead of maintaining objects themselves. I'd expected now, that if I change the object in View A, that Store A would propagate the changes to the root store which would propagate the changes once again to Store B. And when I switch to View B, I should be able now to see my changes. I used Bindings in Store A and B to refer to the root stores objects. But this doesn't work properly and I just don't understand the behavior of Swift's Binding. Here is my concrete set up as a minimalistic version:
public class RootStore: ObservableObject {
#Published var storeA: StoreA?
#Published var storeB: StoreB?
#Published var user: User
init(user: User) {
self.user = user
}
}
extension ObservableObject {
func binding<T>(for keyPath: ReferenceWritableKeyPath<Self, T>) -> Binding<T> {
Binding(get: { [unowned self] in self[keyPath: keyPath] },
set: { [unowned self] in self[keyPath: keyPath] = $0 })
}
}
public class StoreA: ObservableObject {
#Binding var user: User
init(user: Binding<User>) {
_user = user
}
}
public class StoreB: ObservableObject {
#Binding var user: User
init(user: Binding<User>) {
_user = user
}
}
In my SceneDelegate.swift, I've got the following snippet:
user = User()
let rootStore = RootStore(user: user)
let storeA = StoreA(user: rootStore.binding(for: \.user))
let storeB = StoreB(user: rootStore.binding(for: \.user))
rootStore.storeA = storeA
rootStore.storeB = storeB
let contentView = ContentView()
.environmentObject(appState) // this is used for a tabView. You can safely ignore this for this question
.environmentObject(rootStore)
then, the contentView is passed as a rootView to the UIHostingController. Now my ContentView:
struct ContentView: View {
#EnvironmentObject var appState: AppState
#EnvironmentObject var rootStore: RootStore
var body: some View {
TabView(selection: $appState.selectedTab) {
ViewA().environmentObject(rootStore.storeA!).tabItem {
Image(systemName: "location.circle.fill")
Text("ViewA")
}.tag(Tab.viewA)
ViewB().environmentObject(rootStore.storeB!).tabItem {
Image(systemName: "waveform.path.ecg")
Text("ViewB")
}.tag(Tab.viewB)
}
}
}
And now, both Views:
struct ViewA: View {
// The profileStore manages user related data
#EnvironmentObject var storeA: StoreA
var body: some View {
Section(header: HStack {
Text("Personal Information")
Spacer()
Image(systemName: "info.circle")
}) {
TextField("First name", text: $storeA.user.firstname)
}
}
}
struct ViewB: View {
#EnvironmentObject var storeB: StoreB
var body: some View {
Text("\(storeB.user.firstname)")
}
}
Finally, my issue is, that changes are just not reflected as they are supposed to be. When I change something in ViewA and switch to ViewB, I don't see the updated first name of the user. When I change back to ViewA my change is also lost. I used didSet inside the stores and similar for debugging purposes and the Binding actually seems to work. The change is propagated but somehow the View just doesn't update. I also forced with some artificial state changing (adding a state bool variable and just toggling it in an onAppear()) that the view rerenders but still, it doesn't take the updated value and I just don't know what to do.
EDIT: Here is a minimal version of my User object
public struct User {
public var id: UUID?
public var firstname: String
public var birthday: Date
public init(id: UUID? = nil,
firstname: String,
birthday: Date? = nil) {
self.id = id
self.firstname = firstname
self.birthday = birthday ?? Date()
}
}
For simplicity, I didn't pass the attributes in the SceneDelegate.swift snippet above.
In your scenario it is more appropriate to have User as-a ObservableObject and pass it by reference between stores, as well as use in corresponding views explicitly as ObservedObject.
Here is simplified demo combined from your code snapshot and applied the idea.
Tested with Xcode 11.4 / iOS 13.4
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let user = User(id: UUID(), firstname: "John")
let rootStore = RootStore(user: user)
let storeA = StoreA(user: user)
let storeB = StoreB(user: user)
rootStore.storeA = storeA
rootStore.storeB = storeB
return ContentView().environmentObject(rootStore)
}
}
public class User: ObservableObject {
public var id: UUID?
#Published public var firstname: String
#Published public var birthday: Date
public init(id: UUID? = nil,
firstname: String,
birthday: Date? = nil) {
self.id = id
self.firstname = firstname
self.birthday = birthday ?? Date()
}
}
public class RootStore: ObservableObject {
#Published var storeA: StoreA?
#Published var storeB: StoreB?
#Published var user: User
init(user: User) {
self.user = user
}
}
public class StoreA: ObservableObject {
#Published var user: User
init(user: User) {
self.user = user
}
}
public class StoreB: ObservableObject {
#Published var user: User
init(user: User) {
self.user = user
}
}
struct ContentView: View {
#EnvironmentObject var rootStore: RootStore
var body: some View {
TabView {
ViewA(user: rootStore.user).environmentObject(rootStore.storeA!).tabItem {
Image(systemName: "location.circle.fill")
Text("ViewA")
}.tag(1)
ViewB(user: rootStore.user).environmentObject(rootStore.storeB!).tabItem {
Image(systemName: "waveform.path.ecg")
Text("ViewB")
}.tag(2)
}
}
}
struct ViewA: View {
#EnvironmentObject var storeA: StoreA // keep only if it is needed in real view
#ObservedObject var user: User
init(user: User) {
self.user = user
}
var body: some View {
VStack {
HStack {
Text("Personal Information")
Image(systemName: "info.circle")
}
TextField("First name", text: $user.firstname)
}
}
}
struct ViewB: View {
#EnvironmentObject var storeB: StoreB
#ObservedObject var user: User
init(user: User) {
self.user = user
}
var body: some View {
Text("\(user.firstname)")
}
}
Providing an alternative answer here with some changes to your design as a comparison.
The shared state here is the user object. Put it in #EnvironmentObject, which is by definition the external state object shared by views in the hierarchy. This way you don't need to notify StoreA which notifies RootStore which then notifies StoreB.
Then StoreA, StoreB can be local #State, and RootStore is not required. Store A, B can be value types since there's nothing to observe.
Since #EnvironmentObject is by definition an ObservableObject, we don't need User to
conform to ObservableObject, and can thus make User a value type.
final class EOState: ObservableObject {
#Published var user = User()
}
struct ViewA: View {
#EnvironmentObject eos: EOState
#State storeA = StoreA()
// ... TextField("First name", text: $eos.user.firstname)
}
struct ViewB: View {
#EnvironmentObject eos: EOState
#State storeB = StoreB()
// ... Text("\(eos.user.firstname)")
}
The rest should be straight-forward.
What is the take-away in this comparison?
Should avoid objects observing each other, or a long publish chain. It's confusing, hard to track, and not scalable.
MVVM tells you nothing about managing state. SwiftUI is most powerful when you've learnt how to allocate and manage your states. MVVM however heavily relies upon #ObservedObject for binding, because iOS had no binding. For beginners this is dangerous, because it needs to be reference type. The result might be, as in this case, overuse of reference types which defeats the whole purpose of a SDK built around value types.
It also removes most of the boilerplate init codes, and one can focus on 1 shared state object instead of 4.
If you think SwiftUI creators are idiots, SwiftUI is not scalable and requires MVVM on top of it, IMO you are sadly mistaken.
I'm new to swift and having issues on how model classes and views interact. I'm trying implementing a toggle in the view that change the value of a property in a model class but in the struct I don't find the way implementing it:
import SwiftUI
struct DomandaRispostaView: View {
#ObservedObject var dm: DataManager = DataManager.shared
#ObservedObject var domandaRisposta : DomandaRisposta
#State private var isEnabled1 = false
var body: some View {
VStack {
Text(domandaRisposta.testoRisposta)
Form {
Toggle(isOn: $isEnabled1) {
Text(isEnabled1 ? "Checked" : "Unchecked")
domandaRisposta.valoreRisposta == true
}
}
}
}
}
the issue is at this line:
domandaRisposta.valoreRisposta == true
thanks in advance for the help
You don't have to use a separate #State variable to maintain the selection state. I have edited my code to add Codable conformance to the class as you needed.
class DomandaRisposta: ObservableObject, Codable {
#Published var valoreRisposta: Bool = false
enum CodingKeys: CodingKey {
case valoreRisposta
}
init(valoreRisposta: Bool) {
self.valoreRisposta = valoreRisposta
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
valoreRisposta = try container.decode(Bool.self, forKey: .valoreRisposta)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(valoreRisposta, forKey: .valoreRisposta)
}
}
struct DomandaRispostaView: View {
#ObservedObject var domandaRisposta: DomandaRisposta
var body: some View {
Toggle(isOn: $domandaRisposta.valoreRisposta) {
Text(domandaRisposta.valoreRisposta ? "Checked" : "Unchecked")
}
}
}
Just in case if you aren't already doing it, create the view using,
let rootView = DomandaRispostaView(domandaRisposta: model)
any reason why this would not work:
Toggle(isOn: self.$domandaRisposta.valoreRisposta) {
Text(self.domandaRisposta.valoreRisposta ? "Checked" : "Unchecked")
}