Update EnvironmentObject value in ViewModel and then reflect the update in a View - swift

I have an environment object with the property auth in my root ContentView:
class User: ObservableObject {
#Published var auth = false
}
My goal is to update auth to true inside of a function in my AuthViewModel:
class AuthViewModel: ObservableObject {
var user: User = User()
func verifyCode(phoneNumber: String, secret: String) {
self.user.auth = true
}.task.resume()
}
}
And then in my AuthView, I want to print the change when the function verifyCode is called:
#EnvironmentObject var user: User
var body: some View {
print("AUTH SETTINGS -------->",user.auth)
return VStack() { ........

If your class User doesn´t contain any further logic it would be best to declare it as struct and either let it live inside your AuthViewModel or in the View as a #State var. You should have only one source of truth for your data.
As for the print question:
let _ = print(....)
should work.

Related

SwiftUI how to use enviromentObject in viewModel init?

I have a base API:
class API: ObservableObject {
#Published private(set) var isAccessTokenValid = false
#AppStorage("AccessToken") var accessToken: String = ""
#AppStorage("RefreshToken") var refreshToken: String = ""
func request1() {}
func request2() {}
}
And it was passed to all views by using .environmentObject(API()). So in any views can easily access the API to do the http request calls.
Also I have a view model to fetch some data on the view appears:
class ViewModel: ObservableObject {
#Published var data: [SomeResponseType]
init() {
// do the request and then init data using the response
}
}
struct ViewA: View {
#StateObject private var model = ViewModel()
var body: View {
VStack {
model.data...
}
}
}
But in the init(), the API is not accessable in the ViewModel.
So, to solve this problem, I found 3 solutions:
Solution 1: Change API to Singleton:
class API: ObservableObject {
static let shared = API()
...
}
Also we should change the enviromentObject(API()) to enviromentObject(API.shared).
So in the ViewModel, it can use API.shared directly.
Solution 2: Call the request on the onAppear/task
struct ViewA: View {
#EnvironmentObject var api: API
#State private var data: [SomeResponseType] = []
var body: View {
VStack {}
.task {
let r = try? await api.request1()
if let d = r {
data = d
}
}
}
}
Solution 3: Setup the API to the ViewModel onAppear/task
class ViewModel: ObservableObject {
#Published var data: [SomeResponseType]
var api: API?
setup(api: API) { self.api = api }
requestCall() { self.api?.reqeust1() }
}
struct ViewA: View {
#EnvironmentObject var api: API
#StateObject private var model = ViewModel()
var body: View {
VStack {}
.onAppear {
model.setup(api)
model.requestCall()
}
}
}
Even though, I still think they are not a SwiftUI way. And my questions is a little XY problem. Probably, the root question is how to refactor my API. But I am new to SwiftUI.
Solution 2 is best. You can also try/catch the exception and set an #State for an error message.
Try to avoid using UIKit style view model objects because in SwiftUI the View data struct plus #State already fulfils that role. You only need #StateObject when you need a reference type for view data which is not very often given now we have .task.

How do I change the variable "url" which is inside of a class by using a struct? in swift

I'm new to swift and I cannot figure out how to change the url variable by inputting the Binding var url from the struct. I keep getting errors regardless of how I try it. Any help would v vvv appreciated
struct SearchView : View {
#State var showSearchView = true
#State var color = Color.black.opacity(0.7)
**#Binding var url: String**
#ObservedObject var Books = getData()
var body: some View{
if self.showSearchView
{
NavigationView{
List(Books.data) {i in
....}
class getData : ObservableObject{
#Published var data = [Book]()
**var url** = "https://www.googleapis.com/books/v1/volumes?q=harry+potter"
init() {....}
First of all if the current view owns the model object use #StateObject.
Second of all please name classes with starting uppercase and functions and variables with starting lowercase letter.
#StateObject var books = GetData()
...
class GetData : ObservableObject {
You don't need a Binding just address the property directly
books.url = "https://apple.com"
and delete
#Binding var url: String
And if you need to display the changed value immediately use a #Published property and bind the it directly
class GetData : ObservableObject {
#Published var url = "https://www.googleapis.com/books/v1/volumes?q=harry+potter"
...
struct SearchView : View {
#StateObject var books = GetData()
var body: some View {
VStack{
Text(books.url)
TextField("URL", text: $books.url)
}
}
}
Change the *var url** = "https://www.googleapis.com/books/v1/volumes?q=harry+potter" in the second view with: #State var url = "https://www.googleapis.com/books/v1/volumes?q=harry+potter" so it can be mutable

How can I use the #Binding Wrapper in the following mvvm architecture?

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.

Changing a property of my ObservableObject instance doesn't update View. Code:

//in ContentView.swift:
struct ContentView: View {
#EnvironmentObject var app_state_instance : AppState //this is set to a default of false.
var body: some View {
if self.app_state_instance.complete == true {
Image("AppIcon")
}}
public class AppState : ObservableObject {
#Published var inProgress: Bool = false
#Published var complete : Bool = false
public static var app_state_instance: AppState? //I init this instance in SceneDelegate & reference it throughout the app via AppState.app_state_instance
...
}
in AnotherFile.swift:
AppState.app_state_instance?.complete = true
in SceneDelegate.swift:
app_state_instance = AppState( UserDefaults.standard.string(forKey: "username"), UserDefaults.standard.string(forKey: "password") )
AppState.app_state_instance = app_state_instance
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(app_state_instance!))
This ^ does not trigger an update of my view.Anyone know why?
Also, is it possible to make a static var # published?
Also, I'm kind somewhat new to class-based + structs-as-values paradigm, if you have any tips on improving this please do share.
I've considered using an 'inout' ContentView struct reference to update struct state from other files (as going through app_state_instance isn't working with this code).

How to pass a value from an EnvironmentObject to a class instance in SwiftUI?

I'm trying to assign the value from an EnvironmentObject called userSettings to a class instance called categoryData, I get an error when trying to assign the value to the class here ObserverCategory(userID: self.userSettings.id)
Error says:
Cannot use instance member 'userSettings' within property initializer; property initializers run before 'self' is available
Here's my code:
This is my class for the environment object:
//user settings
final class UserSettings: ObservableObject {
#Published var name : String = String()
#Published var id : String = "12345"
}
And next is the code where I'm trying to assign its values:
//user settings
#EnvironmentObject var userSettings: UserSettings
//instance of observer object
#ObservedObject var categoryData = ObserverCategory(userID: userSettings.id)
class ObserverCategory : ObservableObject {
let userID : String
init(userID: String) {
let db = Firestore.firestore().collection("users/\(userID)/categories") //
db.addSnapshotListener { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for doc in snap!.documentChanges {
//code
}
}
}
}
Can somebody guide me to solve this error?
Thanks
Because the #EnvironmentObject and #ObservedObject are initializing at the same time. So you cant use one of them as an argument for another one.
You can make the ObservedObject more lazy. So you can associate it the EnvironmentObject when it's available. for example:
struct CategoryView: View {
//instance of observer object
#ObservedObject var categoryData: ObserverCategory
var body: some View { ,,, }
}
Then pass it like:
struct ContentView: View {
//user settings
#EnvironmentObject var userSettings: UserSettings
var body: some View {
CategoryView(categoryData: ObserverCategory(userID: userSettings.id))
}
}