Save array that can be used throughout the app in swiftUI - ios

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

Related

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

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.

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

How can I prevent a View model from being recreated on multiple views

DISCLAIMER: I'm a newbie in swift
I'm trying to set an MVVM app in such a way that multiple screens can access a single View Model but for some reason, everytime I navigate away from the home page, the ViewModel get re-created.
The ViewModel is set up this way:
extension ContentView {
//view model
class MyViewModel: ObservableObject {
let sdk: mySdk
#Published var allProducts = [ProductItem]()
#Published var itemsArray = [Item]() //This gets updated with content later on
...
init(sdk: mySdk) {
self.sdk = sdk
self.loadProds(forceReload: false)
...
func loadProds(forceReload: Bool){
sdk.getProducts(forceReload: forceReload) { products, error in
if let products = products {
self.allProducts = products
} else {
self.products = .error (error?.localizedDescription ?? "error")
print(error?.localizedDescription)
}
}
...
//itemsArray gets values appended to it as follows:
itemsArray.append(Item(productUid: key, quantity: Int32(value)))
}
}
}
}
The rest of the code is set up like:
struct ContentView: View { // Home Screen content
#ObservedObject var viewmodel: MyViewModel
var body: some View {
...
}
}
The SecondView that should get updated based on the state of the itemsArray is set up like so:
struct SecondView: View {
#ObservedObject var viewModel: ContentView.MyViewModel //I have also tried using #StateObject
init(sdk: mySdk) {
_viewModel = ObservedObject(wrappedValue: ContentView.MyViewModel(sdk: sdk))
}
var body: some View {
ScrollView {
LazyVStack {
Text("Items array count is \(viewModel.itemsArray.count)")
Text("All prods array count is \(viewModel.allProducts.count)")
if viewModel.itemsArray.isEmpty{
Text ("Items array is empty")
}
else {
Text ("Items array is not empty")
...
}
}
}
}
}
The Main View that holds the custom TabView and handles Navigation is set up like this:
struct MainView: View {
let sdk = mySdk(dbFactory: DbFactory())
#State private var selectedIndex = 0
let icons = [
"house",
"cart.fill",
"list.dash"
]
var body: some View{
VStack {
//Content
ZStack {
switch selectedIndex {
case 0:
NavigationView {
ContentView(viewmodel: .init(sdk: sdk))
.navigationBarTitle("Home")
}
case 1:
NavigationView {
SecondView(sdk: sdk)
.navigationBarTitle("Cart")
}
...
...
}
}
}
}
}
Everytime I navigate away from the ContentView screen, any updated content of the viewmodel gets reset. For example, on navigating the SecondView screen itemsArray.count shows 0 but allProducts Array shows the correct value as it was preloaded.
The entire content of ContentView gets recreated on navigating back as well.
I would love to have the data in the ViewModel persist on multiple views unless explicitly asked to refresh.
How can I go about doing that please? I can't seem to figure out where I'm doing something wrong.
Any help will be appreciated.
Your call to ContentView calls .init on your view model, so every time SwiftUI's rendering system needs to redraw itself, you'll get a new instance of the view model created. Similarly, the init() method on SecondView also calls the init method, in its ContentView.MyViewModel(sdk: sdk) form.
A better approach would be to create a single instance further up the hierarchy, and store it as a #StateObject so that SwiftUI knows to respond to changes to its published properties. Using #StateObject once also shows which view "owns" the object; that instance will stick around for as long as that view is in the hierarchy.
In your case, I'd create your view model in MainView – which probably means the view model definition shouldn't be namespaced within ContentView. Assuming you change the namespacing, you'd have something like
struct MainView: View {
#StateObject private var viewModel: ViewModel
init() {
let sdk = mySdk(dbFactory: DbFactory())
let viewModel = ViewModel(sdk: sdk)
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View{
VStack {
//Content
ZStack {
switch selectedIndex {
case 0:
NavigationView {
ContentView(viewModel: viewModel)
.navigationBarTitle("Home")
}
case 1:
NavigationView {
SecondView(viewModel: viewModel)
.navigationBarTitle("Cart")
}
...
...
}
}
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
// etc
}
}
struct SecondView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
// etc
}
}
One of the key things is that ObservedObject is designed to watch for changes on an object that a view itself doesn't own, so you should never be creating objects and assigning them directly to an #ObservedObject property. Instead they should receive references to objects owned by a view higher up, such as those that have been declared with a #StateObject.
First of all, let sdk = mySdk(dbFactory: DbFactory()) should be #StateObject var sdk = mySdk(dbFactory: DbFactory()).
To continue, SecondView & ContentView should have the same ViewModel, hence they should be like this:
ContentView(viewmodel: sdk)
SecondView(sdk: sdk)
Also use #StateObject instead of #ObservedObject

