Is there any way to prevent onReceive from firing when the view initially loads when the variable I'm trying to receive is a #Published property of an #EnvironmentObject?
Here's the view:
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Text(appState.test)
.padding()
}
.onAppear() {
appState.test = "World"
}
.onReceive(appState.$test) { test in
print("Hello from onReceive: \(test)")
}
}
}
Here's the environment object:
public class AppState: ObservableObject {
#Published public var test = "hello"
}
And output:
Hello from onReceive: hello
Hello from onReceive: World
I'm guessing the first one is fired when the environment object is injected. Is there any way to prevent this (other than some hackish bool that I set in the onAppear)? Ideally I'd like to only see "Hello from onReceive: World" This is just a simple example. On my actual app I am fetching data from a service in onAppear, in my environment object I have an error state that when cleared, I want it to fire the onReceive do some other logic then refetch.
It is publisher, so you can use any combine operators. In this case .dropFirst solves your task:
.onReceive(appState.$test.dropFirst()) { test in
print("Hello from onReceive: \(test)")
}
Related
I have a structure where at the end of AddTaskView_Preview: ..... there is { task in } in AddTaskView, exactly like this:
struct AddTaskView_Previews: PreviewProvider {
static var previews: some View {
AddTaskView{task in }, formVM: ToDoFormViewModel())
.environmentObject(DataStore())
}
}
This type does not allow the use of #Binding or #ObservedObject - formVM: ToDoFormViewModel() in my application, because the two values Binding and ObservedObject work for me in other views only with parentheses of type () and not { }!
What about that please? I normally have this code in my views:
ContentView(viewRouter: ViewRouter(), formVM: ToDoFormViewModel()) , but it doesn't fit inside the square brackets { } with { task in } in the above example.
How should I consume my ObservedObject in formVM: ToDoFormViewModel() with those { } ?
Can #StateObject be injected using Resolver?
I have the following:
struct FooView: View {
#StateObject private var viewModel: FooViewModel
some code
}
protocol FooViewModel: ObservableObject {
var someValue: String { get }
func someRequest()
}
class FooViewModelImpl {
some code
}
I would like to inject FooViewModel into FooView using Resolver but have been struggling as Resolver wants to use the #Inject annotation and of course, I need the #StateObject annotation but I cannot seem to use both. Are #StateObject not able to be injected using some Dependency Injection framework like Resolver? I have not found any examples where developers have used DI in this approach.
The latest version of Resolver supports #InjectedObject property wrapper for ObservableObjects. This wrapper is meant for use in SwiftUI Views and exposes bindable objects similar to that of SwiftUI #ObservedObject and #EnvironmentObject.
I am using it a lot now and its very cool feature.
eg:
class AuthService: ObservableObject {
#Published var isValidated = false
}
class LoginViewModel: ObservableObject {
#InjectedObject var authService: AuthService
}
Note: Dependent service must be of type ObservableObject. Updating object state will trigger view update.
If your StateObject has a dependency - and instead to utilise a heavy weight Dependency Injection Framework - you could utilise Swift Environment and a super light wight "Reader Monad" to setup your dependency injected state object, and basically achieve the same, just with a few lines of code.
The following approach avoids the "hack" to setup a StateObject within the body function, which may lead to unexpected behaviour of the StateObject. The dependent object will be fully initialised once and only once with a default initialiser, when the view will be created. The dependency injection happens later, when a function of the dependent object will be used:
Given a concrete dependency, say SecureStore conforming to a Protocol, say SecureStorage:
extension SecureStore: SecureStorage {}
Define the Environment Key and setup the default concrete "SecureStore":
private struct SecureStoreKey: EnvironmentKey {
static let defaultValue: SecureStorage =
SecureStore(
accessGroup: "myAccessGroup"
accessible: .whenPasscodeSetThisDeviceOnly
)
}
extension EnvironmentValues {
var secureStore: SecureStorage {
get { self[SecureStoreKey.self] }
set { self[SecureStoreKey.self] = newValue }
}
}
Elsewhere, you have a view showing some credential from the secure store, which access will be handled by the view model, which is setup as a #StateObject:
struct CredentialView: View {
#Environment(\.secureStore) private var secureStore: SecureStorage
#StateObject private var viewModel = CredentialViewModel()
#State private var username: String = "test"
#State private var password: String = "test"
var body: some View {
Form {
Section(header: Text("Credentials")) {
TextField("Username", text: $username)
.keyboardType(.default)
.autocapitalization(.none)
.disableAutocorrection(true)
SecureField("Password", text: $password)
}
Section {
Button(action: {
self.viewModel.send(.submit(
username: username,
password: password
))
.apply(e: secureStore)
}, label: {
Text("Submitt")
.frame(minWidth: 0, maxWidth: .infinity)
})
}
}
.onAppear {
self.viewModel.send(.readCredential)
.apply(e: secureStore)
}
.onReceive(self.viewModel.$viewState) { viewState in
print("onChange: new: \(viewState.credential)")
username = viewState.credential.username
password = viewState.credential.password
}
}
}
The interesting part here is where and when to perform the dependency injection:
self.viewModel.send(.submit(...))
.apply(e: secureStore) // apply the dependency
Here, the dependency "secureStore" will be injected into the view model in the action function of the Button within the body function, utilising the a "Reader", aka .apply(environment: <dependency>).
Note also that the ViewModel provides a function
send(_ Event:) -> Reader<SecureStorage, Void>
where Event just is an Enum which has cases for every possible User Intent.
final class CredentialViewModel: ObservableObject {
struct ViewState: Equatable {
var credential: Credential =
.init(username: "", password: "")
}
enum Event {
case submit(username: String, password: String)
case readCredential
case deleteCredential
case confirmAlert
}
#Published var viewState: ViewState = .init()
func send(_ event: Event) -> Reader<SecureStorage, Void>
...
Your View Model can then implement the send(_:) function as follows:
func send(_ event: Event) -> Reader<SecureStorage, Void> {
Reader { secureStore in
switch event {
case .readCredential:
...
case .submit(let username, let password):
secureStore.set(
item: Credential(
username: username,
password: password
),
key: "credential"
)
case .deleteCredential:
...
}
}
Note how the "Reader" will be setup. Basically quite easy:
A Reader just holds a function: (E) -> A, where E is the dependency and A the result of the function (here Void).
The Reader pattern may be mind boggling at first. However, just think of send(_:) returns a function (E) -> Void where E is the secure store dependency, and the function then just doing whatever was needed to do when having the dependency. In fact, the "poor man" reader would just return this function, just not a "Monad". Being a Monad opens the opportunity to compose the Reader in various cool ways.
Minimal Reader Monad:
struct Reader<E, A> {
let g: (E) -> A
init(g: #escaping (E) -> A) {
self.g = g
}
func apply(e: E) -> A {
return g(e)
}
func map<B>(f: #escaping (A) -> B) -> Reader<E, B> {
return Reader<E, B>{ e in f(self.g(e)) }
}
func flatMap<B>(f: #escaping (A) -> Reader<E, B>) -> Reader<E, B> {
return Reader<E, B>{ e in f(self.g(e)).g(e) }
}
}
For further information about the Reader Monad:
https://medium.com/#foolonhill/techniques-for-a-functional-dependency-injection-in-swift-b9a6143634ab
Not sure about resolver but you can pass VM to a V using the following approach.
import SwiftUI
class FooViewModel: ObservableObject {
#Published var counter: Int = 0
}
struct FooView: View {
#StateObject var vm: FooViewModel
var body: some View {
VStack {
Button {
vm.counter += 1
} label: {
Text("Increment")
}
}
}
}
struct ContentView: View {
var body: some View {
FooView(vm: FooViewModel())
}
}
No, #StateObject is for a separate source of truth it shouldn't have any other dependency. To pass in an object, e.g. the object that manages the lifetime of the model structs, you can use #ObservedObject or #EnvironmentObject.
You can group your related vars into their own custom struct and use mutating funcs to manipulate them. You can even use #Environment vars if you conform the struct to DynamicProperty and read them in the update func which is called on the struct before the View's body. You can even use a #StateObject if you need a reference type, e.g. to use as an NSObject's delegate.
FYI we don't use view models objects in SwiftUI. See this answer "MVVM has no place in SwiftUI."
ObservableObject is part of the Combine framework so you usually only use it when you want to assign the output of a Combine pipeline to an #Published property. Most of the time in SwiftUI and Swift you should be using value types like structs. See Choosing Between Structures and Classes. We use DynamicProperty and property wrappers like #State and #Binding to make our structs behave like objects.
I've got an app that gets a list of vehicles from a REST backend server. It then uses that list to build a list of vehicles that can be tapped to show the details about one of them:
#State private var selectedVehicle: Vehicle?
#Binding var vehicles: [Vehicle]
List {
NavigationView {
ForEach( vehicles ) { vehicle in
NavigationLink( destination: VehicleDetailScreen( vehicle: vehicle ),
tag: vehicle,
selection: self.$selectedVehicle ) {
Text( vehicle.name )
}
}
}
}
struct VehicleDetailScreen: View {
var vehicle: Vehicle
var body: some View {
// Lots of rendering code omitted
}
}
So far, so good. This works nicely. The problem arises when we fetch updated information from the server. Updating the bound vehicles property works great for updating the list. But the detail screen is still showing data that's no longer relevant.
My first thought was just to pop the detail view off of the NavigationView. Unfortunately, SwiftUI doesn't provide any reliable way that I can find to do this in a two-column view on the iPad.
My next thought was that we needed to pass the vehicle in to VehicleDetailScreen as a #Binding too so that we can update it. But this is tough to do as well because we would need a reference to that binding so that we can cram updated values into it. The only way I can think of to do that would be to rework our network and model object code entirely so that it works like CoreData, keeping objects in memory and updating them with new values from the server, rather than generating new objects. This would be a good deal of effort, and obviously isn't something I'm keen to do if there's another option.
So I'm kind of stuck on this. Any thoughts/ideas/suggestions are very welcome!
Perhaps the concept of #Binding is somewhat confusing. From a #State var (parent view), to #Binding var (child view).
A struct Hashable to facilitate and reorder the elements of the array [Vehicle].
Something like this:
struct Vehicle: Hashable {
var name:String
//var otherItem: Any
}
struct ContentView: View {
#State var vehicle: Vehicle //the struct of your REST
#State var vehicles: [Vehicle] // the array of your REST
var body: some View {
List {
NavigationView {
ForEach(vehicles, id:\.self) { item in // loop the array to get every single item conform to the struct
NavigationLink( destination: VehicleDetailScreen(vehicle: self.$vehicle)) { // here to pass the binding
Text("\(self.vehicle.name)")
}
}
}
}
}
}
//detail view
struct VehicleDetailScreen: View {
#Binding var vehicle: Vehicle // here the binding
var body: some View {
Text("\(vehicle.name)")
}
}
If you want your detail views to update when data changes, you will have to make use of bindings.
As far as architecture goes, I would suggest to create so called Stores that hold data which can be used in multiple views. This, in combination with some static provider for Stores, makes it that you can easily access and modify data anywhere, and let your views update automatically.
When using UIKit, you would manually refresh data by calling reloadTable for instance. In SwiftUI this is not done. You could hypothetically manually trigger the view to update, but I would advice against this, as it is not the way SwiftUI was intended.
I've modified your code to show an example of this:
class StoreProvider {
static let carStore = CarStore()
}
class CarStore: ObservableObject {
#Published var vehicles: [Vehicle] = [Vehicle(id: "car01", name: "Porsche", year: 2016), Vehicle(id: "car02", name: "Lamborghini", year: 2002)]
}
struct Vehicle: Identifiable, Hashable {
let id: String
var name: String
var year: Int
}
struct CarOverview: View {
#ObservedObject var store = StoreProvider.carStore
#State var selectedVehicle: Vehicle?
var body: some View {
NavigationView {
List {
ForEach(store.vehicles.indices) { vehicleIndex in
NavigationLink(destination: VehicleDetailScreen(vehicle: self.$store.vehicles[vehicleIndex])) {
Text(self.store.vehicles[vehicleIndex].name)
}.onTapGesture {
self.selectedVehicle = self.store.vehicles[vehicleIndex]
}
}
}
}
}
}
struct VehicleDetailScreen: View {
#Binding var vehicle: Vehicle
func updateValues() {
vehicle.year = Int.random(in: 1990..<2020)
}
var body: some View {
VStack {
Text(vehicle.name)
Text("Year: ") + Text(vehicle.year.description)
}.onTapGesture(perform: updateValues)
}
}
I'm trying to remove the logic from the view, while keeping the benefits of SwiftUI. Idea 1 works but it makes use of an extra variable than I would want to. Idea 2 gives error: Property wrappers are not yet supported on local properties. The view should return "bar". What is the best way of making this work? Many thanks.
import Combine
import Foundation
import SwiftUI
// Model
enum Model: String, RawRepresentable {
case foo = "foo"
case bar = "bar"
}
// State
var data1: String = Model.foo.rawValue
class State: ObservableObject {
#Published internal var data2: String = data1
}
// Logic
func logic() {
// Idea 1: OK
//data1 = Model.bar.rawValue
//print(State().data2)
// Idea 2: Error Property wrappers are not yet supported on local properties
#EnvironmentObject private var state: State
state.data2 = Model.bar.rawValue
print(state.data2)
}
// View
struct bar: View {
#EnvironmentObject private var state: State
internal var body: some View {
logic()
return Text(verbatim: self.state.data2)
}
}
If you want a function to have access to a view's state, pass the state:
func logic(state: State) {
state.data2 = Model.bar.rawValue
print(state.data2)
}
But what you've done here is an infinite loop. Modifying a view's state causes the view to be re-rendered. So every time the view is rendered, it modifies its state and forces it to be rendered again. That will never resolve. What you may mean here is to change the state when the view first appears, in which case you'd call logic this way:
struct Bar: View {
#EnvironmentObject private var state: State
internal var body: some View {
Text(verbatim: state.data2)
.onAppear{ logic(state: self.state) }
}
}
I am having a little trouble here: I have a class
class TempC {
func GetData(){
//do stuff
}
}
And in ContentView I want to call the function, but I can't manage to do it, I am getting errors...
struct ContentView: View {
var variable : TempC
variable.GetData()
var body: some View {
Text("Hello World")
}
}
Or in any other method. How can I call an external function now?
PS: The error that I get are on the line with variable.GetData() which are:
Consecutive declarations on a line must be separated by ";"
Expected "("in argument list of cantons declaration
Expected "{"in body of function declaration
Expected 'func' keyword in instance method declaration
Invalid redeclaration of 'variable()'
It's like it is expecting to create a new function not to get the one that is already existing.
Depending on what your going to do in that call there are options, ex:
Option 1
struct ContentView: View {
let variable = TempC()
init() {
variable.GetData()
}
var body: some View {
Text("Hello World")
}
}
Option 2
struct ContentView: View {
let variable = TempC()
var body: some View {
Text("Hello World")
.onAppear {
self.variable.GetData()
}
}
}
Similarly you can call it in .onTapGesture or any other, pass reference to your class instance during initialising, etc.