Reading EnvironmentObject<T> outside View.body - swift

I constructed a BindableObject tied to my custom view.
I wanna update the array of items in this class
// in my vm:
class ViewModel {
func fetched() {
listView.listData.items = viewModel.listItems
}
}
final class ListData: BindableObject {
var didChange = PassthroughSubject<ListData, Never>()
var items: [ListItem] = [] {
didSet {
didChange.send(self)
}
}
}
I have a viewModel where I do my fetching because I would like to not have to inject a service into this ListData class. This is because I don't wan't it do any fetching I wanna keep that in my view model.
But when I try to update the ListData.Items in my VM I get this error:
Reading EnvironmentObject outside View.body: file
Are you only allowed to update this variable inside the body of this new view class? In that case are you suppose to do all your network stuff inside a BindableObject?
Haven't found any documentation on the best practice of this yet.

Related

Is there a way to get rid of a new concurrency warning on a #Published variable

I'm currently working on a new iOS project.
To be up to date with the new concurrency from Swift I have switched the Strict concurrency checking setting too "complete". This has of course sprung up lot's of warning and error that I was mostly able to fix.
Never the less there are a few cases that I can't figure out.
My project structure is a very commun MVVM pattern in Swift/SwiftUI.
Where my viewModels implement the ObservableObject and Sendable protocols.
They contain #Published variables that are update internally to refresh my associated SwiftUI views.
My problem is that I have concurrency warnings on these #Published variables and I can't figure out how to get rid of them except settings my viewmodels as #unchecked Sendable, which is not really recommended.
Following is an example of a viewModel and it's associated warning:
final class HomeViewModel: ObservableObject, Sendable {
// The warning concerns this variable
#Published private(set) var pageState: PageState<HomePageModel> = .loading
private var reloadTask: Task<Void, Never>?
init() {
setUp()
}
deinit {
reloadTask?.cancel()
}
func fetchHomeContent() async {
if Task.isCancelled { return }
do {
XXXXX async execution happening here
try Task.checkCancellation()
let sortedSections = sections.sorted { $0.order < $1.order }
await updatePageStage(with: .loaded(HomePageModel(sections: sortedSections)))
} catch {
await updatePageStage(with: .error(error.localizedDescription))
}
}
func reloadData() {
reloadTask?.cancel()
reloadTask = Task { [weak self] in
await self?.fetchHomeContent()
}
}
}
private extension HomeViewModel {
#MainActor
func updatePageStage(with state: PageState<HomePageModel>) {
pageState = state
}
}
Warning
Stored property '_pageState' of 'Sendable'-conforming class 'HomeViewModel' is mutable
As you can see the warning is quite clear but I cannot seem to find a way to make it disappear.
nonisolated is not supported on properties with property wrappers so that doesn't work.
Having a private(set) also.
As you can see the function updatePageStage is #MainActor bounded and my HomeViewModel is a #StateObject making it also #MainActor bounded through Swift implementation.
My question is simple: is there a way to remove this warning without using tricks like #unchecked Sendable or setting explicitly the entire class as #MainActor
Thank you in advance for any tips.
When you use async/await you no longer need an object, it's simply:
struct HomeView {
#State var results = []
#State var error = ""
var body: some View {
.task {
do {
results = try await fetchHomeContent()
} catch {
error =
}
}
}
The task is started when the underlying UIView appears and cancelled when it disappears. This simplifies async lifecycle management greatly. Also you can use task(id: pageNumber) to cancel and restart the task when the id param changes.

Inject a StateObject into SwiftUI View

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.

SwiftUI - ObservableObject doesn't cause update

I have a class called SocialMealplan that looks like the following:
public class SocialMealplan : Identifiable, ObservableObject {
public var id : String {
return owner.id
}
#Published public var owner : YKUser
#Published public var mealplan : Mealplan
init(owner : YKUser, mealplan : Mealplan) {
self.owner = owner
self.mealplan = mealplan
}
}
I then have the following code:
struct MealPlanView: View {
#ObservedObject var currentMealplan: SocialMealplan = SocialMealplan(owner: YKUser.none, mealplan: Mealplan.none)
var body: some View {
/* ... */
ForEach(self.currentMealplan.mealplan.meals, id: \.self) { (meal) in
VStack {
NavigationLink(destination: SelectRecipeView(completion: self.updatedMealplan, date: meal.date)) {
MealplanRow(meal: .constant(meal))
}
}
}.onAppear {
self.refreshMealplan()
}
/* ... */
}
func refreshMealplan() {
// Get the mealplan from the server
self.currentMealplan.mealplan = newMealplan
}
}
The problem is that when I run this code it gets the mealplan, but when it tries to assign the variable nothing happens. refreshMealplan is called and the variable is assigned, but nothing changes on the UI and the view doesn't refresh to reflect the new data.
(Neither does anything happen when I reassign the owner variable)
A new SocialMealPlan object currentMealPlan is being created and initialized every time the view needs to be redrawn/recreated. So one object triggers the update (by assignment to one of the Published vars), but the new updated view refers to its own new freshly initialized copy of currentMealPlan.
Option 1: Make currentMealPlan a #StateObject (so one copy representing a state is kept and referred to). ie #StateObject var currentMealplan = SocialMealplan(owner: YKUser.none, mealplan: Mealplan.none)
Option 2: Or keep the #ObservedObject, but create it before and outside th view. But if other views also need to refer to the currentMealPlan, create one before the View and pass it as an environment variable. ie MealPlanView().environmentObject(currentMealPlan) and then in the view #EnvironmentObject var currentMealPlan: SocialMealPlan

When a Store's object is updated, auto-trigger objectWillChange.send() in ViewModel ObservableObjects

For a Store/Factory/ViewModel pattern using Combine and SwiftUI, I'd like a Store protocol-conforming class to expose a publisher for when specified model object(s) change internal properties. Any subscribed ViewModels can then trigger objectWillChange to display the changes.
(This is necessary because changes are ignored inside a model object that is passed by reference, so #Published/ObservableObject won't auto-fire for Factory-passed Store-owned models. It works to call objectWillChange in the Store and the VM, but that leaves out any passively listening VMs.)
That's a delegate pattern, right, extending #Published/ObservableObject to passed-by-reference objects? Combing through combine blogs, books, and docs hasn't triggered an idea to what's probably a pretty standard thing.
Crudely Working Attempt
I thought PassthroughSubject<Any,Never> would be useful if I exposed a VM's objectWillChange externally, but PassthroughSubject.send() will fire for every object within the model object. Wasteful maybe (although the ViewModel only fires its objectWillChange once).
Attaching a limiter (e.g., throttle, removeDuplicates) on Ext+VM republishChanges(of myStore: Store) didn't seem to limit the .sink calls, nor do I see an obvious way to reset the demand between the PassthroughSubject and the VM's sink... or understand how to attach a Subscriber to a PassthroughSubject that complies with the Protcols. Any suggestions?
Store-Side
struct Library {
var books: // some dictionary
}
class LocalLibraryStore: LibraryStore {
private(set) var library: Library {
didSet { publish() }
}
var changed = PassthroughSubject<Any,Never>()
func removeBook() {}
}
protocol LibraryStore: Store {
var changed: PassthroughSubject<Any,Never> { get }
var library: Library { get }
}
protocol Store {
var changed: PassthroughSubject<Any,Never> { get }
}
extension Store {
func publish() {
changed.send(1)
print("This will fire once.")
}
}
VM-Side
class BadgeVM: VM {
init(store: LibraryStore) {
self.specificStore = store
republishChanges(of: jokesStore)
}
var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
internal var subscriptions = Set<AnyCancellable>()
#Published private var specificStore: LibraryStore
var totalBooks: Int { specificStore.library.books.keys.count }
}
protocol VM: ObservableObject {
var subscriptions: Set<AnyCancellable> { get set }
var objectWillChange: ObservableObjectPublisher { get set }
}
extension VM {
internal func republishChanges(of myStore: Store) {
myStore.changed
// .throttle() doesn't silence as hoped
.sink { [unowned self] _ in
print("Executed for each object inside the Store's published object.")
self.objectWillChange.send()
}
.store(in: &subscriptions)
}
}
class OtherVM: VM {
init(store: LibraryStore) {
self.specificStore = store
republishChanges(of: store)
}
var objectWillChange = ObservableObjectPublisher() // Exposed {set} for external call
internal var subscriptions = Set<AnyCancellable>()
#Published private var specificStore: LibraryStore
var isBookVeryExpensive: Bool { ... }
func bookMysteriouslyDisappears() {
specificStore.removeBook()
}
}
Thanks #NewDev for pointing out subclassing as a smarter route.
If you want to nest ObservableObjects or have an ObservableObject re-publish changes in objects within an object passed to it, this approach works with less code than in my question.
In searching to simplify further with a property wrapper (to get at parent objectWillChange and simplify this further), I noticed a similar approach in this thread: https://stackoverflow.com/a/58406402/11420986. This only differs in using a variadic parameter.
Define VM and Store/Repo Classes
import Foundation
import Combine
class Repo: ObservableObject {
func publish() {
objectWillChange.send()
}
}
class VM: ObservableObject {
private var repoSubscriptions = Set<AnyCancellable>()
init(subscribe repos: Repo...) {
repos.forEach { repo in
repo.objectWillChange
.receive(on: DispatchQueue.main) // Optional
.sink(receiveValue: { [weak self] _ in
self?.objectWillChange.send()
})
.store(in: &repoSubscriptions)
}
}
}
Example Implementation
Repo: add didSet { publish() } to model objects
VM: The super.init() accepts any number of repos to republish
import Foundation
class UserDirectoriesRepo: Repo, DirectoriesRepository {
init(persistence: Persistence) {
self.userDirs = persistence.loadDirectories()
self.persistence = persistence
super.init()
restoreBookmarksAccess()
}
private var userDirs: UserDirectories {
didSet { publish() }
}
var someExposedSliceOfTheModel: [RootDirectory] {
userDirs.rootDirectories.filter { $0.restoredURL != nil }
}
...
}
import Foundation
class FileStructureVM: VM {
init(directoriesRepo: DirectoriesRepository) {
self.repo = directoriesRepo
super.init(subscribe: directoriesRepo)
}
#Published // No longer necessary
private var repo: DirectoriesRepository
var rootDirectories: [RootDirectory] {
repo.rootDirectories.sorted ...
}
...
}
It seems that what you want is a type that notifies when its internal properties change. That sounds an awful lot like what ObservableObject does.
So, make your Store protocol inherit from ObservableObject:
protocol Store: ObservableObject {}
Then a type conforming to Store could decide what properties it wants to notify on, for example, with #Published:
class StringStore: Store {
#Published var text: String = ""
}
Second, you want your view models to automatically fire off their objectWillChange publishers when their store notifies them.
The automatic part can be done with a base class - not with a protocol - because it needs to store the subscription. You can keep the protocol requirement, if you need to:
protocol VM {
associatedtype S: Store
var store: S { get }
}
class BaseVM<S: Store>: ObservableObject, VM {
var c : AnyCancellable? = nil
let store: S
init(store: S) {
self.store = store
c = self.store.objectWillChange.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
}
class MainVM: BaseVM<StringStore> {
// ...
}
Here's an example of how this could be used:
let stringStore = StringStore();
let mainVm = MainVM(store: stringStore)
// this is conceptually what #ObservedObject does
let c = mainVm.objectWillChange.sink {
print("change!") // this will fire after next line
}
stringStore.text = "new text"

Swift - How to access #Published var from func outside of view?

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