I am struggle with understanding about why i have to give Popup view dependency named vm while calling this view since it is observable
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView() /// this line shows error
}
}
}
struct DetailView:View {
#ObservedObject var vm:ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
You have to set your vm property when you init your View. Which is the usual way.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView(vm: ViewModel()) // Initiate your ViewModel() and pass it as DetailView() parameter
}
}
}
struct DetailView:View {
var vm: ViewModel
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Or you could use #EnvironmentObject. You have to pass an .environmentObject(yourObject) to the view where you want to use yourObject, but again you'll have to initialize it before passing it.
I'm not sure it's the good way to do it btw, as an environmentObject can be accessible to all childs view of the view you declared the .environmentObject on, and you usually need one ViewModel for only one View.
struct ContentView: View {
#State private var showPopup1 = false
var body: some View {
VStack {
Button(action: { withAnimation { self.showPopup1.toggle()}}){
Text("showPopup1") }
Text("title")
DetailView().environmentObject(ViewModel()) // Pass your ViewModel() as an environmentObject
}
}
}
struct DetailView:View {
#EnvironmentObject var vm: ViewModel // you can now use your vm, and access it the same say in all childs view of DetailView
var body : some View {
Text("value from VM")
}
}
class ViewModel: ObservableObject {
#Published var title:String = ""
}
Related
In my app, I have a Screen with Toolbar and Main View.
VStack {
ToolbarView()
MainView()
}
Think of it like this:
Toolbar has its own View and ToolbarViewModel where we can select “Tools”
struct ToolbarView: View {
#StateObject private var VM = ToolbarViewModel()
var body: some View {
Text("Toolbar View")
}
}
#MainActor final class ToolbarViewModel: ObservableObject {
var selectedTool: Int = 1
func selectTool() {
//We select a new tool
selectedTool = 2
}
}
Main view has its own View and MainViewModel
struct MainView: View {
#StateObject private var VM = MainViewModel()
var body: some View {
Text("Main View")
}
}
#MainActor final class MainViewModel: ObservableObject {
var selectedTool: Int = 1
}
Now, when I tap a button in the ToolbarView and call a function in ToolbarViewModel to select a new tool, the tool must change in the MainViewModel too.
What would be the correct way of implementing this?
In the screen with the MainView and ToolbarView instances, create #StateObject(s) for both
#StateObject private var mainVM = MainViewModel()
#StateObject private var toolbarVM = ToolbarViewModel()
Then, create Observed objects in both your instances:
ToolbarView:
struct ToolbarView: View {
#ObservedObject var VM: ToolbarViewModel
var body: some View {
Text("Toolbar View")
}
}
MainView:
struct MainView: View {
#ObservedObject var VM: MainViewModel
var body: some View {
Text("Main View")
}
}
Then pass your objects in the screen than you created your instances:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
Finally, whenever you make a change you can just listen to it like:
VStack {
ToolbarView(VM: toolbarVM)
MainView(VM: mainVM)
}
.onChange(of: toolbarVM.isDrawing) { newValue in {
mainVM.isDrawing = newValue
}
.onChange(of: mainVM.isDrawing) { newValue in {
toolbarVM.isDrawing = newValue
}
We don't use view model objects in SwiftUI. The View data struct is already the view model that SwiftUI uses to create/update/remove UIView objects automatically for us. The property wrappers give the struct reference semantics giving us the best of both worlds. You'll have to learn #State and #Binding and put the shared state in a parent View, then pass it down as a let for read access or #Bindng var for write access, e.g.
#State var tools = Tools()
...
VStack {
ToolbarView(tools: $tools)
MainView(tools: tools)
}
struct Tools {
var selectedTool: Int = 1
mutating func selectTool() {
//We select a new tool
selectedTool = 2
}
}
struct MainView: View {
let tools: Tools
var body: some View {
Text("Main View \(tools.selected)")
}
}
struct ToolbarView: View {
#Binding var tools: Tools
var body: some View {
Text("Toolbar View")
Button("Select") {
tools.selectTool()
}
}
}
I want to have a mother view that displays a different child view based on a #State bool.
However I get two errors when doing that.
On the start of the body closure:
Struct 'ViewBuilder' requires that 'EmptyCommands' conform to 'View'
Return type of property 'body' requires that 'EmptyCommands' conform
to 'View'
And inside the control flow statement:
Closure containing control flow statement cannot be used with result
builder 'CommandsBuilder'
struct ResultView: View {
#State var resultViewSuccess = false
let resultViewModel: ResultViewModel
var body: some View {
Group {
if let showresultView = resultViewSuccess {
ViewOne(viewModel: resultViewModel)
} else {
ViewTwo(
resultViewSuccess: $resultViewSuccess,
viewModel: resultViewModel,
)
}
}
}
}
struct ViewTwo: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
NavigationView {
ButtonResult(
resultViewSuccess: $resultViewSuccess,
viewModel: viewModel)
}
}
struct ButtonResult: View {
#Binding var resultViewSuccess: Bool
#StateObject var viewModel: ResultViewModel
var body: some View {
Button(action: {
self.resultViewSuccess = true
}) {
Text("View Results")
}
}
}
My code is something like this:
class ViewModel: ObservableObject {
#Published var value = ""
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.userSession != nil {
MyTabView()
} else {
LoginView()
}
}
.environmentObject(viewModel)
}
}
struct MyTabView: View {
var body: some View {
TabView {
View1()
.tabItem{}
View2()
.tabItem{}
View3()
.tabItem{}
View4()
.tabItem{}
}
}
}
struct View4: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
NavigationLink(destination: EditView().environmentObject(viewModel)){
Text("Edit")
}
}
}
}
struct EditView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
if viewModel.value != "" { //this is where I get the error
Text("\(viewModel.value)")
}
}
}
I've tried putting the environmentObject at MyTabView() in ContentView()
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.userSession != nil {
MyTabView().environmentObject(viewModel)
} else {
LoginView()
}
}
}
}
I've tried putting the environmentObject at NavigationView in View4()
struct View4: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
NavigationLink(destination: EditView()){
Text("Edit")
}
}.environmentObject(viewModel)
}
}
The value from ViewModel is not getting passed into the EditView. I have tried many solutions I can find but non of those are helping with the error.
Can anyone please let me know what have I done wrong?
Here is the test code I used (entirely based on yours) that shows
"...The value from ViewModel is getting passed into the EditView...".
Unless I missed something, the code you provide does not reproduce the error you show.
class ViewModel: ObservableObject {
#Published var value = ""
#Published var userSession: String? // <-- for testing
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
Group {
if viewModel.userSession != nil {
MyTabView()
} else {
LoginView()
}
}.environmentObject(viewModel)
}
}
struct MyTabView: View {
var body: some View {
TabView {
Text("View1").tabItem{Text("View1")}
Text("View2").tabItem{Text("View2")}
Text("View3").tabItem{Text("View3")}
View4().tabItem{Text("View4")}
}
}
}
struct View4: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
NavigationView {
NavigationLink(destination: EditView().environmentObject(viewModel)){
Text("Edit")
}
}
}
}
struct EditView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
if viewModel.value != "" { // <-- here no error
Text(viewModel.value) // <-- here viewModel.value is a String
}
}
}
struct LoginView: View {
#EnvironmentObject var viewModel: ViewModel
var body: some View {
Button("Click me", action: {
viewModel.userSession = "something" // <-- to trigger the if in ContentView
viewModel.value = "testing-4-5-6" // <-- here change the value
})
}
}
Try this code and let us know if you get the error you show.
If I create an ObservableObject with a #Published property and inject it into a SwifUI view with .environmentObject(), the view responds to changes in the ObservableObject as expected.
class CounterStore: ObservableObject {
#Published private(set) var counter = 0
func increment() {
counter += 1
}
}
struct ContentView: View {
#EnvironmentObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Tapping on "Increment" will increase the count.
However, if I don't use the EnvironmentObject and instead pass the store instance into the view, the compiler does not complain, the store method increment() is called when the button is tapped, but the count in the View does not update.
struct ContentViewWithStoreAsParameter: View {
var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter) (DOES NOT UPDATE)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
Here's how I'm calling both Views:
#main
struct testApp: App {
var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // broken
}
}
}
}
Is there a way to pass an ObservableObject into a View as a parameter? (Or what magic is .environmentalObject() doing behind the scenes?)
It should be observed somehow, so next works
struct ContentViewWithStoreAsParameter: View {
#ObservedObject var store: CounterStore
//...
You can pass down your store easily as #StateObject:
#main
struct testApp: App {
#StateObject var store = CounterStore()
var body: some Scene {
WindowGroup {
VStack {
ContentView().environmentObject(store) // works
ContentViewWithStoreAsParameter(store: store) // also works
}
}
}
}
struct ContentViewWithStoreAsParameter: View {
#StateObject var store: CounterStore
var body: some View {
VStack {
Text("Count: \(store.counter)") // now it does update
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
However, the store should normally only be available for the views that need it, why this solution would make the most sense in this context:
struct ContentView: View {
#StateObject var store = CounterStore()
var body: some View {
VStack {
Text("Count: \(store.counter)")
Button(action: { store.increment() }) {
Text("Increment")
}
}
}
}
I want to have a #State property in my ViewModel.
This is my code:
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Hi all")
ContentView2(viewModel: viewModel)
}
}
struct ContentView2: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
Text("Hi all")
.modifier(GeometryGetter(rect: viewModel.$dynamicRect))
}
}
public struct GeometryGetter: ViewModifier {
#Binding var rect: CGRect
public init(rect: Binding<CGRect>) {
self._rect = rect
}
public func body(content: Content) -> some View {
return GeometryReader { proxy -> Color in
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global)
}
return Color.clear
}
}
}
}
extension ContentView {
class ViewModel: ObservableObject {
#State var dynamicRect: CGRect = .zero
}
}
Problem is that i'm getting this error:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update
Is this doable? I tried looking for information about holding the #State in the viewmodel buy couldn't find something too useful