How to pass by reference in Swift - number incrementing app using MVVM - ios

I've just started learning swift and was going to build this number-incrementing sample app to understand MVVM. I don't understand why is my number on the view not updating upon clicking the button.
I tried to update the view everytime user clicks the button but the count stays at zero.
The View
import SwiftUI
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The ViewModel
import SwiftUI
class CounterViewModel: ObservableObject {
#ObservedObject var model = Model()
func increment() {
self.model.count += 1
}
}
The Model
import Foundation
class Model : ObservableObject{
#Published var count = 0
}

Following should work:
import SwiftUI
struct Model {
var count = 0
}
class CounterViewModel: ObservableObject {
#Published var model = Model()
func increment() {
self.model.count += 1
}
}
struct ContentView: View {
#ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("\(viewModel.model.count)")
Button(action: {
self.viewModel.increment()
}) {
Text("Increment")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Please note:
ObservableObject and #Published are designed to work together.
Only a value, that is in an observed object gets published and so the view updated.
A distinction between model and view model is not always necessary and the terms are somewhat misleading. You can just put the count var in the ViewModel. Like:
#Published var count = 1
It makes sense to have an own model struct (or class), when fx you fetch a record from a database or via a network request, than your Model would take the complete record.
Something like:
struct Adress {
let name: String
let street: String
let place: String
let email: String
}
Please also note the advantages (and disadvantages) of having immutable structs as a model. But this is another topic.

Hi it's a bad idea to use MVVM in SwiftUI because Swift is designed to take advantage of fast value types for view data like structs whereas MVVM uses slow objects for view data which leads to the kind of consistency bugs that SwiftUI's use of value types is designed to eliminate. It's a shame so many MVVM UIKit developers (and Harvard lecturers) have tried to push their MVVM garbage onto SwiftUI instead of learning it properly. Fortunately some of them are changing their ways.
When learning SwiftUI I believe it's best to learn value semantics first (where any value change to a struct is also a change to the struct itself), then the View struct (i.e. when body is called), then #Binding, then #State. e.g. have a play around with this:
// use a config struct like this for view data to group related vars
struct ContentViewConfig {
var count = 0 {
didSet {
// could do validation here, e.g. isValid = count < 10
}
}
// include other vars that are all related, e.g. you could have searchText and searchResults.
// use mutating func for logic that affects multiple vars
mutating func increment() {
count += 1
//othervar += 1
}
}
struct ContentView: View {
#State var config = ContentViewConfig() // normally structs are immutable, but #State makes it mutable like magic, so its like have a view model object right here, but better.
var body: some View {
VStack {
ContentView2(count: config.count)
ContentView3(config: $config)
}
}
}
// when designing a View first ask yourself what data does this need to do its job?
struct ContentView2: View {
let count: Int
// body is only called if count is different from the last time this was init.
var body: some View {
Text(count, format: .number)
}
}
struct ContentView3: View {
#Binding var config: ContentViewConfig
var body: some View {
Button(action: {
config.increment()
}) {
Text("Increment")
}
}
}
}
Then once you are comfortable with view data you can move on to model data which is when ObservableObject and singletons come into play, e.g.
struct Item: Identifiable {
let id = UUID()
var text = ""
}
class MyStore: ObservableObject {
#Published var items: [Item] = []
static var shared = MyStore()
static var preview = MyStore(preview: true)
init(preview: Bool = false) {
if preview {
items = [Item(text: "Test Item")]
}
}
}
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(MyStore.shared)
}
}
}
struct ContentView: View {
#EnvironmentObject store: MyStore
var body: some View {
List($store.items) { $item in
TextField("Item", $item.text)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyStore.preview)
}
}
Note we use singletons because it would be dangerous to use #StateObject for model data because its lifetime is tied to something on screen we could accidentally lose all our model data which should have lifetime tied to the app running. Best to think of #StateObject when you need a reference type in a #State, i.e. involving view data.
When it comes to async networking use the new .task modifier and you can avoid #StateObject.

