How to share published model between two view models in SwiftUI? - ios

I am trying to access the same shared model within two different view models. Both associated views need to access the model within the view model and need to edit the model. So I can't just use the EnvironmentObject to access the model.
I could pass the model to the view model through the view, but this wouldn't keep both model versions in sync. Is there something that could work like binding? Because with binding I can access the model but then it won't publish the changes in this view.
Simplified Example:
First view in NavigationView with adjacent view two:
struct ContentView1: View {
#StateObject var contentView1Model = ContentView1Model()
var body: some View {
NavigationView {
VStack{
TextField("ModelName", text: $contentView1Model.model.name)
NavigationLink(destination: ContentView2(model: contentView1Model.model)){
Text("ToContentView2")
}
}
}
}
}
class ContentView1Model: ObservableObject {
#Published var model = Model()
//Some methods that modify the model
}
Adjacent view 2 which needs access to Model:
struct ContentView2: View {
#StateObject var contentView2Model: ContentView2Model
init(model: Model) {
self._contentView2Model = StateObject(wrappedValue: ContentView2Model(model: model))
}
var body: some View {
TextField("ModelName", text: $contentView2Model.model.name)
}
}
class ContentView2Model: ObservableObject {
#Published var model: Model // Tried binding but this won't publish the changes.
init(model: Model) {
self.model = model
}
}
Model:
struct Model {
var name = ""
}
Thanks for the help!

