Passing Value to a Another View: Using State and Binding In SwiftUI - swift

I am trying to pass a value from my View LingView, to another View ContentView. I have a state variable in the login view as such:
#State var userName: String = ""
#State private var password: String = ""
I then pass the value from the Content View Constructor in two places:
This is in RootView
var body: some View {
ContentView(userName: LoginView().$userName)
.fullScreenCover(isPresented: $authenticator.needsAuthentication) {
LoginView()
.environmentObject(authenticator) // see note
}
}
This is in Content View:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(userName: Login().$userName)
.environmentObject(Authenticator())
}
}
I want to use the variable to pass into the getPictures() function that a database file uses. I am kind of confused as to what I am suppose to do. The parent view in this case would be LoginView correct? I am unsure why I keep getting: Cannot find type 'userName' in scope.
#Binding var userName: String
print(userName)
var pictureURL = DbHelper().getPictures()
After running the following code. I understand that you should make state private to a View, but in this case how would I pass the state value to the content view? The LoginView does not call the ContentView directly. Maybe I don't understand Bindings and State, but I have read this article: https://learnappmaking.com/binding-swiftui-how-to/

You are initialising two separate login views. The username binding passed to ContentView is therefore a different binding to the one you have under .fullscreenCover
To make it work, you can declare a State variable in RootView,
#State private var userName: String = "" // In RootView
then pass its binding to both ContentView and LoginView.
#Binding var userName: String // In ContentView and LoginView
Simply put, State holds your actual username value, while Binding gives you a method of seeing and changing it from somewhere else.

Related

Updating property wrapper like #StateObject, affects other view rendering that does not use that property