Related

SwiftUI: Communication between different ViewModels

I have a parent view which contains two child views. Each child view gets passed a different EnvironmentObject from the parent view. As a representation for all kinds of different changes, the second child view contains a Button which can be used to call a function in its ViewModel which then is supposed to change a variable in the ViewModel of the first child view.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
var body: some View {
ZStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
...
}}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
var body: some View {
Button(action: {
viewModel_2.changeValue_in_ViewModel_1(value: 1)
}, label: {
Text("Tap to change value")
})
}}
class ViewModel_1: ObservableObject {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
}
}
Is there a way to make those two ViewModels able to communicate with each other?
Thanks!
It would be solved simply by ViewModel_2 referencing ViewModel_1.
However, it is not necessary to refer to all ViewModel_1, so you can separate only the desired logic using protocol and let ViewModel_2 own it.
This is the sample code for the above explanation.
Searching for dependency injection can yield a lot of information about it.
struct ParentView: View {
#StateObject var viewModel_1: ViewModel_1
#StateObject var viewModel_2: ViewModel_2
init() {
let viewModel_1 = ViewModel_1()
_viewModel_1 = StateObject(wrappedValue: viewModel_1)
_viewModel_2 = StateObject(wrappedValue: ViewModel_2(changeValue: viewModel_1 as! ChangeValue))
}
var body: some View {
VStack {
ChildView_1()
.environmentObject(viewModel_1)
ChildView_2()
.environmentObject(viewModel_2)
}
}
}
struct ChildView_1: View {
#EnvironmentObject var viewModel_1: ViewModel_1
var body: some View {
Text("\(viewModel_1.someValue)")
}
}
struct ChildView_2: View {
#EnvironmentObject var viewModel_2: ViewModel_2
#State var count: Int = 0
var body: some View {
Button(action: {
count = count + 1
viewModel_2.changeValue_in_ViewModel_1(value: count)
}, label: {
Text("Tap to change value")
})
}
}
protocol ChangeValue {
func changeValue(value: Int)
}
class ViewModel_1: ObservableObject, ChangeValue {
#Published var someValue: Int = 0
func changeValue(value: Int) -> Void {
self.someValue = value
}
}
class ViewModel_2: ObservableObject {
private let changeValue: ChangeValue
init (changeValue: ChangeValue) {
self.changeValue = changeValue
}
func changeValue_in_ViewModel_1(value: Int) -> Void {
//something like viewModel_2.changeValue(value: value)
changeValue.changeValue(value: value)
}
}
We don't actually use view model objects in SwiftUI. The View struct is a view model already, being a value type it's more efficient and less error prone than an object but the property wrappers make it behave like an object, SwiftUI diffs the View struct and it creates/updates actual UIView/NSViews on screen for us. If you use actual view model objects you'll get bugs and face the problems that you are experiencing.
You can group related #State vars into their own struct and use mutating func for logic. That way it can be tested independently but the best thing is any chance to a property of the struct is detected by SwiftUI as a change to the whole struct which makes its dependency tracking super fast.
environmentObject is designed to hold a store object that contains the model structs (usually in arrays) in #Published properties. There isn't usually more than one environmentObject. This object is usually responsible for persisting or syncing the model data.

Updating EnvironmentObject from within the View Model

