I have a Swift Combine question. Let’s say I have an ObservableObject with a few properties like this:
class AppState: ObservableObject{
static let shared = AppState()
#Published var a: Object?
#Published var b: Object?
#Published var c = [Object]()
}
I know I can be notified if a single object changes like this:
myCancellable = AppState.shared.$a.sink { a in
//Object 'a' changed
}
But is there a way to watch multiple properties and respond if any of them change?
Something like:
myCancellable = AppState.shared.[$a, $b, $c].sink { a, b, c in
//Objects 'a', 'b', or 'c' changed
}
The possible variant is to observe any published changes of entire object, like
myCancellable = AppState.shared
.objectWillChange.sink {
// react here
}
Assuming that a, b, and c are the same type (which Asperi pointed out to me they are not in the example, since c is an array), you can use Publishers.MergeMany:
myCancellable = Publishers.MergeMany([$a,$b,$c]).sink { newValue in
}
There are also more specific versions of this function for a set number of arguments. For example, in your case, you could use Publishers.Merge3
Often, in examples I've seen online, you'll see Publishers.MergeMany followed by collect(), which gathers results from all of the publishers before moving on, publishing the results as an array. From your question, though, it doesn't sound like this will meet your needs, as you want a notification each time a singular member changes.
Thanks for the great answers. Another idea I stumbled across was to just store my objects in a struct and observe that.
struct Stuff{
var a: Object?
var b: Object?
var c = [Object]()
}
Then in my AppState class:
class AppState: ObservableObject{
static let shared = AppState()
#Published var stuff: Stuff!
}
Then in the publisher:
myCancellable = AppState.shared.$stuff.sink { stuff in
print(stuff.a)
print(stuff.b)
print(stuff.c)
}
I like this approach since I don't want to observe everything that might change in the AppState class (which is probably an indication I should break it into smaller pieces). This seems to be working well so far.
Related
I recently worked on SwiftUI and starting writing code in a declarative way. But here comes a confusion. Like what is shown below, I want to (1)load the song data and (2)show the view by setting isInfoViewShown, after song is assigned to any value.
I assume didSet{} and Combine #Published .sink{} are doing things interchangeably. So I want to ask what are the differences between them? And in my own opinion, didSet{} can do most of jobs Combine do. So why should Apple announce Combine framework?
Any help is appreciated.
class InfoViewModel: ObservableObject {
#Published var song: Song? {
didSet { // Here's the didSet{}: [1] load song data
if let song = song {
load(song: song)
}
}
}
private var songSelectedSubscription: AnyCancellable?
#Published var isInfoViewShown: Bool = false
init() { // Here's the Combine #Published .sink{}: [2] show the view
songSelectedSubscription = $song.sink{ self.isInfoViewShown = ($0 == nil ? false : true) }
}
}
Sure, there are lots of ways to observe changes to data, KVO, Notification Center, didSet, combine etc, so in one sense these things are indeed similar. Differences though are:
a property can only have one didSet, which makes it hard for any number of observers to register an interest in the property.
But the big win with Combine is: a Combine pipeline allows you to create streams easily where you can say for example, transform the stream of changes to a stream that: observes changes to the user's input string, debounces it (rate limiting changes so we don't spam the server), filter those changes for any value that is at least 3 characters long, and that produces a new stream which you can observe. Map/FlatMap is also a really important combine operator, transform a stream of a's into a stream of b's. Also merging two streams together with combineLatest and so on, so you can take a stream of a's and stream of b's and make a stream of (a, b)'s and then map that to a stream of validated c's, for example.
ObservableObject is designed to hold the model structs. For view data like isInfoViewShown that should be #State in the View struct. In SwiftUI the View struct is comparable to the view model object in UIKit it looks like you are used to. Since it is a struct (value) type is faster and less error-prone. You can extract related #State var into their own struct using mutating func for testable logic which you probably did in your view model objects. A View struct will automatically call body when a let or a #State var value changes, this dependency tracking is built-in to SwiftUI, you no-longer need to use Combine for this like you might in UIKit.
FYI when we use Combine inside the ObservableObject we assign the end of the pipeline to the #Published we don't tend to use sink because then we would need to perform manual cancellation in deinit whereas assign does that automatically. Inside the #Published auto-genenerated willSet, it calls objectWillChange.send() which SwiftUI coalesces multiple of into a single recalculation of body in any View struct that use #StateObject, #ObservedObject or #EnvironmentObject (regardless of the object's properties accessed or not - let and #State var are more optimal in that regard).
I've got the following code, which seems very simple.
import SwiftUI
struct Tester : View
{
#State var blah : String = "blah"
func setBlah(_ val : String) {
blah = val
}
var body: some View {
Text("text")
}
}
var test = Tester()
test.setBlah("blee")
print(test.blah)
I would normally expect to see the final print statement display "blee", but it is "blah" -- the variable never changed. If I pass blah into another view via a binding, I am able to change the value of blah there. Any insight here would be appreciated -- including rtfm, if you can point me to the right manual -- I have looked, and haven't found an answer.
Edit: after reading #jnpdx 's answer, I have a slightly different version, but it still doesn't work -- I'm not worried about this specific code working, but trying to understand the magic that #jnpdx refers to, in terms of how #State works, and why a binding passed to another view is able to modify the original #State variable, while I am unable to within the struct itself. I am guessing there is some part of SwiftUI that needs to be instantiated that the #State property's communicate with in order to store the variables outside of the struct, as the apple documentation says. New version follows:
import Foundation
import SwiftUI
struct Tester : View
{
#State var blah : String
func setBlah(_ val : String) {
$blah.wrappedValue = val
}
var body: some View {
Text("smoe text")
}
}
var test = Tester(blah: "blah")
test.setBlah("blee") // expect to see blee printed, but get nil instead
print(test.blah)
Thanks :)
#State in SwiftUI doesn't work like simple mutating functions on a struct -- it's more like a separate layer of state that gets stored alongside the view hierarchy.
Let's look at what this would have to look like if it were not SwiftUI/#State:
struct Tester
{
var blah : String = "blah"
mutating func setBlah(_ val : String) {
blah = val
}
}
var test = Tester()
test.setBlah("blee") // prints correctly
print(test.blah)
Note that above, setBlah has to be marked mutating because it mutates the struct. Whereas in your example, the compiler doesn't require it, because the struct itself is not actually mutating -- the #State property wrapper is doing some behind-the-scenes magic.
Check out the documentation on State: https://developer.apple.com/documentation/swiftui/state
In particular:
Don’t initialize a state property of a view at the point in the view hierarchy where you instantiate the view, because this can conflict with the storage management that SwiftUI provides. To avoid this, always declare state as private, and place it in the highest view in the view hierarchy that needs access to the value. Then share the state with any child views that also need access, either directly for read-only access, or as a binding for read-write access.
By marking #State as private, you can prevent things like outside entities trying to manipulate it directly. However, in your example, you've circumvented this a bit by making a setter function that would avoid the private issue even if it were included. So, really, setBlah should be marked private as well.
Explanation
I am still in the process of learning to utilize SwiftUI patterns in the most optimal way. But most SwiftUI MVVM implementation examples I find are very simplistic. They usually have one database class and then 1-2 viewmodels that take data from there and then you have views.
In my app, I have a SQLite DB, Firebase and different areas of content. So I have a few separate model-vm-view paths. In the Android equivalent of my app, I used a pattern like this:
View - ViewModel - Repository - Database
This way I can separate DB logic like all SQL queries in the repository classes and have the VM handle only view related logic. So the whole thing looks something like this:
In Android this works fine, because I just pass through the LiveData object to the view. But when trying this pattern in SwiftUI, I kind of hit a wall:
It doesn't work / I don't know how to correctly connect the Published objects of both
The idea of "chaining" or nesting ObservableObjects seems to be frowned upon:
This article about Nested Observable Objects in SwiftUI:
I’ve seen this pattern described as “nested observable objects”, and it’s a subtle quirk of SwiftUI and how the Combine ObservableObject protocol works that can be surprising. You can work around this, and get your view updating with some tweaks to the top level object, but I’m not sure that I’d suggest this as a good practice. When you hit this pattern, it’s a good time to step back and look at the bigger picture.
So it seems like one is being pushed towards using the simpler pattern of:
View - ViewModel - Database Repository
Without the repository in-between. But this seems annoying to me, it would make my viewmodel classes bloated and would mix UI/business code with SQL queries.
My Code
So this is a simplified version of my code to demonstrate the problem:
Repository:
class SA_Repository: ObservableObject {
#Published var selfAffirmations: [SelfAffirmation]?
private var dbQueue: DatabaseQueue?
init() {
do {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
// Etc. other SQL code
} catch {
print(error.localizedDescription)
}
}
private func fetchSelfAffirmations() {
let saObservation = ValueObservation.tracking { db in
try SelfAffirmation.fetchAll(db)
}
if let unwrappedDbQueue = dbQueue {
let _ = saObservation.start(
in: unwrappedDbQueue,
scheduling: .immediate,
onError: {error in print(error.localizedDescription)},
onChange: {selfAffirmations in
print("change in SA table noticed")
self.selfAffirmations = selfAffirmations
})
}
}
public func updateSA() {...}
public func insertSA() {...}
// Etc.
}
ViewModel:
class SA_ViewModel: ObservableObject {
#ObservedObject private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
init() {
selfAffirmations = saRepository.selfAffirmations ?? []
}
public func updateSA() {...}
public func insertSA() {...}
// + all the Firebase stuff later on
}
View:
struct SA_View: View {
#ObservedObject var saViewModel = SA_ViewModel()
var body: some View {
NavigationView {
List(saViewModel.selfAffirmations, id: \.id) { selfAffirmation in
SA_ListitemView(content: selfAffirmation.content,
editedValueCallback: { newString in
saViewModel.updateSA(id: selfAffirmation.id, newContent: newString)
})
}
}
}
}
Attempts
Obviously the way I did it here is wrong, because it clones the data from repo to vm once with selfAffirmations = saRepository.selfAffirmations ?? [] but then it never updates when I edit the entries from the view, only on app restart.
I tried $selfAffirmations = saRepository.$selfAffirmations to just transfer the binding. But the repo one is an optional, so I'd need to make the vm selfAffirmations an optional too, which would then mean handling unnecessary logic in the view code. And not sure if it would even work at all.
I tried to do it manually with Combine but this way seemed to not be recommended and fragile. Plus it also didn't work:
selfAffirmations = saRepository.selfAffirmations ?? []
cancellable = saRepository.$selfAffirmations.sink(
receiveValue: { [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
)
Question
Overall I would just need some way to pass through the data from the repo to the view, but have the vm be in the middle as a separator. I read about the PassthroughSubject in Combine, which sounds like it would be fitting, but I'm not sure if I am just misunderstanding some concepts here.
Now I am not sure if my architecture concepts are wrong/unfitting, or if I just don't understand enough about Combine publishers yet to make this work.
Any advice would be appreciated.
After getting some input from the comments, I figured out a clean way.
The problem for me was understanding how to make a property of a class publish its values. Because the comments suggested that property wrappers like #ObservedObject was a frontend/SwiftUI only thing, making me assume that everything related was limited to that too, like #Published.
So I was looking for something like selfAffirmations.makePublisher {...}, something that would make my property a subscribable value emitter. I found that arrays naturally come with a .publisher property, but this one seems to only emit the values once and never again.
Eventually I figured out that #Published can be used without #ObservableObject and still work properly! It turns any property into a published property.
So now my setup looks like this:
Repository (using GRDB.swift btw):
class SA_Repository {
private var dbQueue: DatabaseQueue?
#Published var selfAffirmations: [SelfAffirmation]?
// Set of cancellables so they live as long as needed and get deinitialiazed with the class end
var subscriptions = Array<DatabaseCancellable>()
init() {
dbQueue = Database.sharedInstance.dbQueue
fetchSelfAffirmations()
}
private func fetchSelfAffirmations() {
// DB code....
}
}
And viewmodel:
class SA_ViewModel: ObservableObject {
private var saRepository = SA_Repository()
#Published var selfAffirmations: [SelfAffirmation] = []
// Set of cancellables to keep them running
var subscriptions = Set<AnyCancellable>()
init() {
saRepository.$selfAffirmations
.sink{ [weak self] repoSelfAffirmations in
self?.selfAffirmations = repoSelfAffirmations ?? []
}
.store(in: &subscriptions)
}
}
Currently I have the code below which I'm trying to use as a navigation switch so I can navigate through different views without using the crappy NavigationLinks and otherwise. I'm by default a WebDev, so I've been having a mountain of issues transferring my knowledge over to Swift, the syntax feels completely dissimilar to any code I've written before. Anyways, here's the code;
import Foundation
import Combine
import SwiftUI
class ViewRouter: ObservableObject {
let objectWillChange: PassthroughSubject<ViewRouter,Never>
#Published var currentPage: String = "page1" {
didSet {
objectWillChange.send(self)
}
}
init(currentPage: String) {
self.currentPage = currentPage
}
}
As you can see, it's really simple, and I just use the object to switch values and display different views on another file, the only errors which prevent me from building it is the fact that the initializer is saying "Return from initializer without initializing all stored properties", even though the only variable is the currentPage variable which is defined. I know it's saying that objectWillChange is not defined by the message, but objectWillChange doesn't have any value to be assigned. Any help would be appreciated.
You just declare objectWillChange, but don't initialise it.
Simply change the declaration from
let objectWillChange: PassthroughSubject<ViewRouter,Never>
to
let objectWillChange = PassthroughSubject<ViewRouter,Never>()
However, using a PassthroughSubject shouldn't be necessary. currentPage is already #Published, so you can simply subscribe to its publisher. What you are trying to achieve using a PassthroughSubject and didSet is already defined by the swiftUI property wrappers, ObservableObject and Published.
class ViewRouter: ObservableObject {
#Published var currentPage: String
init(currentPage: String) {
self.currentPage = currentPage
}
}
Then you can simply do
let router = ViewRouter(currentPage: "a")
router.$currentPage.sink { page in print(page) }
router.currentPage = "b" // the above subscription prints `"b"`
I would like to use Combine's #Published attribute to respond to changes in a property, but it seems that it signals before the change to the property has taken place, like a willSet observer. The following code:
import Combine
class A {
#Published var foo = false
}
let a = A()
let fooSink = a.$foo.dropFirst().sink { _ in // `dropFirst()` is to ignore the initial value
print("foo is now \(a.foo)")
}
a.foo = true
outputs:
foo is now false
I'd like the sink to run after the property has changed like a didSet observer so that foo would be true at that point. Is there an alternative publisher that signals then, or a way of making #Published work like that?
There is a thread on the Swift forums for this issue. Reasons of why they made the decision to fire signals on "willSet" and not "didSet" explained by Tony_Parker
We (and SwiftUI) chose willChange because it has some advantages over
didChange:
It enables snapshotting the state of the object (since you
have access to both the old and new value, via the current value of
the property and the value you receive). This is important for
SwiftUI's performance, but has other applications.
"will" notifications are easier to coalesce at a low level, because you can
skip further notifications until some other event (e.g., a run loop
spin). Combine makes this coalescing straightforward with operators
like removeDuplicates, although I do think we need a few more grouping
operators to help with things like run loop integration.
It's easier to make the mistake of getting a half-modified object with did,
because one change is finished but another may not be done yet.
I do not intuitively understand that I'm getting willSend event instead of didSet, when I receive a value. It does not seem like a convenient solution for me. For example, what do you do, when in ViewController you receiving a "new items event" from ViewModel, and should reload your table/collection? In table view's numberOfRowsInSection and cellForRowAt methods you can't access new items with self.viewModel.item[x] because it's not set yet. In this case, you have to create a redundant state variable just for the caching of the new values within receiveValue: block.
Maybe it's good for SwiftUI inner mechanisms, but IMHO, not so obvious and convenient for other usecases.
User clayellis in the thread above proposed solution which I'm using:
Publisher+didSet.swift
extension Published.Publisher {
var didSet: AnyPublisher<Value, Never> {
self.receive(on: RunLoop.main).eraseToAnyPublisher()
}
}
Now I can use it like this and get didSet value:
self.viewModel.$items.didSet.sink { [weak self] (models) in
self?.updateData()
}.store(in: &self.subscriptions)
I'm not sure if it is stable for future Combine updates, though.
UPD: Worth to mention that it can possibly cause bugs (races) if you set value from a different thread than the main.
Original topic link: https://forums.swift.org/t/is-this-a-bug-in-published/31292/37?page=2
You can write your own custom property wrapper:
import Combine
#propertyWrapper
class DidSet<Value> {
private var val: Value
private let subject: CurrentValueSubject<Value, Never>
init(wrappedValue value: Value) {
val = value
subject = CurrentValueSubject(value)
wrappedValue = value
}
var wrappedValue: Value {
set {
val = newValue
subject.send(val)
}
get { val }
}
public var projectedValue: CurrentValueSubject<Value, Never> {
get { subject }
}
}
Further to Eluss's good explanation, I'll add some code that works. You need to create your own PassthroughSubject to make a publisher, and use the property observer didSet to send changes after the change has taken place.
import Combine
class A {
public var fooDidChange = PassthroughSubject<Void, Never>()
var foo = false { didSet { fooDidChange.send() } }
}
let a = A()
let fooSink = a.fooDidChange.sink { _ in
print("foo is now \(a.foo)")
}
a.foo = true
Before the introduction of ObservableObject SwiftUI used to work the way that you specify - it would notify you after the change has been made. The change to willChange was made intentionally and is probably caused by some optimizations, so using ObservableObjsect with #Published will always notify you before the changed by design. Of course you could decide not to use the #Published property wrapper and implement the notifications yourself in a didChange callback and send them via objectWillChange property, but this would be against the convention and might cause issues with updating views. (https://developer.apple.com/documentation/combine/observableobject/3362556-objectwillchange) and it's done automatically when used with #Published.
If you need the sink for something else than ui updates, then I would implement another publisher and not go agains the ObservableObject convention.
Another alternative is to just use a CurrentValueSubject instead of a member variable with the #Published attribute. So for example, the following:
#Published public var foo: Int = 10
would become:
public let foo: CurrentValueSubject<Int, Never> = CurrentValueSubject(10)
This obviously has some disadvantages, not least of which is that you need to access the value as object.foo.value instead of just object.foo. It does give you the behavior you're looking for, however.