#EnvironmentObject with parent child classes

I am trying to create a global loader which needs to work across all loading activity in the app. Created a baseStore with loading property which holds the boolean indicator for whole app. All other stores inherit BaseStore.
When trying to update the published variable from child views its not updating header indicator. Any ideas what am I doing wrong?
class BaseStore : ObservableObject {
#Published var isLoading = false
}
class ChildStore: BaseStore {
func update(){
super.isLoading = true
}
}
somewhere in the view
import SwiftUI
struct HeaderView: View {
#EnvironmentObject var baseStore: BaseStore
var body: some View {
if(baseStore.isLoading == true){
ProgressView()
.scaleEffect(0.6, anchor: .center).progressViewStyle(ConfiguredProgressViewStyle())
}
}
}
struct HeaderView_Previews: PreviewProvider {
static var previews: some View {
return HeaderView()
.environmentObject(BaseStore());
}
}
struct MainView: View {
#EnvironmentObject var childStore: ChildStore
var body: some View {
}.onAppear(perform: {childStore.update()})
}

SwiftUI #Binding Initialize

Been playing around with SwiftUI and understood the concept of BindableObjects etc so far (at least I hope I do).
I bumped into a stupid problem I can't seem to find an answer for:
How do you initialize a #Binding variable?
I have the following code:
struct LoggedInView : View {
#Binding var dismissView: Bool
var body: some View {
VStack {
Text("Hello World")
}
}
}
In my preview code, I want to pass that parameter of type Binding<Bool>:
#if DEBUG
struct LoggedInView_Previews : PreviewProvider {
static var previews: some View {
LoggedInView(dismissView: **Binding<Bool>**)
}
}
#endif
How would I go an initialize it? tried:
Binding<Bool>.init(false)
Binding<Bool>(false)
Or even:
#Binding var dismissView: Bool = false
But none worked... any ideas?
When you use your LoggedInView in your app you do need to provide some binding, such as an #State from a previous view or an #EnvironmentObject.
For the special case of the PreviewProvider where you just need a fixed value you can use .constant(false)
E.g.
#if DEBUG
struct LoggedInView_Previews : PreviewProvider {
static var previews: some View {
LoggedInView(dismissView: .constant(false))
}
}
#endif
Using Binding.constant(false) is fine but only for static previews. If you actually wanna launch a Live Preview, constant will not behave the same way as the real case as it will never be updated by your actions. I personally use Live Preview a lot, as I can play around with an isolated view.
Here is what I do for previews requiring Binding:
import SwiftUI
struct SomeView: View {
#Binding var code: String
var body: some View {
// some views modifying code binding
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
PreviewWrapper()
}
struct PreviewWrapper: View {
#State(initialValue: "") var code: String
var body: some View {
SomeView(code: $code)
}
}
}
If you need a simple property that belongs to a single view you
should use #State
If you need to have complex property that may
belong to several view(like 2-3 views) you shall use
#ObjectBinding
Lastly, if you need to have property that needs to use all around views you shall use #EnvironmentObject.
Source for detail information
For your case, if you still would like to initialize your Binding variable you can use:
var binding: Binding = .constant(false)
In preview you have to use .constant(Bool(false)):
#if DEBUG
struct LoggedInView_Previews : PreviewProvider {
static var previews: some View {
LoggedInView(dismissView: .constant(Bool(false))
}
}
#endif
if you have an object like viewModel you can also use .constant()
struct View_Previews: PreviewProvider {
static var previews: some View {
View(vm:.constant(ViewModel(text: "Sample Text")))
}
}
I'm using different configurations of my view within one preview (I'm working on a custom control and want to see a different configuration of it). I've extended the implementation provided by #NeverwinterMoon in order to create multiple independent instances of a view.
struct SomeView: View {
#Binding var value: Int
var body: some View {
// some views modifying code binding
}
}
struct SomeView_Previews: PreviewProvider {
static var previews: some View {
VStack {
// The same view but with different configurations
// Configuration #1
PreviewWrapper() { value in
SomeView(value: value)
.background(Color.blue)
}
// Configuration #2
PreviewWrapper(initialValue: 2) { value in
SomeView(value: value)
.padding()
}
}
}
struct PreviewWrapper<Content: View>: View {
#State var value: Int
private let content: (Binding<Int>) -> Content
init(
initialValue: Int = 0,
#ViewBuilder content: #escaping (Binding<Int>) -> Content
) {
self.value = initialValue
self.content = content
}
var body: some View {
content($value)
}
}
}

Resources