In SwiftUI, I want to pass an environment object to a view model so I can change/update it. The EnvironmentObject is a simple AppState which consists of a single property counter.
class AppState: ObservableObject {
#Published var counter: Int = 0
}
The view model "CounterViewModel" updates the environment object as shown below:
class CounterViewModel: ObservableObject {
var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
The ContentView displays the value:
struct ContentView: View {
#ObservedObject var counterVM: CounterViewModel
init(counterVM: CounterViewModel) {
self.counterVM = counterVM
}
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
I am also injecting the state as shown below:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationStack {
let appState = AppState()
ContentView(counterVM: CounterViewModel(appState: appState))
.environmentObject(appState)
}
}
}
The problem is that when I click the increment button, the counterVM.counter never returns the updated value. What am I missing?
Your class CounterViewModel is an ObservableObject, but it has no #Published properties – so no changes will be published automatically to the views.
But you can manually publish changes by using objectWillChange.send():
func increment() {
objectWillChange.send()
appState.counter += 1
}
We actually don't use view model objects in SwiftUI for view data. We use an #State struct and if we need to mutate it in a subview we pass in a binding, e.g.
struct Counter {
var counter: Int = 0
mutating func increment() {
counter += 1
}
}
struct ContentView: View {
#State var counter = Counter()
var body: some View {
ContentView2(counter: $counter)
}
}
struct ContentView2: View {
#Binding var counter: Counter // if we don't need to mutate it then just use let and body will still be called when the value changes.
var body: some View {
VStack {
Text(counter.counter, format: .number) // the formatting must be done in body so that SwiftUI will update the label automatically if the region settings change.
Button("Increment") {
counter.increment()
}
}
}
}
I'm not sure why both the CounterViewModel and the AppState need to be observable objects, since you are using a view model to format the content of your models. I would consider AppState to be a model and I could therefore define it as a struct. The CounterViewModel will then be the ObservableObject and it published the AppState. In this way your code is clean and works.
Code for AppState:
import Foundation
struct AppState {
var counter: Int = 0
}
Code for CounterViewModel:
import SwiftUI
class CounterViewModel: ObservableObject {
#Published var appState: AppState
init(appState: AppState) {
self.appState = appState
}
var counter: Int {
appState.counter
}
func increment() {
appState.counter += 1
}
}
Code for the ContentView:
import SwiftUI
struct ContentView: View {
#StateObject var counterVM = CounterViewModel(appState: AppState())
var body: some View {
VStack {
Text("\(counterVM.counter)")
Button("Increment") {
counterVM.increment()
}
}
}
}
Do remind, that in the View where you first define an ObservableObject, you define it with #StateObject. In all the views that will also use that object, you use #ObservedObject.
This code will work.
Did you check your xxxxApp.swift (used to be the AppDelegate) file ?
Sometimes Xcode would do it for you automatically, sometimes won't you have to add it manually and add your environment object. * It has to be the view that contains all the view you want to share the object to.
var body: some Scene {
WindowGroup {
VStack {
ContentView()
.environmentObject(YourViewModel())
}
}
}

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

Save array that can be used throughout the app in swiftUI

I saved some data in an array and that array should be used globally through out the app. That array can be edited or deleted. I have found we can use #EnvironmentObject and tried the following
#main
struct DineApp: App {
var body: some Scene {
WindowGroup {
View1().environmentObject(BookViewModel())
}
}
BookViewModel :
class BookViewModel : ObservableObject{
#Published var bookarray = [Book]()
func saveBook (Id: Int, Name: String){
bookarray.append(Book(Id: id, Name: name ). // How to use this bookarray through out the app
}
}
View1:
struct View1: View {
#EnvironmentObject var bookviewModel : BookViewModel
var body: some View {
//Code to save book on a button click
saveBook(Id: selectedId, Name: selectedName) //Selected books will be saved.
}
}
struct View1_Previews: PreviewProvider {
static let bookViewModel = BookViewModel()
static var previews: some View {
View1().environmentObject(bookViewModel)
}
}
View4:
struct View4: View {
#EnvironmentObject var bookviewModel : BookViewModel
var body: some View {
// Display saved books in a list. But it shows empty List
List(bookviewModel.bookarray) id:\name){row in
Text(row.name)
}
}
}
struct View4_Previews: PreviewProvider {
static let bookViewModel = BookViewModel()
static var previews: some View {
View4().environmentObject(bookViewModel)
}
}
This gives me an error that EnvironmentObject should be passed from ancestor view. I couldn't understand , where to pass and how to pass. The point is I need to access the bookarray in multiple views, so I couldn't understand whats the point of passing from one view to another.
In obj-C we have appdelegate and we can store any data in it if we want to use the data globally through out the app . what is the similar in swiftUI?
EnvironmentObject should be passed from ancestor view
means that you have to inject the object into the environment on a higher level of the view hierarchy. And the views which want to have access to the object must be descendants of this view.
The top level is the #main struct, add it there for example
#main
struct SwiftUIApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.environmentObject(BookViewModel())
}
}
View4 must be a descendant of SwiftUIApp or of ContentView