When using different property wrappers associated with view updates, changes in one place affect rendering of views that do not use that property.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
Text(thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
class MyViewModel: ObservableObject {
#Published var firstTitle = "firstTitle"
#Published var secondTitle = "secondTitle"
func chageFirstTitle() {
firstTitle = "hello world"
}
}
I understand that the reason why the Text exposing the viewModel.secondTitle is re-rendered is because the #StateObject varviewModel = MyViewModel() dependency changed when the `viewModel.firstTitle changed.
However, I don't know why Text using #State var thirdTitle = "thirdTitle" is re-rendered too. In WWDC21 session Demystify SwiftUI, I saw that the view is re-rendered only when the related dependency is updated according to the dependency graph. But, even though the thirdTitle is irrelevant to the change of the viewModel, third Text using that dependency is re-rendered and the background color is changed.
What's even more confusing is that if I seperate the third Text into a separate view ( ThirdView ) and receive the thirdTitle using #Binding, the background color does not change because it is not re-rendering at that time.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
ThirdView(text: $thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
Regarding the situation I explained, could you help me to understand the rendering conditions of the view?
To render SwiftUI calls body property of a view (it is computable, i.e. executes completely on call). This call is performed whenever any view dependency, i.e. dynamic property, is changed.
So, viewModel.chageFirstTitle() changes dependency for ContentView and ContentView.body is called and every primitive in it is rendered. ThirdView also created but as far as its dependency is not changed, its body is not called, so content is not re-rendered.
A few things wrong here. We don't use view model objects in SwiftUI for view data, it's quite inefficient/buggy to do so. Instead, use a struct with mutating funcs with an #State. Pass in params to sub-Views as lets for read access, #Binding is only when you need write access. In terms of rendering, first of all body is only called if the let property is different from the last time the sub-View is init, then it diffs the body from the last time it was called, if there are any differences then SwiftUI adds/removes/updates actual UIKit UIViews on your behalf, then actual rendering of those UIViews, e.g. drawRect, is done by CoreGraphics.
struct ContentViewConfig {
var firstTitle = "firstTitle"
var secondTitle = "secondTitle"
mutating func changeFirstTitle() {
firstTitle = "hello world"
}
}
struct ContentView: View {
#State var config = Config()
...
struct ThirdView: View {
let text: String
...
Combine's ObservableObject is usually only used when needing to use Combine, e.g. using combineLatest with multiple publishers or for a Store object to hold the model struct arrays in #Published properties that are not tied to a View's lifetime like #State. Your use case doesn't look like a valid use of ObservableObject.

Passing environment object between non-view classes in SwiftUI

I understand that EnvironmentObject property wrapper can be used to pass around objects to views. I have a session object which I am passing around to my views. Now I have a requirement to pass this into one of my model classes (i.e., non-view). Ideally, this model (receiving the session object) is instantiated as a StateObject.
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var transactionsModel = TransactionsModel(token: session.token)
The code above will not work (understandably) because:
cannot use instance member 'session' within property initializer; property initializers run before 'self' is available
Any suggestions on how I can pass in the session into TransactionsModel?
Try initializing the StateObject in an .onAppear() prop to a child view, like this:
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var transactionsModel: TransactionModel?
var body: some View {
SomeChildView()
.onAppear(perform: {
transactionModel = TransactionModel(token: session.token)
})
}
}
This way, the variable is initialized when the view renders on the screen. It doesn't matter much which child view you add the onAppear prop to, as long as it is rendered as soon as the parent does.
The best way that I've found to do this (because you cannot have an optional StateObject) is:
struct CreditDetailsView: View {
#EnvironmentObject var session: Session
#StateObject var localModel = LocalModel()
var body: some View {
SomeChildView()
.onAppear {
localModel.transactionModel = TransactionModel(token: session.token)
}
}
class LocalModel: ObservableObject {
#Published transactionModel: TransactionModel?
}
}
This is an incorrect answer. Please check out the chosen answer above.
You can access session object in init. In this case, transactionsModel should be done to be already initialized in any ways.
#EnvironmentObject var session: Session
#StateObject var transactionsModel = TransitionalModel(token: "")
init() {
let token = self.session.token
_transactionsModel = StateObject(wrappedValue: TransitionalModel(token: token))
}
Although it's out of the question, I am not sure if it's good way to pass something between them who look like being in different levels in the level of View.

SwiftUi Fullscreen cover without using state variable

i'm trying to do a simple login screen using swiftUI.
i put my isLogin into a class and set it as an EnviromentObject Variable.
How do I use it in a fullscreen cover?
My class
class AuthUser:ObservableObject {
#Published var isLogin:Bool = false
#Published var isCorrect:Bool = false
}
My View :
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
#State var showLogin:Bool = false
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $showLogin, content: AfterLogin.init)
}
}
You can use binding directly to property of environment object, like
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
The structure you're attempting to follow is called MVVM or Model-View-View-Model, which is nearly a requirement for swiftUI I believe the only thing that you're missing is the actual usage of that ViewModel, or in your case the AuthUser. So let's dig into that.
View Model
class AuthUser: ObservableObject {
#Published var isLogin = false
//Any other code, methods, constructors you want.
}
View
struct UIController: View {
#ObservedObject var userAuth = AuthUser()
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
What did I change? I changed your #EnvironmentObject to a ObservedObject because in your case you're not likely to actually be using an environment object. What are those special tags?
ObservableObject, this means that it can be observed for state changes.
ObservedObject, an object that is watching for state changes.
Published, an object that has public availability.
EnvironmentObject, an object that is created for the environment and accessible wherever in the app. This particular object is not immediately disposed which is why we removed it in your example.
So what's the purpose of doing things this way? Well it has to do with abstraction and readability. MVVM frameworks provide a way to Bind a view to an object. That binding means that any changes on the view change the data, and any changes to the data change the view. Swift makes this concept simple enough. In your case we're binding the isPresented to the ObservedObject and checking that objects isLogin state. If it changes, the view is also changed.

Passing state between 2 pages SwiftUI

I'm trying to re create an older version of my Onboarding setup with the new SwiftUI and when I try to share the state so the view changes, it simply doesn't know that something has changed, this is what I'm doing:
In the main .swift struct (not ContentView.swift) I defined the pages like this:
#main
struct AnotherAPP: App {
#ObservedObject var onBoardingUserDefaults = OnBoardingUserDefaults()
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
// Onboarding screen
if (onBoardingUserDefaults.isOnBoardingDone == false) {
OnboardingPageView()
} else {
UserLoginView()
}
}
}
}
So on the onBoarding page when I click the button to go to the login, it stores it, but it doesn't actually refreshes the view. There (in the OnboardingPageView.swift) I call the UserDefaults like this:
#ObservedObject private var onBoardingUserDefaults = OnBoardingUserDefaults()
and on the button I change it like this:
self.onBoardingUserDefaults.isOnBoardingDone = true
UserDefaults.standard.synchronize()
So what's going on?
I know for instance if I create a #State on the #main and I bind it to the OnboardingPageView it works, as soon as I hit that button it takes me there.
You can use AppStorage to manage UserDefaults variable in multiple views:
#main
struct TestApp: App {
#AppStorage("isOnBoardingDone") var isOnBoardingDone = false
var body: some Scene {
WindowGroup {
if !isOnBoardingDone {
OnboardingPageView(isOnBoardingDone: $isOnBoardingDone)
} else {
UserLoginView()
}
}
}
}
struct OnboardingPageView: View {
#Binding var isOnBoardingDone: Bool
var body: some View {
Button("Complete") {
isOnBoardingDone = true
}
}
}
If I understood correctly, you are trying to pass the value of a state variable in the Content View to another view in the same app. For simplicity, Let's say your variable is initialised as follows in ContentView:
#State private var countryIndex = 0 //Assuming the name of the variable is countryIndex
Now, to transfer the value write the following in the Content View (or wherever the variable is initially):
//Other code
NavigationLink(destination: NextPage(valueFromContentView: $countryIndex)) {
Text("Moving On")
}//In this case, the variable that will store the value of countryIndex in the other view is called valueFromContentView
//Close your VStacks and your body and content view with a '}'
In your second view or the other view, initialise a Binding variable called valueFromContentView using:
#Binding var valueFromContentView: Int
Then, scroll down to the code that creates your previews. FYI, It is another struct called ViewName_Previews: PreviewProvider { ... }
IF you haven't changed anything, it will be:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
}
}
Remember, my second view is called NextPage.
Inside the previews braces, enter the code:
NextPage(valueFromContentView: .constant(0))
So, the code that creates the preview for your application now looks like:
struct NextPage_Previews: PreviewProvider {
static var previews: some View {
NextPage(valueFromContentView: .constant(0)) //This is what you add
}
}
Remember, NextPage is the name of my view and valueFromContentView is teh binding variable that I initialised above
Like this, you now can transfer the value of a variable in one view to another view.

