How do I reference an `actor`'s properties from a `Button`'s action or `Binding`? - swift

I have an actor like this, which performs long, complex work constantly in the background:
actor Foo {
var field: [Bar]
struct Bar {
// ...
}
}
How do I update its field from a SwiftUI view?
I tried this, but got these errors:
import SwiftUI
struct MyView: View {
#StateObject
var foo: Foo
var body: some View {
Text("Field count is \(foo.field.count)") // 🛑 Actor-isolated property 'field' can not be referenced from the main actor
Button("Reset foo") {
foo.field = [] // 🛑 Actor-isolated property 'field' can not be mutated from the main actor
}
}
}
How do I access & mutate my actor from within a SwiftUI view?

The problem with accessing the field property of the actor is that it requires and await call, if the access is made outside of the actor. This means a suspension point in your SwiftUI code, which means that when the SwiftUI code resumes, it might no longer be executing on the main thread, and that's a big problem.
If the actor doesn't do background work, then Asperi's solution that uses #MainAction would nicely work, as in that case the SwiftUI accesses happen on the main thread.
But if the actor runs in the background, you need another sync point that runs code on the main thread, that wraps the Foo actor, and which is consumed by your view:
actor Foo {
private(set) var field: [Bar]
func updateField(_ field: [Bar]) {
self.field = field
}
struct Bar {
// ...
}
}
class FooModel: ObservableObject {
private let foo: Foo
#Published var field: [Foo.Bar] = [] {
didSet {
Task { await foo.updateField(field) }
}
}
init(foo: Foo) {
self.foo = foo
Task { self.field = await foo.field }
}
}
struct MyView: View {
#StateObject
var foo: FooModel
However this is only half of the story, as you'll need to also send notifications from Foo to FooModel when the value of field changes. You can use a PassthroughSubject for this:
actor Foo {
var field: [Bar] {
didSet { fieldSubject.send(field) }
}
private let fieldSubject: PassthroughSubject<[Bar], Never>
let fieldPublisher: AnyPublisher<[Bar], Never>
init() {
field = ... // initial value
fieldSubject = PassthroughSubject()
fieldPublisher = fieldSubject.eraseToAnyPublisher()
}
func updateField(_ field: [Bar]) {
self.field = field
}
struct Bar {
// ...
}
}
and subscribe to the published from the model:
class FooModel: ObservableObject {
private let foo: Foo
#Published var field: [Foo.Bar] = [] {
didSet {
Task { await foo.updateField(field) }
}
}
init(foo: Foo) {
self.foo = foo
Task { self.field = await foo.field }
foo.fieldPublisher.receive(on: DispatchQueue.main).assign(to: &$field)
}
}
As you can see, there's a non-trivial amount of code to be written, due to the fact that actors run on arbitrary threads, while your SwiftUI code (or any UI code in general) must be run only on the main thread.

Related

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.

Swift Combine - How to subscribe to nested Observable Objects

