How to bind the Publisher of ObservableObject by SwiftUI - swift

I don't know how to bind Publisher in ObservableObject class on view file.
User
struct User: Identifiable,Decodable{
var id: String?
var email: String
var username: String
}
ViewModel
class UserProfileViewModel: ObservableObject{
#Published var user: User?
//fetch data and bind the user property
}
UserProfile
struct UserProfile: View {
#ObservedObject private var userProfileVM = UserProfileViewModel()
var body: some View {
VStack{
UserForm(name: Binding($userProfileVM.user.username)!,
email: Binding($userProfileVM.user.email)!
) }
}
}
UserForm
struct UserForm: View {
#Binding var name: String
#Binding var email: String
var body: some View {
TextField("name", text: $name)
.keyboardType(.namePhonePad)
TextField("email", text: $email)
}
}
I don't know how to bind userProfileVM.user.username and userProfileVM.user.email in UserProfile on UserForm.
Now I got the errors Value of optional type 'User?' must be unwrapped to refer to member 'email' of wrapped base type 'User' & Chain the optional using '?' to access member 'email' only for non-'nil' base values in UserProfile.
Please how to resolve it.

Generally if you are using ! there is something incorrect. It should be very rare.
You need to check if the #Published var user: User? is not nil.
struct UserProfile: View {
//StateObject is for initializing ObservedObject is for passng around.
#StateObject private var userProfileVM = UserProfileViewModel()
var body: some View {
VStack{
if let userB = Binding($userProfileVM.user){
//Check if there is a user.
UserForm(name: userB.username, email: userB.email)
}else{
//Show/do something if there isn't
ProgressView()
.task{
//Load user somehow...
}
}
}
}
}

Related

How to observer a property in swift ui

How to observe property value in SwiftUI.
I know some basic publisher and observer patterns. But here is a scenario i am not able to implement.
class ScanedDevice: NSObject, Identifiable {
//some variables
var currentStatusText: String = "Pending"
}
here CurrentStatusText is changed by some other callback method that update the status.
Here there is Model class i am using
class SampleModel: ObservableObject{
#Published var devicesToUpdated : [ScanedDevice] = []
}
swiftui component:
struct ReviewView: View {
#ObservedObject var model: SampleModel
var body: some View {
ForEach(model.devicesToUpdated){ device in
Text(device.currentStatusText)
}
}
}
Here in UI I want to see the real-time status
I tried using publisher inside ScanDevice class but sure can to use it in 2 layer
You can observe your class ScanedDevice, however you need to manually use a objectWillChange.send(),
to action the observable change, as shown in this example code.
class ScanedDevice: NSObject, Identifiable {
var name: String = "some name"
var currentStatusText: String = "Pending"
init(name: String) {
self.name = name
}
}
class SampleViewModel: ObservableObject{
#Published var devicesToUpdated: [ScanedDevice] = []
}
struct ReviewView: View {
#ObservedObject var viewmodel: SampleViewModel
var body: some View {
VStack (spacing: 33) {
ForEach(viewmodel.devicesToUpdated){ device in
HStack {
Text(device.name)
Text(device.currentStatusText).foregroundColor(.red)
}
Button("Change \(device.name)") {
viewmodel.objectWillChange.send() // <--- here
device.currentStatusText = UUID().uuidString
}.buttonStyle(.bordered)
}
}
}
}
struct ContentView: View {
#StateObject var viewmodel = SampleViewModel()
var body: some View {
ReviewView(viewmodel: viewmodel)
.onAppear {
viewmodel.devicesToUpdated = [ScanedDevice(name: "device-1"), ScanedDevice(name: "device-2")]
}
}
}

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

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.

Passing data with ObservableObject keyword

How can I move data to other screens with ObservableObject keyword?
I save the data on the first page to the variable I created with the keyword Published, but I cannot access this data on the second page.
User Model
import Combine
struct User: Codable, Identifiable {
var id = UUID()
var name: String
var surName: String
}
UserDataStore
import Combine
class UserDataStore: ObservableObject {
#Published var users: [User] = []
}
ContentView
I get information from the user with TextField objects on the contentView screen. After pressing the button, I add it to the array in the UserDataStore. I redirect to the detail page.
struct ContentView: View {
#State var name: String = ""
#State var surName: String = ""
#State var user = User(name: "", surName: "")
#State var show: Bool = false
#ObservedObject var userStore = UserDataStore()
var body: some View {
NavigationView {
VStack(spacing: 50) {
TextField("isim", text: $name)
TextField("soyİsim", text: $surName)
NavigationLink(
destination: DetailView(),
isActive: $show,
label: {
Button(action: {
self.user.name = name
self.user.surName = surName
self.userStore.users.append(user)
self.show = true
}) {
Text("Kaydet")
}
})
}
}
}
}
DetailView
On the detail page, I try to view the recorded information, but I cannot.
struct DetailView: View {
#ObservedObject var user = UserDataStore()
var body: some View {
ForEach(user.users) { item in
Text("\(item.name)")
}
}
}
Like #"New Dev" explained you're initializing a new instance of UserDataStore therefore your data isn't accessible from the DetailView.
You can use an EnvironmentObject to access the data from ContentView to DetailView.
In order to do this you would have to set the NavigationLinks destination to:
destination: DetailView().environmentObject(userStore)
Then you can access it from the DetailView like this:
#EnvironmentObject var user: UserDataStore

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.

#EnvironmentObject Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'

I have an EnvironmentObject that I want to use as a datasource for my button title:
struct ContentView: View {
#State var showDetailsView = false
#EnvironmentObject var storage: Storage
var body: some View {
NavigationView {
ZStack {
Button(action: {
self.doSomethingAsync()
}) {
Text($storage.buttonTitle) // won't compile here
Here is my storage object:
class Storage: ObservableObject {
#Published var buttonTitle: String
#Published var dataObject: DataObject
init(dataObject: DataObject = DataObject(name: "Test")) {
self.dataObject = dataObject
buttonTitle = "Try"
}
}
Text takes in a String not a Binding<String>. Replace the line you pointed out with the following:
Text(storage.buttonTitle)