Using #State in my viewmodel and have it update my UI - swiftui

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

Related

How to establish communication between ViewModels of the same screen in SwiftUI MVVM

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()
}
}
}

Infinite loop when setting a #Published property

I have a simple view that shows some photos, through a list. Clicking on any row should display a detailed view of that photo. I'm using the MVVM pattern. However, an infinite loop occurs when I try to set the “selectedPhoto” property of the view model. Is there any way to avoid this loop without having to create a property in the detailed view itself?
Here is the Photo struct:
struct Photo: Identifiable {
var id = UUID()
var name: String
}
Here is the ContentView with an extension (the “updatePhoto” method is causing the infinite loop):
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
showDetailView(with: selectedPhoto)
}
}
.navigationTitle("Favorite Photo")
}
}
}
extension ContentView {
func showDetailView(with selectedPhoto: Photo?) -> some View {
if let selectedPhoto = selectedPhoto {
viewModel.updatePhoto(selectedPhoto)
}
return DetailView(viewModel: viewModel)
}
}
Here is the view model:
class ViewModel: ObservableObject {
#Published var photos = [
Photo(name: "Photo 1"),
Photo(name: "Photo 2"),
Photo(name: "Photo 3")
]
#Published var selectedPhoto: Photo?
func updatePhoto(_ selectedPhoto: Photo?) {
self.selectedPhoto = selectedPhoto
}
}
And here is the DetailView:
struct DetailView: View {
#ObservedObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
Text(viewModel.selectedPhoto?.name ?? "Unknown photo name")
}
}
Try this approach, using a NavigationLink to present the DetailView,
and passing the selectedPhoto to it using #State var selectedPhoto: Photo.
struct Photo: Identifiable {
let id = UUID()
var name: String
}
class ViewModel: ObservableObject {
#Published var photos = [Photo(name: "Photo 1"),Photo(name: "Photo 2"),Photo(name: "Photo 3")]
}
struct DetailView: View {
#State var selectedPhoto: Photo
var body: some View {
Text(selectedPhoto.name)
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
ForEach(viewModel.photos) { selectedPhoto in
NavigationLink(selectedPhoto.name, destination: DetailView(selectedPhoto: selectedPhoto))
}
}
.navigationTitle("Favorite Photo")
}
}
}
Note that NavigationView is being deprecated and you will have to use NavigationStack instead.

I'm using #EnvironmentObject in SwiftUI and I got this "View.environmentObject(_:) for ViewModel may be missing as an ancestor of this view" Error

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.

Missing argument for parameter 'View Call' in call

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 = ""
}

A View.environmentObject(_:) for may be missing as an ancestor of this view

I just updated to Xcode 11.4 and it's broken my code. I am storing some user settings in an ObservableObject as follows:
class UserSettings: ObservableObject {
#Published var cardOrder = UserDefaults.standard.integer(forKey: "Card Order")
#Published var cardTheme = UserDefaults.standard.integer(forKey: "Card Theme")
#Published var translation = UserDefaults.standard.integer(forKey: "Translation")
#Published var overdueFirst = UserDefaults.standard.bool(forKey: "Overdue First")
#Published var randomNum = 0
}
This is my main menu, the settings environment object is successfully passed down to the Settings view where I'm able to save and retrieve user selections.
struct ContentView: View {
#State var settings = UserSettings()
var body: some View {
SubView().environmentObject(settings)
}
}
struct SubView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
List {
NavigationLink (destination: Flashcard()){
HStack {
Image(systemName: "rectangle.on.rectangle.angled")
Text(verbatim: "Study")
}
}
NavigationLink (destination: Settings()) {
HStack {
Image(systemName: "gear")
Text(verbatim: "Settings")
}
}
}
}
}
But in my flashcard view, I am getting an error: Fatal error: No ObservableObject of type UserSettings found. A View.environmentObject(_:) for UserSettings may be missing as an ancestor of this view.: file SwiftUI, line 0
The error is on line 13 where I initiate Frontside. In the original code, I just called the Frontside subview, but I thought to solve the error I had to add .environmentObject(settings), but even after adding it my app compiles but crashes as soon I go to the Flashcard view.
struct Flashcard: View {
#EnvironmentObject var settings: UserSettings
#State var colour = UserDefaults.standard.integer(forKey: "Card Theme") * 6
#State private var showResults: Bool = false
#State private var fullRotation: Bool = false
#State private var showNextCard: Bool = false
var body: some View {
let zstack = ZStack {
Frontside(id: $settings.randomNum, sheet: $showingSheet, rotate: $fullRotation, invis: $showNextCard, col: $colour).environmentObject(self.settings)
//
Backside(id: $settings.randomNum, sheet: $showingSheet, bookmark: $bookmarked, results: $showResults, rotate: $fullRotation, invis: $showNextCard, col: $colour, trans: $translation).environmentObject(self.settings)
//
}
}
Does anyone know what I'm doing wrong? This code compiled and ran fine in the previous Xcode.
I think you should pass settings object to FlashCard and Settings as well.
try this:
struct ContentView: View {
#State var settings = UserSettings()
var body: some View {
SubView().environmentObject(settings)
}
}
struct SubView: View {
#EnvironmentObject var settings: UserSettings
var body: some View {
List {
NavigationLink (destination: Flashcard().environmentObject(settings)){
HStack {
Image(systemName: "rectangle.on.rectangle.angled")
Text(verbatim: "Study")
}
}
NavigationLink (destination: Settings().environmentObject(settings)) {
HStack {
Image(systemName: "gear")
Text(verbatim: "Settings")
}
}
}
}
}
An #EnvironmentObject has to be filled with an #StateObject, an #ObservedObject or an ObservableObject directly NOT an #State
struct ContentView: View {
//#ObservedObject
#StateObject var settings = UserSettings()
var body: some View {
SubView().environmentObject(settings)
}
}
Note: UserSettings has to be an ObservableObject
Apple documentation on managing model data
struct BookReader: App {
#StateObject var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
struct LibraryView: View {
#EnvironmentObject var library: Library
// ...
}
I'm running iOS 14.3 in the simulator and in my case the error was about my environmentObject NavigationController. It was resolved by modifying ContentView() with .environmentObject(NavigationController()) in the SceneDelegate and, if you want the preview to work, also in ContentView_Previews.
import SwiftUI
#main
// there is a file with the name of your "projectApp" (JuegosSwiftUIApp in my case)
struct JuegosSwiftUIApp: App {
var body: some Scene {
WindowGroup {
DatosIniciales() // any view
.environmentObject(Datos()) // this solved it (Datos() is class type Observableobject)
}
}
}