Can i pass a Bool as an environment object to subViews in SwiftUI? - swift

I have a bool
#State var isDragging: Bool
How can I pass this as an environment object to subViews?

You need to create an ObservableObject:
class Model: ObservableObject {
#Published var isDragging: Bool = false
}
And then use:
struct MyView: View {
#EnvironmentObject var mymodel: Model
var body : some View {
if mymodel.isDragging { ... }
}
}
And also, you should watch to WWDC 2019 session "Data Flow in Swift". Although some of the type names have been changed since, the concepts remain the same.

Related

SwiftUI how to use enviromentObject in viewModel init?

I have a base API:
class API: ObservableObject {
#Published private(set) var isAccessTokenValid = false
#AppStorage("AccessToken") var accessToken: String = ""
#AppStorage("RefreshToken") var refreshToken: String = ""
func request1() {}
func request2() {}
}
And it was passed to all views by using .environmentObject(API()). So in any views can easily access the API to do the http request calls.
Also I have a view model to fetch some data on the view appears:
class ViewModel: ObservableObject {
#Published var data: [SomeResponseType]
init() {
// do the request and then init data using the response
}
}
struct ViewA: View {
#StateObject private var model = ViewModel()
var body: View {
VStack {
model.data...
}
}
}
But in the init(), the API is not accessable in the ViewModel.
So, to solve this problem, I found 3 solutions:
Solution 1: Change API to Singleton:
class API: ObservableObject {
static let shared = API()
...
}
Also we should change the enviromentObject(API()) to enviromentObject(API.shared).
So in the ViewModel, it can use API.shared directly.
Solution 2: Call the request on the onAppear/task
struct ViewA: View {
#EnvironmentObject var api: API
#State private var data: [SomeResponseType] = []
var body: View {
VStack {}
.task {
let r = try? await api.request1()
if let d = r {
data = d
}
}
}
}
Solution 3: Setup the API to the ViewModel onAppear/task
class ViewModel: ObservableObject {
#Published var data: [SomeResponseType]
var api: API?
setup(api: API) { self.api = api }
requestCall() { self.api?.reqeust1() }
}
struct ViewA: View {
#EnvironmentObject var api: API
#StateObject private var model = ViewModel()
var body: View {
VStack {}
.onAppear {
model.setup(api)
model.requestCall()
}
}
}
Even though, I still think they are not a SwiftUI way. And my questions is a little XY problem. Probably, the root question is how to refactor my API. But I am new to SwiftUI.
Solution 2 is best. You can also try/catch the exception and set an #State for an error message.
Try to avoid using UIKit style view model objects because in SwiftUI the View data struct plus #State already fulfils that role. You only need #StateObject when you need a reference type for view data which is not very often given now we have .task.

Data communication between 2 ObservableObjects

I have 2 independent ObservableObjects called ViewModel1 and ViewModel2.
ViewModel2 has an array of strings:
#Published var strings: [String] = [].
Whenever that array is modified i want ViewModel1 to be informed.
What's the recommended approach to achieve this?
Clearly, there are a number of potential solutions to this, like the aforementioned NotificationCenter and singleton ideas.
To me, this seems like a scenario where Combine would be rather useful:
import SwiftUI
import Combine
class ViewModel1 : ObservableObject {
var cancellable : AnyCancellable?
func connect(_ publisher: AnyPublisher<[String],Never>) {
cancellable = publisher.sink(receiveValue: { (newStrings) in
print(newStrings)
})
}
}
class ViewModel2 : ObservableObject {
#Published var strings: [String] = []
}
struct ContentView : View {
#ObservedObject private var vm1 = ViewModel1()
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
VStack {
Button("add item") {
vm2.strings.append("\(UUID().uuidString)")
}
ChildView(connect: vm1.connect)
}.onAppear {
vm1.connect(vm2.$strings.eraseToAnyPublisher())
}
}
}
struct ChildView : View {
var connect : (AnyPublisher<[String],Never>) -> Void
#ObservedObject private var vm2 = ViewModel2()
var body: some View {
Button("Connect child publisher") {
connect(vm2.$strings.eraseToAnyPublisher())
vm2.strings = ["Other strings","From child view"]
}
}
}
To test this, first try pressing the "add item" button -- you'll see in the console that ViewModel1 receives the new values.
Then, try the Connect child publisher button -- now, the initial connection is cancelled and a new one is made to the child's iteration of ViewModel2.
In order for this scenario to work, you always have to have a reference to ViewModel1 and ViewModel2, or at the least, the connect method, as I demonstrated in ChildView. You could easily pass this via dependency injection or even through an EnvironmentObject
ViewModel1 could also be changed to instead of having 1 connection, having many by making cancellable a Set<AnyCancellable> and adding a connection each time if you needed a one->many scenario.
Using AnyPublisher decouples the idea of having a specific types for either side of the equation, so it would be just as easy to connect ViewModel4 to ViewModel1, etc.
I had same problem and I found this method working well, just using the idea of reference type and taking advantage of class like using shared one!
import SwiftUI
struct ContentView: View {
#StateObject var viewModel2: ViewModel2 = ViewModel2.shared
#State var index: Int = Int()
var body: some View {
Button("update strings array of ViewModel2") {
viewModel2.strings.append("Hello" + index.description)
index += 1
}
}
}
class ViewModel1: ObservableObject {
static let shared: ViewModel1 = ViewModel1()
#Published var onReceiveViewModel2: Bool = Bool() {
didSet {
print("strings array of ViewModel2 got an update!")
print("new update is:", ViewModel2.shared.strings)
}
}
}
class ViewModel2: ObservableObject {
static let shared: ViewModel2 = ViewModel2()
#Published var strings: [String] = [String]() {
didSet { ViewModel1.shared.onReceiveViewModel2.toggle() }
}
}