SwiftUI Dependency Injection

I have a SwiftUI app, its a tab based app
struct Tab_View: View {
var body: some View {
TabView{
Main1_View().tabItem {
Text("Blah 1")
Image("TabBar1")
}
Main2_View().tabItem {
Text("Blah 2")
Image("TabBar2")
}
}
}
}
Each view has its own view controller
struct Main1_View: View {
#ObservedObject var viewModel: Main1_ViewModel = Main1_ViewModel()
var body: some View {
VStack(spacing:0){
<<< VIEW CODE >>>
}
}
}
ViewModel Example
class Main1_ViewModel: ObservableObject {
#ObservableObject var settings: GameSettings
func Randomise(){
dataSource = settings.selectedFramework;
}
}
The class GameSettings is used by multiple viewmodels, is an ObservableObject, same instance of the class everywhere.
My background is C# and using CastleWindsor for dependency injection.
My Question: Is there a SwiftUI equivalent to pass around an instance of GameSettings?
Because of your requirement to use an ObservableObject (GameSettings) inside another ObservableObject (the view model) and use dependency injection, things are going to be a little bit convoluted.
In order to get a dependency-injected ObservableObject to a View, the normal solution is to use #EnvironmentObject. But, then you'll have to pass the object from the view to its view model. In my example, I've done that in onAppear. The side effect is that the object is an optional property on the view model (you could potentially solve this by setting a dummy initial value).
Because nested ObservableObjects don't work out-of-the box with #Published types (which work with value types, not reference types), you'll want to make sure that you use objectWillChange to pass along any changes from GameSettings to the parent view model, which I've done using Combine.
The DispatchQueue.main.asyncAfter part is there just to show that the view does in fact update when the value inside GameSettings is changed.
(Note, I've also changed your type names to use the Swift conventions of camel case)
import SwiftUI
import Combine
struct ContentView: View {
#StateObject private var settings = GameSettings()
var body: some View {
TabView {
Main1View().tabItem {
Text("Blah 1")
Image("TabBar1")
}
Main2View().tabItem {
Text("Blah 2")
Image("TabBar2")
}
}.environmentObject(settings)
}
}
struct Main1View: View {
#EnvironmentObject var settings: GameSettings
#StateObject var viewModel: Main1ViewModel = Main1ViewModel()
var body: some View {
VStack(spacing:0){
Text("Game settings: \(viewModel.settings?.myValue ?? "no value")")
}.onAppear {
viewModel.settings = settings
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
settings.myValue = "changed"
}
}
}
}
class GameSettings : ObservableObject {
#Published var myValue : String = "Test"
}
class Main1ViewModel: ObservableObject {
private var cancellable : AnyCancellable?
var settings: GameSettings? {
didSet {
print("Running init on Main1ViewModel")
self.objectWillChange.send()
cancellable = settings?.objectWillChange.sink(receiveValue: { _ in
print("Sending...")
self.objectWillChange.send()
})
}
}
}
struct Main2View : View {
var body: some View {
Text("Hello, world!")
}
}
Dependency injection, DI is the practice of providing an object with the other objects it depends on rather than creating them internally.
My Opinion
For SwiftUI you can use #EnvironmentObject. #EnvironmentObject and the View Model Factory both provide a clean solution to this.
Check this tutorial
https://mokacoding.com/blog/swiftui-dependency-injection/

Resources