Ok, Model is struct, so it is just copied when you pass it from ContentViewModeltoContentView2Model` via
ContentView2(model: contentView1Model.model)
This is the case when it is more preferable to have model as standalone ObservableObject, so it will be passed by reference from one view model into another.
class Model: ObservableObject {
#Published var name = ""
}
and then you can inject it and modify in any needed subview, like
struct ContentView1: View {
#StateObject var contentView1Model = ContentView1Model()
var body: some View {
NavigationView {
VStack{
ModelEditView(model: contentView1Model.model) // << !!
NavigationLink(destination: ContentView2(model: contentView1Model.model)){
Text("ToContentView2")
}
}
}
}
}
struct ContentView2: View {
#StateObject var contentView2Model: ContentView2Model
init(model: Model) {
self._contentView2Model = StateObject(wrappedValue: ContentView2Model(model: model))
}
var body: some View {
ModelEditView(model: contentView2Model.model) // << !!
}
}
struct ModelEditView: View {
#ObservedObject var model: Model
var body: some View {
TextField("ModelName", text: $model.name)
}
}

Related

How to access property in viewmodel to view in SwiftUI?

I have viewmodel and view i have return some API logic in viewmodel and getting one dictionary after some logic..i want to access that dictionary value from view for ex.
viewmodel.someDic
but for now every-time i am getting empty dic.
class Viewmodel: ObservableObject {
#Published private var poductDetails:[String:ProductDetail] = [:]
func createItems(data: ProductRootClass) {
var productDetils = [String: SelectedProductDetail](){
//some logic
productDetils // with some object
self.poductDetails = productDetils
}
}
}
struct View: View {
#StateObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .leading) {
Text("\(viewModel.poductDetails)")
}
.onAppear(perform: {
print("\(viewModel.poductDetails)")
})
}
}
I want to access this dictionary from view.
I tried accessing by returning productDetils from any function but get empty everytime.
may i know the way to access property from viewmodel to view?
You need a class conforming to ObservableObject and a property marked as #Published
class ViewModel : ObservableObject {
#Published var productDetails = [String:ProductDetail]()
func createItems(data: ProductRootClass) {
var productDetils = [String: SelectedProductDetail]()
//some logic
productDetils // with some object
self.productDetails = productDetils
}
}
Whenever the property is modified the view will be updated.
In the view create an instance and declare it as #StateObject
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack(alignment: .leading) {
ForEach(viewModel.productDetails.keys.sorted(), id: \.self) { key in
Text("\(viewModel.poductDetails[key]["someOtherKey"] as! String)")
}
}
}
}
I would prefer an array as model, it's easier to access
You need to get rid of the view model and make a proper model.
class Model: ObservableObject {
#Published private var productDetails:[ProductDetail] = []
}
struct ProductDetail: Identifiable {
let id = UUID()
var title: String
}
Now you can do ForEach(model.productDetails)
You can make your viewModel as a Singleton.
class ViewModel : ObservableObject {
static let viewModelSingleton = ViewModel()
#Published var productDetails = [String : ProductDetail]()
\\API logic}
Access this viewModel Singleton in the view
struct view : View {
#ObservedObject var viewModelObject = ViewModel.viewModelSingleton
var body: some View {
VStack(alignment: .leading) {
Text("\(viewModelObject.productDetails)")
}
}}

MVVM Passing data from view to another view's viewModel

I'm new to MVVM and i am trying to pass a location data from my ContenView to DetailsView's viewModel which is DetailsViewViewModel.
My Opening View -> ContentView (My data is here)
Second View -> DetailsView
Data must be reach -> DetailsViewViewModel
Here is my sheet in ContentView
.sheet(item: $viewModel.selectedPlace) { place in
DetailsView(location: place) { newLocation in
viewModel.updateLocation(location: newLocation)
}
I know i'm trying to send my data to details view and it's wrong. It was like that before i convert the architecture to the MVVM and this is the only place that i couldn't convert.
Also here is my DetailsViewViewModel
extension DetailsView {
#MainActor class DetailsViewViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, failed
}
var location: Location
#Published var name: String
#Published var description: String
#Published var loadingState = LoadingState.loading
#Published var pages = [Page]()
init() {
self.location = // ??? how should i initialize?
self.name = location.name
self.description = location.description
}
What is the proper way to this. Using another views data in another views viewmodel.
Let me try to put in an example that uses the convenience of #EnvironmentObject:
Your view model is a class that conforms to ObservableObject, so you can have those nice variables #Published that change the state of the views.
Your main view - or also your App - must "own" the view model, meaning it needs to create the one and only instance of your view model that will be used by all views.
You pass the view model from one view to another using #StateObject and #ObservableObject, but in this example I prefer to use another approach. Make your main view inject the instance of your view model in the environment, so all other views will read from that. The main view uses .environmentObject(viewModel) to do that.
The other views read the view model from the environment by calling #EnvironmentObject. They create a variable specifying only the type - there can only be one instance per type in the environment.
This is the way with which all view will read from the same model. See below a functioning example:
Step 1:
class MyViewModel: ObservableObject {
#Published private(set) var color: Color = .blue
#Published var showSheet = false
func changeColorTo(_ color: Color) {
self.color = color
}
}
Steps 2 and 3:
struct Example: View {
#StateObject private var viewModel = MyViewModel() // Here is the step (2)
var body: some View {
OneView()
.environmentObject(viewModel) // Here is the step (3)
}
}
Step 4 in two different views:
struct OneView: View {
#EnvironmentObject var viewModel: MyViewModel // Here is step (4)
var body: some View {
VStack {
Text("Current color")
.padding()
.background(viewModel.color)
Button {
if viewModel.color == .blue {
viewModel.changeColorTo(.yellow)
} else {
viewModel.changeColorTo(.blue)
}
} label: {
Text("Change color")
}
Button {
viewModel.showSheet.toggle()
} label: {
Text("Now, show a sheet")
}
.padding()
}
.sheet(isPresented: $viewModel.showSheet) {
DetailView()
}
}
}
struct DetailView: View {
#EnvironmentObject var viewModel: MyViewModel // Here is step (4)
var body: some View {
VStack {
Text("The sheet is showing")
.padding()
Button {
viewModel.showSheet.toggle()
} label: {
Text("Now, stop showing the sheet")
}
}
}
}
since location data is your business layer data, you need a use-case to provide it to both view models, and to optimize it caching the response is the way to go.
-ViewModel is responsible to hold the latest view states and data
-The domain layer is responsible to handle business logic
-The data layer (networking, cache, persistence, or in-memory) is responsible for providing the most efficient data storage/retrieval solutions
So, if you are okay with these defenitions and think of writing test for these view models you know that it is not right to inject data from another ViewModel because you would not test that view model on making sure it passes the data to the next viewModel and it is not its responsibility, but you write many tests for you data layer to make sure service calls and caching systems are working properly.
#StateObject var viewModel = ViewModel()
struct ParentView: View {
var body: some View {
Button(action: {
}, label: {
Text("btn")
})
.sheet(item: $viewModel.selectedPlace) { place in
DetailView(name: place.name,
location: place.location,
description: place.description)
}
}
}
struct DetailView: View {
var name: String
var location: String
var description: String
var body: some View {
VStack {
Text(name)
Text(location)
Text(description)
}
}
}
You need to initialise DetailsViewModel from ContentView sheet when you are adding the DetailsView like below:
ContentView
struct ContentView: View {
#StateObject var vm = ViewModel()
var body: some View {
Text("Hello, world!")
.sheet(item: $vm.selectedPlace,
onDismiss: didDismiss) {newLocation in
//Here Initialise the DetailViewModel with a location
DetailsView(detailsVM: DetailsViewModel(location: newLocation))
}
}
func didDismiss(){
}
}
DetailsView:
struct DetailsView: View {
#StateObject var detailsVM : DetailsViewModel
var body: some View {
Text("This is the DetailesView")
}
}
DetailsViewModel:
class DetailsViewModel:ObservableObject{
#Published var location:Location
init(location:Location){
self.location = location
}
}

Trying to Update Previous View on Form Submission SwiftUI

I am working on a project where users will be able to enter their stock trades (lots) and later view some statistics. My problem is that when going from the list view to a detail edit view and pressing save, the list view doesn't get notified of this change (neither does the home view)
I could always try to pass the previous view models down to the edit view to invalidate them but this seems like a hack to me, so I am wondering if I am missing some important piece of SwiftUI.
Here is the code for the List View
struct StockView: View {
#ObservedObject var vm: StockViewModel
init(_ symbol: String) {
vm = StockViewModel(symbol)
}
var body: some View {
List {
ForEach(vm.lots, id: \.id) { lot in
NavigationLink(destination: LotEditView(lot.id)) {
...
}
}
}.navigationTitle("My Lots")
.listStyle(PlainListStyle())
}
}
and the code for the Edit View
struct LotEditView: View {
#Environment(\.presentationMode) var mode
#ObservedObject var vm: LotEditViewModel
init(_ id: UUID) {
vm = LotEditViewModel(id)
}
var body: some View {
VStack {
Form {
...
}
}.navigationTitle("Edit Lot")
.toolbar {
Button("Done") {
vm.save()
mode.wrappedValue.dismiss()
}
}
}
}
The List ViewModel is initialized with a stock name and then uses that to get the lots from a mock database, then the id of each of these lots is passed to the Edit ViewModel. The vm.save() simply updates the database which in this case is just an array which I've confirmed is being updated.
So what you want to do is to have one instance of that Mock database and inject it into viewModels.
I think the easiest way here would be to make this mock database as a #StateObject. Create it in the inital view(might be homeView or even Appdelegate) and pass it on as environmentObject to other views.
struct StockView: View {
#ObservedObject var vm: StockViewModel
#StateObject var dbService = MyMockDatabase()
init(_ symbol: String) {
vm = StockViewModel(dbService: dbService, symbol: symbol)
}
var body: some View {
List {
ForEach(vm.lots, id: \.id) { lot in
NavigationLink(destination: LotEditView(lot.id).environmentObject(dbService)) {
...
}
}
}.navigationTitle("My Lots")
.listStyle(PlainListStyle())
}
}
And in your next screen use it as:
struct LotEditView: View {
#Environment(\.presentationMode) var mode
#EnvironmentObject var dbService: MyMockDatabase
#ObservedObject var vm: LotEditViewModel
init(_ id: UUID) {
vm = LotEditViewModel(dbService: dbService, id: id)
}
var body: some View {
VStack {
Form {
...
}
}.navigationTitle("Edit Lot")
.toolbar {
Button("Done") {
vm.save()
mode.wrappedValue.dismiss()
}
}
}
}
MockArray would look something like:
class MyMockDatabase: ObservableObject {
#Published var array = [...]
}

SwiftUI / Combine Pass data between two models

I have question regarding how to pass data between two models.
struct SettingsCell: View {
#State var isOn: Bool
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject var model = SettingsModel()
var body: some View {
List {
SettingsCell(isOn: model.someValue)
}
}
}
So i want to pass isOn state from cell, to main model, and react there. Send requests for example.
You need to declare isOn as #Binding in SettingsCell.
#State should only be used for properties initialised inside the View itself and must always be private. If you want to pass in a value that should update the View whenever it changes, but the value is created outside the View, you need to use Binding.
Another really important thing to note is that #ObservedObjects must always be injected into Views, you must not initialise them inside the view itself. This is because whenever an #ObservedObject is updated, it updates the view itself, so if you initialised the object inside the view, whenever the object updates the view, the view would create a new #ObservedObject and hence your changes wouldn't be persisted from the view to the model.
If you are targeting iOS 14 and want to create the model inside the view, you can use #StateObject instead.
struct SettingsCell: View {
#Binding private var isOn: Bool
init(isOn: Binding<Bool>) {
self._isOn = isOn
}
var body: some View {
Toggle(name, isOn: $isOn)
}
}
class SettingsModel: ObservableObject {
#Published var someValue: Bool = false
}
struct SettingsView: View {
#ObservedObject private var model: SettingsModel
init(model: SettingsModel) {
self.model = model
}
var body: some View {
List {
SettingsCell(isOn: $model.someValue)
}
}
}
Binding is used in cases where the data is "owned" by a parent view - i.e. the parent holds the source of truth - and needs the child view to update it:
struct SettingsCell: View {
#Binding var isOn: Bool // change to Binding
var body: some View {
Toggle(name, isOn: $isOn)
}
}
struct SettingsView: View {
// unrelated, but better to use StateObject
#StateObject var model = SettingsModel()
var body: some View {
List {
// pass the binding by prefixing with $
SettingsCell(isOn: $model.someValue)
}
}
}

SwiftUI MVVM: child view model re-initialized when parent view updated

I'm attempting to use MVVM in a SwiftUI app, however it appears that view models for child views (e.g. ones in a NavigationLink) are re-initialized whenever an ObservableObject that's observed by both the parent and child is updated. This causes the child's local state to be reset, network data to be reloaded, etc.
I'm guessing it's because this causes parent's body to be reevaluated, which contains a constructor to SubView's view model, but I haven't been able to find an alternative that lets me create view models that don't live beyond the life of the view. I need to be able to pass data to the child view model from the parent.
Here's a very simplified playground of what we're trying to accomplish, where incrementing EnvCounter.counter resets SubView.counter.
import SwiftUI
import PlaygroundSupport
class EnvCounter: ObservableObject {
#Published var counter = 0
}
struct ContentView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: .init())
}
.environmentObject(envCounter)
}
}
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Sub view")
Button(action: { self.viewModel.counter += 1 }) {
Text("SubView counter is at \(self.viewModel.counter)")
}
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
A new property wrapper is added to SwiftUI in Xcode 12, #StateObject. You should be able to fix it by simply changing #ObservedObject for #StateObject as follows.
struct SubView: View {
class ViewModel: ObservableObject {
#Published var counter = 0
}
#EnvironmentObject var envCounter: EnvCounter
#StateObject var viewModel: ViewModel // change on this line
var body: some View {
// ...
}
}
To solve this problem I created a custom helper class called ViewModelProvider.
The provider takes a hash for your view, and a method that builds the ViewModel. It then either returns the ViewModel, or builds it if its the first time that it received that hash.
As long as you make sure the hash stays the same as long as you want the same ViewModel, this solves the problem.
class ViewModelProvider {
private static var viewModelStore = [String:Any]()
static func makeViewModel<VM>(forHash hash: String, usingBuilder builder: () -> VM) -> VM {
if let vm = viewModelStore[hash] as? VM {
return vm
} else {
let vm = builder()
viewModelStore[hash] = vm
return vm
}
}
}
Then in your View, you can use the ViewModel:
Struct MyView: View {
#ObservedObject var viewModel: MyViewModel
public init(thisParameterDoesntChangeVM: String, thisParameterChangesVM: String) {
self.viewModel = ViewModelProvider.makeViewModel(forHash: thisParameterChangesVM) {
MOFOnboardingFlowViewModel(
pages: pages,
baseStyleConfig: style,
buttonConfig: buttonConfig,
onFinish: onFinish
)
}
}
}
In this example, there are two parameters. Only thisParameterChangesVM is used in the hash. This means that even if thisParameterDoesntChangeVM changes and the View is rebuilt, the view model stays the same.
I was having the same problem, your guesses are right, SwiftUI computes all your parent body every time its state changes. The solution is moving the child ViewModel init to the parent's ViewModel, this is the code from your example:
class EnvCounter: ObservableObject {
#Published var counter = 0
#Published var subViewViewModel = SubView.ViewModel.init()
}
struct CounterView: View {
#ObservedObject var envCounter = EnvCounter()
var body: some View {
VStack {
Text("Parent view")
Button(action: { self.envCounter.counter += 1 }) {
Text("EnvCounter is at \(self.envCounter.counter)")
}
.padding(.bottom, 40)
SubView(viewModel: envCounter.subViewViewModel)
}
.environmentObject(envCounter)
}
}

Resources