Store SwiftUI functions globally

I use ObservableObject to store a #Published property but how can I store a global function that can be used in any view? Can this function include in itself a call of a #Published property?
These are some possibilities:
class ViewModel: ObservableObject {
// declare as a property
let func1: () -> Void = {
print("func1")
}
// declare as a `func`
func func2() {
print("func2")
}
}
struct ContentView: View {
#ObservedObject var vm = ViewModel()
var body: some View {
Text("asd")
.onAppear(perform: vm.func1)
// .onAppear(perform: vm.func2)
}
}
You can use an #EnvironmentObject to make them available globally.

Changing a property of my ObservableObject instance doesn't update View. Code:

//in ContentView.swift:
struct ContentView: View {
#EnvironmentObject var app_state_instance : AppState //this is set to a default of false.
var body: some View {
if self.app_state_instance.complete == true {
Image("AppIcon")
}}
public class AppState : ObservableObject {
#Published var inProgress: Bool = false
#Published var complete : Bool = false
public static var app_state_instance: AppState? //I init this instance in SceneDelegate & reference it throughout the app via AppState.app_state_instance
...
}
in AnotherFile.swift:
AppState.app_state_instance?.complete = true
in SceneDelegate.swift:
app_state_instance = AppState( UserDefaults.standard.string(forKey: "username"), UserDefaults.standard.string(forKey: "password") )
AppState.app_state_instance = app_state_instance
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(app_state_instance!))
This ^ does not trigger an update of my view.Anyone know why?
Also, is it possible to make a static var # published?
Also, I'm kind somewhat new to class-based + structs-as-values paradigm, if you have any tips on improving this please do share.
I've considered using an 'inout' ContentView struct reference to update struct state from other files (as going through app_state_instance isn't working with this code).

How to pass a value from an EnvironmentObject to a class instance in SwiftUI?

I'm trying to assign the value from an EnvironmentObject called userSettings to a class instance called categoryData, I get an error when trying to assign the value to the class here ObserverCategory(userID: self.userSettings.id)
Error says:
Cannot use instance member 'userSettings' within property initializer; property initializers run before 'self' is available
Here's my code:
This is my class for the environment object:
//user settings
final class UserSettings: ObservableObject {
#Published var name : String = String()
#Published var id : String = "12345"
}
And next is the code where I'm trying to assign its values:
//user settings
#EnvironmentObject var userSettings: UserSettings
//instance of observer object
#ObservedObject var categoryData = ObserverCategory(userID: userSettings.id)
class ObserverCategory : ObservableObject {
let userID : String
init(userID: String) {
let db = Firestore.firestore().collection("users/\(userID)/categories") //
db.addSnapshotListener { (snap, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}
for doc in snap!.documentChanges {
//code
}
}
}
}
Can somebody guide me to solve this error?
Thanks
Because the #EnvironmentObject and #ObservedObject are initializing at the same time. So you cant use one of them as an argument for another one.
You can make the ObservedObject more lazy. So you can associate it the EnvironmentObject when it's available. for example:
struct CategoryView: View {
//instance of observer object
#ObservedObject var categoryData: ObserverCategory
var body: some View { ,,, }
}
Then pass it like:
struct ContentView: View {
//user settings
#EnvironmentObject var userSettings: UserSettings
var body: some View {
CategoryView(categoryData: ObserverCategory(userID: userSettings.id))
}
}