SwiftUI pass data to subview

So I have read a lot about swiftUI and I am confused. I understand the #State and how it update the view behind the scenes. The thing I can't understand is why when I the subview in this example is automatically updated when the state changes in the top view/struct given the fact that on the subview the var subname is not a #State property. I would expect this not to be updated. Can someone enlighten me please?
import SwiftUI
struct SwiftUIView: View {
#State private var name = "George"
var body: some View {
VStack{
SubView(subName: name).foregroundColor(.red)
Button(action: {
self.name = "George2"
}) {
Text("Change view name")
}
}
}
}
struct SubView: View {
var subName:String
var body: some View {
Text(subName)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
PS.
If I change the subview to
struct SubView: View {
#State var subName:String
var body: some View {
Text(subName)
}
}
it's not updated when I press the button.
var subName: String
struct SubView: View {
var subName: String
// ...
}
In the above example you don't really have a state. When the subName property in the parent view changes, the whole SubView is redrawn and thus provided with a new value for the subName property. It's basically a constant (try changing it to let, you'll have the same result).
#State var subName: String
struct SubView: View {
#State var subName: String
// ...
}
Here you do have a #State property which is initialised once (the first time the SubView is created) and remains the same even when a view is redrawn. Whenever you change the subName variable in the parent view, in the SubView it will stay the same (it may be modified from within the SubView but will not change the parent's variable).
#Binding var subName: String
struct SubView: View {
#Binding var subName: String
// ...
}
If you have a #Binding property you can directly access the parent's #State variable. Whenever it's changed in the parent it's changed in the SubView (and the other way around) - that's why it's called binding.
Whenever an #State property of a View changes, the view is refreshed, which means its whole body is recalculate. Since SubView is part of the body of SwiftUIView, whenever SwiftUIView refreshes itself, it redraws its SubView using the new value of its #State name property.