This is a slightly more abstract version of this question. In the app, these nested Observable Objects are indeed used in a view (so I'd rather use Observable Objects rather than straight Publishers). However, I would like to be able to simply subscribe to the view models in order to test them. The protocol is there so I can mock out Nested in tests.
This is the basic setup:
protocol NestedProtocol: AnyObject {
var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
#Published var string = ""
}
class Parent: ObservableObject {
#Published var nested: NestedProtocol
init(nested: NestedProtocol) {
self.nested = nested
}
}
var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)
parent.$nested.sink { newValue in
print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)
Then this command
nested.string = "foo1" outputs "NEW VALUE ", which is expected as the initial value of nested. I would like it to output "NEW VALUE foo1". (TIL published variables seem to be current value publishers.)
Of course I could do
nested.string = "foo1"
parent.nested = nested
and I would get "NEW VALUE foo1", but that's smelly.
I tried
protocol NestedProtocol: ObservableObject {
var string: String { get set }
}
class Nested<T>: ObservableObject where T: NestedProtocol {
...
But in real life, I would like nested to declare some static constants, which is not allowed in generic types. So that doesn't work.
From the cited question/answer, I also tried combinations of
Parent
init() {
nested.objectWillChange.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: sinkHole)
}
Nested
init() {
string.sink { [weak self] (_) in
self?.objectWillChange.send()
}.store(in: sinkHole)
}
No dice. Those methods were getting called but that outer-level sink was still just returning "NEW VALUE "
I also tried calling
parent.nested.string = "foo1"
So now I'm modifying the parent, and that should work, right? Wrong.
There's a bunch to unpack here.
First, you might know that if a property is a value-type, like a struct or String, then marking it as #Published just works:
class Outer {
#Published var str: String = "default"
}
let outer = Outer()
outer.$str.sink { print($0) }
outer.str = "changed"
Will output:
default
changed
Your question, however, is about a nested observable object, which is a reference type. So, the above wouldn't work with a reference-type.
But in your example you're using a protocol as an existential (i.e. in place of an eventual instance), and as you noted, without inheriting from AnyObject, then it really behaves like a value-type:
protocol InnerProtocol {
var str: String { get set }
}
class Inner: InnerProtocol {
#Published var str: String = "default"
}
class Outer {
#Published var inner: InnerProtocol
init(_ inner: InnerProtocol) { self.inner = inner }
}
let inner = Inner()
let outer = Outer(inner)
outer.$inner.sink { print($0.str) }
outer.inner.str = "changed"
This would also output:
default
changed
which looks like what you wanted, but in fact it doesn't really "observe" any changes in the nested object. When you do outer.inner.str, it has value-type semantics, so it's as-if you re-assigned the .inner property. But if you are truly interested in observing changes of the object itself, then this approach wouldn't work at all. For example:
nested.str = "inner changed"
would not cause an output. Neither would there be an output if the inner object changed its own property, e.g.:
init() {
DisplatchQueue.main.asyncAfter(.now() + 1) {
self.str = "async changed"
}
}
So, it's unclear what exactly you're trying to achieve. If you want to observe a reference type property, you'd need to observe it directly.
class Inner: ObservableObject {
#Published var str: String
//...
}
class Outer: ObservableObject {
var inner: Inner
//...
}
//...
outer.inner.$str.sink { ... }
// or
outer.inner.objectWillChange.sink { ... }
You can achieve this with a protocol too, if you insist:
protocol InnerProtocol: ObservableObject {
var str: String { get set }
}
class Inner: InnerProtocol {
#Published var str: String = "default"
}
class Outer<T: InnerProtocol>: ObservableObject {
var inner: T
init(_ inner: T) { self.inner = inner }
}
let inner = Inner()
let outer = Outer(inner)
outer.inner.$str.sink { ... }
inner.str = "changed"
This took me hours and I stumbled upon it by accident while trying to modify my protocol in various ways.
Lesson 1:
protocol NestedProtocol: AnyObject {
var string: String { get set }
}
should be
protocol NestedProtocol {
var string: String { get set }
}
Why? I'm not sure. Apparently, if the Parent cannot assume that the published object is a class, then it watches modifications on it more closely? My instinct tells me the exact opposite, but it goes to show how much I can trust my instinct.
Lesson 2:
Indeed my 4th idea was correct and you need to name the parent in the nested object modification:
nested.string = "foo1"
should be
parent.nested.string = "foo1"
Again, they're all classes so it goes slightly against my understanding, but I don't know all the magic that goes on under #Published.
The final complete version looks like this:
protocol NestedProtocol {
var string: String { get set }
}
class Nested: ObservableObject, NestedProtocol {
#Published var string = ""
}
class Parent: ObservableObject {
#Published var nested: NestedProtocol
init(nested: NestedProtocol) {
self.nested = nested
}
}
var sinkHole = Set<AnyCancellable>()
let nested = Nested()
let parent = Parent(nested: nested)
parent.$nested.sink { newValue in
print("NEW VALUE \(newValue.string)")
}.store(in: &sinkHole)
and
nested.string = "foo1"
parent.nested.string = "foo2"
returns
"NEW VALUE "
"NEW VALUE foo2"

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

Is it possible to convert instance of a class to an instance of a subclass?

I've recently came across a case where it would be very convenient to convert an instance of a class to a subclass, while the instance has been created within the parent class. But I've never seen such thing. So is there a way to do something like:
class Foo {
var name: String
}
class Bar: Foo {
var friendName: String
}
let foo = Foo(name: "Alice")
foo.toBar(friendName: "Bob")
// foo now of type Bar, as if I'd done
// foo = Bar(name: "Alice", friendName: "Bob")
If that's not possible, is there some reasons this would be impossible from a design perspective?
===edit=== description of a use case where it could make sense
Let say there's two views representing what correspond to the same database record for a book, on is a just a preview of the book and another is a more complex view. Models could be:
protocol BookMetaDelegate {
func onReadStatusUpdate()
}
/// describe a book
class BookMeta {
var delegate: BookMetaDelegate?
private var _hasBeenRead: Bool
var hasBeenRead: Bool {
get {
return _hasBeenRead
}
set {
guard newValue != _hasBeenRead else { return }
_hasBeenRead = newValue
delegate?.onReadStatusUpdate()
}
}
var title: String
}
/// contains all the content of a book
class Book: BookMeta {
var content: BookContent
var lastPageRead: Int
/// some logic that only makes sense in a Book instance
func getLastPageRead() {
return content.getPage(lastPageRead)
}
}
and views could look like:
class BookPreview: UIView, BookMetaDelegate {
var book: BookMeta
init(book: BookMeta) {
book.delegate = self
}
func onReadStatusUpdate() {
print("read status has changed! UI should update")
}
}
class BookView: UIView {
var book: Book
init(book: Book) {
book.hasBeenRead = true
}
}
Then things could happen like
fetch(bookMetaWithId: 123).then { bookMeta in // bookMeta is of type BookMeta
let preview = BookPreview(book: bookMeta)
...
fetch(contentOf: bookMeta).then { content, lastPageRead in
bookMeta.asBook(content: content, lastPageRead: lastPageRead)
let bookView = BookView(book: bookMeta) // doing so will change the hasBeenRead flag and message the instance's delegate, ie the preview
...
}
}
Thinking more about it, it sounds like that if such thing was possible, it'd break things like:
class Foo {
var name: String
}
class Bar: Foo {
var friendName: String
}
class Bla: Foo {
var surname: String
}
func something(foo: Foo) {
foo.toBla(surname: "Will")
}
let bar = Bar(name: "Alice", friendName: "Bob")
something(foo: bar) // what does that do ??? is bar a Bla now ?
so that'd be a good reason for making such casting impossible.