Swift / Apple Development Chat

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I’ve been banging my head against one particular puzzle related to Swift concurrency:

I have an identical network request I want to make to different endpoints and see which ones respond in a reasonable amount of time, so I can make a determination on which endpoint to use going forward. But I also want the data from the first endpoint to respond so that the app can take the response and continue processing it, without having to wait on the results of the remaining requests which should all be identical. Concurrency task groups seem built for this at first, until you realize that the group cannot exit until all the child tasks have either completed or cancelled. This is bad if one of the endpoints is going to timeout. You don’t want to take that 100ms turnaround and make it many seconds waiting for that timeout to happen. It also means that if you cancel one of the requests right before it was going to respond you might not know that there is a better route that you should be picking for future requests instead.

I’m thinking I’ll have to fire the requests from a continuation instead of a task group. The first request to complete will resume the continuation, and the remaining requests will update the state.

But maybe I’m missing something better here?
 
Last edited:

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
I’ve been banging my head against one particular puzzle related to Swift concurrency:

I have an identical network request I want to make to different endpoints and see which ones respond in a reasonable amount of time, so I can make a determination on which endpoint to use going forward. But I also want the data from the first endpoint to respond so that the app can take the response and continue processing it, without having to wait on the results of the remaining requests which should all be identical. Concurrency task groups seem built for this at first, until you realize that the group cannot exit until all the child tasks have either completed or cancelled. This is bad if one of the endpoints is going to timeout. You don’t want to take that 100ms turnaround and make it many seconds waiting for that timeout to happen. It also means that if you cancel one of the requests right before it was going to respond you might not know that there is a better route that you should be picking for future requests instead.

I’m thinking I’ll have to fire the requests from a continuation instead of a task group. The first request to complete will resume the continuation, and the remaining requests will update the state.

But maybe I’m missing something better here?
I don’t think you are. You need to update the state separately anyway if you want the function to return after the response from the first endpoint arrives, right?
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I don’t think you are. You need to update the state separately anyway if you want the function to return after the response from the first endpoint arrives, right?

Some variation of it anyways. I’ve shifted between a couple different approaches around checking what network paths lead to the desired host (My code is aware of up to 3, but it’s possible none of those are even available). But interestingly, Combine seems suited to let me get rid of the continuation and solve it a bit more elegantly using one of two approaches that feed a publisher with completion handlers, and then read it out using concurrent code:

- Detached async task that updates state that is part of a CurrentValueSubject, setting details on the available paths (in priority order) once it knows the status on a given path, if all paths have been checked, and if there’s at least one path available. This fires on network changes, or otherwise when we think we need to recalculate the available paths. Tasks that want to make requests to the host can listen to the value subject as an AsyncSequence and wait for the state to be either “There’s no path and I’ve checked them all” or “there’s at least one good path”. Because of how CurrentValueSubject works, the current state is always available to new subscribers and so if there’s already one path (or more), this will return quickly. The task can then use the paths in priority order to attempt to reach the host and even do fallbacks if one returns an error (although timeouts are a lot harder). This makes network requests take a little longer in some cases as the request is waiting for the reachability checks to complete before doing it’s work rather than baking in the reachability check.

- Use a PassthroughSubject when the request is fired to the host along multiple paths, and convert that to an AsyncSequence. The task waiting on the response can pull the first value out and then unsubscribe. The remaining network requests will update the cached state and then publish to the void. This makes early network calls more expensive as the device could get multiple responses. It also complicates handling of some aspects of path state when it comes to paths that are taking a while to respond.

Neat. I’m liking it. The tradeoff is: latency vs data/complexity. We are talking on the order of one request worth of latency vs 12KB extra data, generally. I might just be overanalyzing it, so I might just go with the one that uses the simplest state.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
- Detached async task that updates state that is part of a CurrentValueSubject, setting details on the available paths (in priority order) once it knows the status on a given path, if all paths have been checked, and if there’s at least one path available. This fires on network changes, or otherwise when we think we need to recalculate the available paths. Tasks that want to make requests to the host can listen to the value subject as an AsyncSequence and wait for the state to be either “There’s no path and I’ve checked them all” or “there’s at least one good path”. Because of how CurrentValueSubject works, the current state is always available to new subscribers and so if there’s already one path (or more), this will return quickly. The task can then use the paths in priority order to attempt to reach the host and even do fallbacks if one returns an error (although timeouts are a lot harder). This makes network requests take a little longer in some cases as the request is waiting for the reachability checks to complete before doing it’s work rather than baking in the reachability check.
Hmm. I always try to avoid Combine (I don't like the readability of it, plus I'm unsure what will happen to it once Swift Concurrency matures), but it's a very elegant solution here. I don't think you could build this as elegantly without Combine. I was thinking you could build a @Published struct instead, holding both the state of the path-finding calls (whether it is still checking paths / at least one good path / no path and all have been checked) and a list of paths sorted by priority. But then, you'd have no way of making a Task wait until that @Published property changes to at least one good path / no path and all have been checked. Or at least I can't think of one. So Combine it is.

On this topic, there’s one related to new Instruments tools for investigating what’s going on with Swift concurrency. Neat stuff. I’ll get some use out of that.
I finally got to watch that talk. One of my favourite talks this year, not only because of the new tool but also because it does an excellent job at explaining how to use concurrency in practice and some common ways people misuse it. It also shows how to do something equivalent to the 'old' concurrentPerform by spawning several detached Tasks, which I wasn't sure was the correct way to do it (I'm still somewhat concerned about what would happen if you spawn too many tasks, but since Apple is showing it on their sample code...).
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Hmm. I always try to avoid Combine (I don't like the readability of it, plus I'm unsure what will happen to it once Swift Concurrency matures), but it's a very elegant solution here. I don't think you could build this as elegantly without Combine. I was thinking you could build a @Published struct instead, holding both the state of the path-finding calls (whether it is still checking paths / at least one good path / no path and all have been checked) and a list of paths sorted by priority. But then, you'd have no way of making a Task wait until that @Published property changes to at least one good path / no path and all have been checked. Or at least I can't think of one. So Combine it is.

Honestly, I don't think Combine is going anywhere, but we should continue to see deeper and deeper integration between the two as it matures. For a lot of the main thread publishing that SwiftUI uses, you don't need/want concurrency getting in the way. And concurrency is not really about providing Pub/Sub functionality. There's overlap because perhaps you want to update published state from async tasks, or you want to have a task wait for a particular state change to be published. That where the integration comes in, but one doesn't really need to replace the other. Combine added AsyncPublisher last year which conforms to AsyncSequence, and so allows you to await on the values coming from the publisher. It's the crux of how my approach works.

A way to think about what I've done is that I'm really just following reactive programming practices better by explicitly making my state a value that can be subscribed to. And I actually think that's a good thing. At the very least the data flow makes more sense to me now, and it's easier to decouple it this way since the pub/sub model is more decoupled by design. Network state changes trigger server state changes, which tasks that need the server state can subscribe to and await on.

As I get more used to reactive programming, I'm finding I'm leaning on Combine more and more, especially once I have a good strategy for unit testing publisher and subscriber behaviors. My internal services provide publishers that the ViewModel subscribes to, in order to get updates (or even other services). CoreData-based view models use publishers based on KVO to directly update @Published properties. The Combine-based version of observing an AVPlayer is much nicer to work with than hooking up everything directly. And my playback service that I'm in the middle of fixing up publishes its own state which I can feed both to my own ViewModel, and to MPNowPlayingInfoCenter, decoupling things nicely where before the service was an observable object that pushed out to MPNowPlayingInfoCenter before.

If anything, Combine really feels like the replacement for KVO in Swift (and it's a good one with a lot more power than KVO has in Obj-C). I really can't see Combine being replaced by concurrency for that reason alone.

EDIT: If we do see a Combine "replacement", my bet is more that it's going to be a version of Combine that runs on Linux as well and doesn't have the strict dependencies on things like RunLoops and GCD for thread hopping and instead can be told which global actor to receive events on, or if it should be received on a background task.

I finally got to watch that talk. One of my favourite talks this year, not only because of the new tool but also because it does an excellent job at explaining how to use concurrency in practice and some common ways people misuse it. It also shows how to do something equivalent to the 'old' concurrentPerform by spawning several detached Tasks, which I wasn't sure was the correct way to do it (I'm still somewhat concerned about what would happen if you spawn too many tasks, but since Apple is showing it on their sample code...).

Yeah, very useful talk.

Tasks by themselves don't need a ton of heap space, IIRC. Since they contain blocks, there's the same state you would need to keep for a dispatched block with GCD. The only real difference is that they also preserve their stack state while they are suspended. But I would not be surprised at all if tasks don't save off their stack until the first suspension. So for CPU-bound tasks that don't suspend, I suspect performance is going to be very close to GCD in the same scenario.

I had a bug where I created 20k network requests in a task group, and they would eventually start timing out because they would be waiting more than 60 seconds for URLSession to start sending the request. The number of tasks didn't seem to really be much of a problem on its own. Throttling the number of tasks in flight at a time in the task group is pretty easy and solved the issue until I can address the underlying problem that leads to needing that many network requests in the first place.
 
Last edited:

ArgoDuck

Power User
Site Donor
Posts
101
Reaction score
161
Location
New Zealand
Main Camera
Canon
I’m enjoying this ongoing exchange between the two of you. I’ve only just started into Core Data - previously I developed a thin object model on top of SQLite - and I plan to leap into concurrency later this year. Thus, much of your discussion is over my head right now, but really useful for highlighting issues and possibilities.

Thanks! Continuing to follow with interest.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I’m enjoying this ongoing exchange between the two of you. I’ve only just started into Core Data - previously I developed a thin object model on top of SQLite - and I plan to leap into concurrency later this year. Thus, much of your discussion is over my head right now, but really useful for highlighting issues and possibilities.

Thanks! Continuing to follow with interest.

Everything I know about Core Data, I learned from banging my head against what feels like a brick wall. There are times where I sometimes wished I went with something else, especially as I skew more and more towards reactive programming with my project.

Out of curiosity, did you use any libraries on top of SQLite like GRDB, or just raw SQLite?
 

ArgoDuck

Power User
Site Donor
Posts
101
Reaction score
161
Location
New Zealand
Main Camera
Canon
Everything I know about Core Data, I learned from banging my head against what feels like a brick wall. There are times where I sometimes wished I went with something else, especially as I skew more and more towards reactive programming with my project.

Out of curiosity, did you use any libraries on top of SQLite like GRDB, or just raw SQLite?
I’m familiar with the head banging 😆

Raw, except I abstracted a little, creating my own fairly thin (but useful) library to make my use of SQLite more swift-like, or before that more C++ like. I actually developed mostly in windows until 2018 when I switched to Apple and Swift. What I did was a start toward something like GRDB, just for my own use but with similar goals I guess. That is, though very experienced with SQL, I didn’t like mixing clumsy string interpolations etc into my code.

And had I known about GRDB, or thought to look, I would likely have gone that way instead…

My interest in Core Data is motivated by the ease of hosting the data store in iCloud for device sharing, and just steering a bit closer to an ‘Apple-ly’ way of doing things.

What this ‘way’ is, is something I’m still working through. Coming from C++ and OOP to POP, SwiftUI and Swift, functional programming and MVVM (or?) has been quite a transition!

Back to Core Data I’ve only recently started to grasp that it is more than just a wrapper around SQLite, actually thanks to your link to Dave De Long’s articles…

Anyway, this whole thread with all the contributors has been useful and stimulating. At some point, my own ideas might coalesce enough to be worth volunteering a remark or two.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
As I get more used to reactive programming, I'm finding I'm leaning on Combine more and more, especially once I have a good strategy for unit testing publisher and subscriber behaviors. My internal services provide publishers that the ViewModel subscribes to, in order to get updates (or even other services). CoreData-based view models use publishers based on KVO to directly update @Published properties. The Combine-based version of observing an AVPlayer is much nicer to work with than hooking up everything directly. And my playback service that I'm in the middle of fixing up publishes its own state which I can feed both to my own ViewModel, and to MPNowPlayingInfoCenter, decoupling things nicely where before the service was an observable object that pushed out to MPNowPlayingInfoCenter before.
I always get a bit lost with the semantics on those topics. SwiftUI, when the UI is hooked up to ObservedObject properties, is considered reactive programming too, right? I know SwiftUI + async/await does not overlap Combine completely, but most of the things I had used Combine for in past projects were no longer needed when using SwiftUI. Well, IIRC the @Published 'magic' uses Combine under the hood, so I should rather say that I have no longer needed to use Combine explicitly (not as much).

I had a bug where I created 20k network requests in a task group, and they would eventually start timing out because they would be waiting more than 60 seconds for URLSession to start sending the request. The number of tasks didn't seem to really be much of a problem on its own. Throttling the number of tasks in flight at a time in the task group is pretty easy and solved the issue until I can address the underlying problem that leads to needing that many network requests in the first place.
Oh, that's great. It's on the order of magnitude of the number of Tasks that I could end up having in a worst-case scenario.

What this ‘way’ is, is something I’m still working through. Coming from C++ and OOP to POP, SwiftUI and Swift, functional programming and MVVM (or?) has been quite a transition!
I'm still skeptical about the whole POP thing, or at least skeptical of the way people are using it. I admit I haven't watched the Protocol-Oriented-Programming in Swift talk that started all of this, but the way everyone understood it (in my obviously anecdotal experience of the people I've worked with) seems flawed to me. But I have to read a lot more about it before fully forming an opinion.

MVVM, on the other hand? I love it. Interestingly, now that I'm revisiting Apple's sample code (to see how they architected their more complex sample apps), they aren't using MVVM at all. Instead, they just inject the model via @EnvironmentObject to the views. Hmm.

Back to Core Data I’ve only recently started to grasp that it is more than just a wrapper around SQLite
Yup, this was one of the *oooh* moments to me too, when I first started to work with CoreData.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I always get a bit lost with the semantics on those topics. SwiftUI, when the UI is hooked up to ObservedObject properties, is considered reactive programming too, right? I know SwiftUI + async/await does not overlap Combine completely, but most of the things I had used Combine for in past projects were no longer needed when using SwiftUI. Well, IIRC the @Published 'magic' uses Combine under the hood, so I should rather say that I have no longer needed to use Combine explicitly (not as much).

ObservableObjects are a reactive concept, yes. Reactive programming is the idea of declaring how a change in state in one part of an app translates to changes in other state (UI or data). SwiftUI is built around a “push-pull” model of reactive programming, where the invalidation by something like objectWillChange, or other publishers is the push, and then the running of a view’s body to calculate the new UI state is the pull.

It’s hard to be “pure” reactive in languages like JS or Swift because they are imperative programming languages, but you can still model data flows in a reactive manner with them, and use imperative code to describe transformations, which is what SwiftUI and Combine does.

I'm still skeptical about the whole POP thing, or at least skeptical of the way people are using it. I admit I haven't watched the Protocol-Oriented-Programming in Swift talk that started all of this, but the way everyone understood it (in my obviously anecdotal experience of the people I've worked with) seems flawed to me. But I have to read a lot more about it before fully forming an opinion.

I do recommend the talk as it describes the nature of POP better than I ever could. But the Swift runtime is built on POP, as is SwiftUI. While I don’t think it is the savior of programming, it has let me kick most inheritance to the curb in favor of conformance, and for me, being able to compose conformances leads to more testable code. So I favor it over OOP style inheritance. NSObject and NSManagedObject are about all I subclass from these days.

MVVM, on the other hand? I love it. Interestingly, now that I'm revisiting Apple's sample code (to see how they architected their more complex sample apps), they aren't using MVVM at all. Instead, they just inject the model via @EnvironmentObject to the views. Hmm.

One reason I like MVVM is that the VM layer in SwiftUI can be as thin or as thick as needed, making it very pragmatic. Perhaps you can get away with just interacting with the model directly because it’s using NSManagedObject. Maybe you have a bunch of services and so you want that separation. Do what makes the most sense at the time, IMO.

I tend to favor the approach of adding complexity as needed, so Apple’s approach here makes some sense to me.
 

ArgoDuck

Power User
Site Donor
Posts
101
Reaction score
161
Location
New Zealand
Main Camera
Canon
MVVM, on the other hand? I love it. Interestingly, now that I'm revisiting Apple's sample code (to see how they architected their more complex sample apps), they aren't using MVVM at all. Instead, they just inject the model via @EnvironmentObject to the views. Hmm..
In the apple developer forums (IIRC, don’t have the link right now) one developer started something of a crusade to argue that SwiftUI largely supplants the need for view models. I can kind of see the point, whilst also seeing that many models need the VM layer to avoid potential mess.

I‘m a social scientist and statistician these days - I stopped being a full time developer quite a while ago - and many of my models are fairly pure computational expressions of some underlying scientific model. The question for me then is how can I make the app reasonably interesting? I have to expose parameters but what intermediate results can I expose as well? I’m an impatient sort!

SwiftUI has been good for this - I’m very excited by the new Chart capability! - but earlier I found myself building things into the model purely for their presentational value. These are things that don’t translate directly to some SwiftUI element (eg they need aggregation or whatever). They don’t belong in the model, hence MVVM makes a lot of sense to me too.

I like @Nycturne’s comments above too! Hmm, still bouncing things around…
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
In the apple developer forums (IIRC, don’t have the link right now) one developer started something of a crusade to argue that SwiftUI largely supplants the need for view models. I can kind of see the point, whilst also seeing that many models need the VM layer to avoid potential mess.

Yeah, I don’t fully buy it. I’d say that you don’t need to start by building view models right out of the gate, but I wouldn’t leave tools unused if it helps make the code more maintainable. If your view model is just wrapping the model, then it’s not really adding any value for sure.

Apple‘s documentation now points out that State/Binding is recommended for view state, while ObservableObject is recommended for model objects. With that in mind, it isn’t too insane to convert a model object to a view model as things get more complex.

SwiftUI has been good for this - I’m very excited by the new Chart capability! - but earlier I found myself building things into the model purely for their presentational value. These are things that don’t translate directly to some SwiftUI element (eg they need aggregation or whatever). They don’t belong in the model, hence MVVM makes a lot of sense to me too.

My current project doesn’t really need charts, but I am glad to see charting functionality built in. It’s long overdue, IMO.

As for the bolded bit, that’s why I started moving to MVVM in my current project. I still have views that take NSManagedObjects as ObservableObjects, but they are becoming rarer as the views want to display things that aren’t just straight pulled from the model. But when I’m at around 22kloc right now, it’s not surprising that just binding to the model isn’t good enough anymore.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
I do recommend the talk as it describes the nature of POP better than I ever could. But the Swift runtime is built on POP, as is SwiftUI. While I don’t think it is the savior of programming, it has let me kick most inheritance to the curb in favor of conformance, and for me, being able to compose conformances leads to more testable code. So I favor it over OOP style inheritance. NSObject and NSManagedObject are about all I subclass from these days.
Finally watched it yesterday. It was quite enjoyable. I'll watch the other related talks next (Protocol and Value Oriented Programming in UIKit Apps), but I think I know by this point that the problem I have is not with POP itself, but rather how people are using it to add unnecessary abstractions to some parts of the code. Just like no one created a superclass for every class just in case you might want an alternative implementation in the future, protocols should be created/extracted as required, not upfront as part of the creation of a class, IMHO.

In any case, the talk had some *very* cool things. I liked most how it significantly extends the type information available to the compiler vs using inheritance in some common cases.

One reason I like MVVM is that the VM layer in SwiftUI can be as thin or as thick as needed, making it very pragmatic. Perhaps you can get away with just interacting with the model directly because it’s using NSManagedObject. Maybe you have a bunch of services and so you want that separation. Do what makes the most sense at the time, IMO.

I tend to favor the approach of adding complexity as needed, so Apple’s approach here makes some sense to me.
Problem is, it's very common for developers to consider views interacting directly with models as a code smell. Having Views be able to access "more than what they need" (i.e, an entire model instead of just the parts relevant to the view) is also frowned upon. I had been adhering to that idea until very recently. However, seeing how Apple architected the Fruta sample app (injecting the model to all subviews) has made me question that assumption. Creating a single, separate ViewModel for each view can create several 'sources of truth' if you're not careful and start passing value types around between ViewModels.

A good example of this just happened to me at work: I was implementing an image-editing workflow spanning several screens (a grid view with all the images -> a detail view of the image -> an image editor screen). After saving the edited image on the last screen, I noticed that previous views in the navigation hierarchy still showed the old, non-edited image. That's because each view had its own view model, and those were each acquiring a copy of the image. This should (ideally) not happen in SwiftUI. Having a single source of truth is an important goal of the framework.

The main problem of this approach is that the environment-shared model can become too big. I haven't found (yet) a good solution to that problem, that doesn't end up creating alternative sources of truth.

SwiftUI has been good for this - I’m very excited by the new Chart capability! - but earlier I found myself building things into the model purely for their presentational value. These are things that don’t translate directly to some SwiftUI element (eg they need aggregation or whatever). They don’t belong in the model, hence MVVM makes a lot of sense to me too.
The new charts are great. I haven't delved too deep into that yet, but I plan to replace some crude graphs I had in my side-project app with Swift charts as soon as I finish what I'm building now. I didn't have much faith in them, seeing how Apple's own graphs in the Health app are mediocre at best, but it seems to be a powerful framework after all.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Finally watched it yesterday. It was quite enjoyable. I'll watch the other related talks next (Protocol and Value Oriented Programming in UIKit Apps), but I think I know by this point that the problem I have is not with POP itself, but rather how people are using it to add unnecessary abstractions to some parts of the code. Just like no one created a superclass for every class just in case you might want an alternative implementation in the future, protocols should be created/extracted as required, not upfront as part of the creation of a class, IMHO.

In any case, the talk had some *very* cool things. I liked most how it significantly extends the type information available to the compiler vs using inheritance in some common cases.

I think TDD proponents might disagree with the bolded statement, in the sense that creating a protocol that represents a unit under test is the first step for being able to isolate that unit from others when testing. Since it’s pretty common that unit tests test objects in isolation under OOP, that unit tests in POP also test objects. So getting that level of duplication seems like a side effect of decoupling in a language like Swift.

I’ve had to use the technique to cleave off dependencies to Network and AVFoundation while testing. Extract a protocol from the Apple type I depend on, and then feed a mock implementation that unit tests can manipulate. But it does mean I have stuff like this in production code:

Swift:
protocol AVPlayerProtocol {
    /* Stuff I care about */
}

extension AVPlayer: AVPlayerProtocol {}

final class MyType {
    private var avplayer: AVPlayerProtocol
    init(avplayer: AVPlayerProtocol) { /* ... */ }
}

Problem is, it's very common for developers to consider views interacting directly with models as a code smell. Having Views be able to access "more than what they need" (i.e, an entire model instead of just the parts relevant to the view) is also frowned upon. I had been adhering to that idea until very recently. However, seeing how Apple architected the Fruta sample app (injecting the model to all subviews) has made me question that assumption.

Usually when I have the model directly accessible to the view, it is a small subset and used read-only. So think of things like cells in collection views and the like. And it’s usually some sort of CoreData object, so I’m not copying value types around.

Creating a single, separate ViewModel for each view can create several 'sources of truth' if you're not careful and start passing value types around between ViewModels.

A good example of this just happened to me at work: I was implementing an image-editing workflow spanning several screens (a grid view with all the images -> a detail view of the image -> an image editor screen). After saving the edited image on the last screen, I noticed that previous views in the navigation hierarchy still showed the old, non-edited image. That's because each view had its own view model, and those were each acquiring a copy of the image. This should (ideally) not happen in SwiftUI. Having a single source of truth is an important goal of the framework.

This scenario is why Combine is a developer-facing framework, IMO. You are right that there should be a single source of truth, and that should live in the model for cases like these. But Combine exists explicitly to make it possible for things like ViewModels to subscribe to the bits of the model that could update underneath it in places where SwiftUI as a framework shouldn’t be used to create these sort of bindings.

The main problem of this approach is that the environment-shared model can become too big. I haven't found (yet) a good solution to that problem, that doesn't end up creating alternative sources of truth.

Redux seems to handle this sort of thing fine by creating a store that contains all of the app’s state, and letting segments of the app subscribe just to the parts it actually cares about.

Creating one big ObservableObject isn’t realistic, but you could easily bring together a cluster of observables into a larger store and play around a bit with letting the store inject it’s children into the environment. Views could then “subscribe” to just the components they need out of the cluster rather than everything.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Here's a lesson for the day related to Concurrency and Combine: Race conditions are still the enemy. I have two actors. Actor one holds onto network state and updates itself based on NWPathMonitor, exposing a publisher that can be subscribed to. Actor two needs to update its own internal state based on the network state, and check to see if a network host is reachable via different paths. So it subscribes to the publisher provided by the first.

My first attempt used a non isolated sink for the publisher, which then spawned a task to update the second actor. Unfortunately, in cases where values are updated quickly, ordering cannot be guaranteed when creating these tasks. So I'd get all sorts of breaks.

Instead, I have the second actor create a child task and subscribe to the publisher within that task using ".values". At least this way, you can guarantee ordering within the child task, and since is now in a concurrency context, you can ensure the actor receives the correct ordering as well.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
I think TDD proponents might disagree with the bolded statement, in the sense that creating a protocol that represents a unit under test is the first step for being able to isolate that unit from others when testing. Since it’s pretty common that unit tests test objects in isolation under OOP, that unit tests in POP also test objects. So getting that level of duplication seems like a side effect of decoupling in a language like Swift.
Oh but TDD *is* a genuine need for an alternative implementation. I don't mind that. I just argue that protocol extraction should be done immediately before adding those tests, and not in hope of maybe adding tests in the future.

I’ve had to use the technique to cleave off dependencies to Network and AVFoundation while testing. Extract a protocol from the Apple type I depend on, and then feed a mock implementation that unit tests can manipulate. But it does mean I have stuff like this in production code:

Swift:
protocol AVPlayerProtocol {
    /* Stuff I care about */
}

extension AVPlayer: AVPlayerProtocol {}

final class MyType {
    private var avplayer: AVPlayerProtocol
    init(avplayer: AVPlayerProtocol) { /* ... */ }
}
I think that's perfectly fine code. Apple does almost exactly that in the POP talk to make the Renderer class they create testable. Again, no problem as long as the abstraction is actually used :p

Usually when I have the model directly accessible to the view, it is a small subset and used read-only. So think of things like cells in collection views and the like. And it’s usually some sort of CoreData object, so I’m not copying value types around.
Hmm. How do you make only a small subset of the model available to the view?

This scenario is why Combine is a developer-facing framework, IMO. You are right that there should be a single source of truth, and that should live in the model for cases like these. But Combine exists explicitly to make it possible for things like ViewModels to subscribe to the bits of the model that could update underneath it in places where SwiftUI as a framework shouldn’t be used to create these sort of bindings.

Redux seems to handle this sort of thing fine by creating a store that contains all of the app’s state, and letting segments of the app subscribe just to the parts it actually cares about.

Creating one big ObservableObject isn’t realistic, but you could easily bring together a cluster of observables into a larger store and play around a bit with letting the store inject it’s children into the environment. Views could then “subscribe” to just the components they need out of the cluster rather than everything.
Yeah, Combine is a good match for this use case. For changes that affect the views, though, changes in the model listened in the view model would have to be re-published to a @Published property so the view could update... I'm not convinced.

I thought about breaking up my ObservableObject into smaller objects. So far that's my preferred route, but I'll have to sit down and carefully examine the options. I'm planning a refactor of my side-project app into two separate modules when I finish the updates to the renderer I've been working on. Basically I will try to extract the renderer itself to a different module, so it can be used like one would use a SceneKit or ARKit view, to completely decouple the renderer from the app itself. That should also allow me to practice with some of the things mentioned in this thread (better use protocols, handle the massive-observableObject problem, maybe even add tests). And DocC. I want to try DocC.

My first attempt used a non isolated sink for the publisher, which then spawned a task to update the second actor. Unfortunately, in cases where values are updated quickly, ordering cannot be guaranteed when creating these tasks. So I'd get all sorts of breaks.
Ooh. Good catch. I didn't know that could happen.
 
Last edited:

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Oh but TDD *is* a genuine need for an alternative implementation. I don't mind that. I just argue that protocol extraction should be done immediately before adding those tests, and not in hope of maybe adding tests in the future.

I think that's perfectly fine code. Apple does almost exactly that in the POP talk to make the Renderer class they create testable. Again, no problem as long as the abstraction is actually used :p

Yeah, I think my confusion is that in my mind, TDD means you always have that protocol (since you are writing the tests against an unimplemented protocol before you write the implementation). So it was a little hard for me to try to guess where you drew the line. Sounds like we aren’t that different in mindset here. My mindset is that POP effectively is steering folks towards TDD-like behaviors, but if they aren’t leveraging it, it is a shame and a waste of energy.

Hmm. How do you make only a small subset of the model available to the view?

There’s not much enforcement in the language itself, for sure. This is more a “discipline” thing in my case.

But for the most part, my views don’t use @EnvironmentObject often. Instead it all gets passed in. So I’m either passing in a View Model or a Core Data object that the view represents most of the time. The type of project I am working on makes this a natural approach. So a view isn’t going to be wandering around the object graph and poking at things. Code that applies transformations to the object graph doesn’t live within it, so a view model is effectively mandatory in my architecture for any UI that wants to make changes to the model, to get at the parts of the editor workflow that doesn’t exist in the object graph.

Yeah, Combine is a good match for this use case. For changes that affect the views, though, changes in the model listened in the view model would have to be re-published to a @Published property so the view could update... I'm not convinced.

True, but this is why there’s an .assign() that explicitly takes the @Published property’s publisher, and doesn’t even need you to store anything in an AnyCancellable. So the update binding from the model is literally a line of code in an initializer.

My view models use this approach for binding individual properties to the Core Data object backing the view model. I came to this approach partly because of your own suggestion that with Core Data, you should just deal with the fact that properties are optional. Which this lets me do rather elegantly without leaking things like the default displayable value into the model. Below is something similar to what I have setup as a view model for items that are displayed in List/LazyVGrid. It also has the neat side effect that I can use this to effectively type-erase dataObject if I need it to, and support some rather different Core Data entities if I want, rather than making those entities conform to a protocol that starts handling things the View Model should really own, IMO.

Swift:
class CollectionItemViewModel: ObservableObject {
    @Published var title: String = "<<TITLE>>"
    @Published var subtitle: String = "<<SUBTITLE>>"
    @Published var thumbnail: UIImage? // Don’t actually do this, especially in LazyVGrid. 
    
    init(_ dataObject: MyCoreDataObject) {
        // KVO Publishers
        dataObject.publisher(for: \.albumName).compactMap({$0}).assign(to: &$title)
        dataObject.publisher(for: \.albumArtist).compactMap({$0}).assign(to: &$subtitle)
        // Custom Publisher
        dataObject.lazyThumbnailPublisher().assign(to: &$thumbnail)
    }
}

FYI, using this pattern also means you can subscribe UI to actors without having to manage the isolation contexts except when hooking up the subscriber. So I can have shared state like a download queue live inside an actor, but also have the ability to update the UI with the queue’s state from a ViewModel using very little code. Literally just need to wrap the subscription code in a Task and add await in the appropriate spot. The alternative is spinning up a long-lived Task that watches an AsyncSequence of updates, and managing that yourself.

I thought about breaking up my ObservableObject into smaller objects. So far that's my preferred route, but I'll have to sit down and carefully examine the options. I'm planning a refactor of my side-project app into two separate modules when I finish the updates to the renderer I've been working on. Basically I will try to extract the renderer itself to a different module, so it can be used like one would use a SceneKit or ARKit view, to completely decouple the renderer from the app itself. That should also allow me to practice with some of the things mentioned in this thread (better use protocols, handle the massive-observableObject problem, maybe even add tests). And DocC. I want to try DocC.

That’s generally what I’m doing as well, since it means that at least in my cases, I am passing one specific object to a view, and environmentObject gets regulated to more “global” scope objects that actually need to be accessed from random parts of the API. Things like the download queue I mentioned above.

Ooh. Good catch. I didn't know that could happen.

Using the actor playground we talked about earlier, I did mention it was non-deterministic behavior when it came to ordering, because of things executing concurrently. I just didn’t actually take my own learnings into account. Whoops.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
My mindset is that POP effectively is steering folks towards TDD-like behaviors, but if they aren’t leveraging it, it is a shame and a waste of energy.
Yep, that was basically my point.

But for the most part, my views don’t use @EnvironmentObject often. Instead it all gets passed in. So I’m either passing in a View Model or a Core Data object that the view represents most of the time. The type of project I am working on makes this a natural approach. So a view isn’t going to be wandering around the object graph and poking at things. Code that applies transformations to the object graph doesn’t live within it, so a view model is effectively mandatory in my architecture for any UI that wants to make changes to the model, to get at the parts of the editor workflow that doesn’t exist in the object graph.
I think that's going to depend a lot on the kind of app you're writing. For instance, my main side project is basically a Metal-backed view with a sidebar with a lot of buttons and switches for options and the like. I think using @EnvironmentObject there makes a lot of sense, because all the subviews need access to the shared state, and passing it around in the initializer to tens of subviews would be needlessly complicated.

On the other hand, if my app was navigation heavy, I can see @EnvironmentObject being much less useful. Also, I don't like that it can cause crash at runtime if you forget to inject it, I wish it could be ensured at compile time.

True, but this is why there’s an .assign() that explicitly takes the @Published property’s publisher, and doesn’t even need you to store anything in an AnyCancellable. So the update binding from the model is literally a line of code in an initializer.

My view models use this approach for binding individual properties to the Core Data object backing the view model. I came to this approach partly because of your own suggestion that with Core Data, you should just deal with the fact that properties are optional. Which this lets me do rather elegantly without leaking things like the default displayable value into the model. Below is something similar to what I have setup as a view model for items that are displayed in List/LazyVGrid. It also has the neat side effect that I can use this to effectively type-erase dataObject if I need it to, and support some rather different Core Data entities if I want, rather than making those entities conform to a protocol that starts handling things the View Model should really own, IMO.

Swift:
class CollectionItemViewModel: ObservableObject {
    @Published var title: String = "<<TITLE>>"
    @Published var subtitle: String = "<<SUBTITLE>>"
    @Published var thumbnail: UIImage? // Don’t actually do this, especially in LazyVGrid.
   
    init(_ dataObject: MyCoreDataObject) {
        // KVO Publishers
        dataObject.publisher(for: \.albumName).compactMap({$0}).assign(to: &$title)
        dataObject.publisher(for: \.albumArtist).compactMap({$0}).assign(to: &$subtitle)
        // Custom Publisher
        dataObject.lazyThumbnailPublisher().assign(to: &$thumbnail)
    }
}
This is... actually super nice. Damn, you're gonna make me consider using Combine more often after all. This has basically everything I want: the initializer ensures the model is present at compile time, it keeps the single source of truth (without having to worry about storing AnyCancellables), it's still purely reactive (no worries about stale data), and as you point out: the default displayed value is not present in the Model. Super nice. I can't really think of any drawbacks right now.

Those last couple weeks I've been thinking that keeping a single source of truth in SwiftUI is probable more important than any other traditional code writing guideline. Sometimes people obsess too much about things like separation of concerns IMHO, and it often leads to several sources of truth that may or may not be in sync. If I were to become super strict with one single thing, right now I think it'd be the single source of truth. SwiftUI does behave like magic at times if you ensure that (that and being careful with view identities).
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Yep, that was basically my point.

Yeah, no worries. Please don't feel like I'm trying to prod at you here. I'm mostly just voicing my thoughts more to ensure I communicate effectively and avoid ambiguity.

I think that's going to depend a lot on the kind of app you're writing. For instance, my main side project is basically a Metal-backed view with a sidebar with a lot of buttons and switches for options and the like. I think using @EnvironmentObject there makes a lot of sense, because all the subviews need access to the shared state, and passing it around in the initializer to tens of subviews would be needlessly complicated.

On the other hand, if my app was navigation heavy, I can see @EnvironmentObject being much less useful. Also, I don't like that it can cause crash at runtime if you forget to inject it, I wish it could be ensured at compile time.

Yeah, agreed. So it really depends on how you can break down your view hierarchy and navigation, IMO.

This is... actually super nice. Damn, you're gonna make me consider using Combine more often after all. This has basically everything I want: the initializer ensures the model is present at compile time, it keeps the single source of truth (without having to worry about storing AnyCancellables), it's still purely reactive (no worries about stale data), and as you point out: the default displayed value is not present in the Model. Super nice. I can't really think of any drawbacks right now.

Those last couple weeks I've been thinking that keeping a single source of truth in SwiftUI is probable more important than any other traditional code writing guideline. Sometimes people obsess too much about things like separation of concerns IMHO, and it often leads to several sources of truth that may or may not be in sync. If I were to become super strict with one single thing, right now I think it'd be the single source of truth. SwiftUI does behave like magic at times if you ensure that (that and being careful with view identities).

Thanks for the kind words. As I've been moving to Combine for non-Core Data parts of my model, I've even been creating a couple property wrappers to make this a bit easier as well. So I can more easily have model objects that expose state that can be subscribed to. I'm surprised Apple doesn't already offer something like this as either part of @Published or another property wrapper. But they are pretty easy to create yourself.

Swift:
actor NetworkMonitor {
    // Wrapper for CurrentValueSubject<T, Never>.
    // $state projected value is a ValueSubject<T>.Publisher (AnyPublisher<T, Never>) to avoid leaking out the setter.
    @ValueSubject var state: NetworkState = .init()
   
    // Needed if wanting a protocol conformance sadly, unless I'm missing something.
    var statePublisher: ValueSubject<NetworkState>.Publisher { $state }
   
    // This requires isolation access
    private func updateNetworkState(...) {
        /* ... */
        state = .init(/* New State */)
    }
}

// Actor publishers can get a little tricker in view models, but you can still subscribe to them
// in a way that is surprisingly readable if you keep it simple.
await networkMonitor.$state.receive(on: .main).map(NetworkViewState.init).assign(to: &$networkViewState)
// NetworkViewState in this example would be some simple struct that might contain things like a label icon and title or something.

One trick I ran across for when you need to use sink() and the like is to use a result builder to let you "collect" AnyCancellable into a collection like a set. I use this when I do need to pass the value to a function for processing or to trigger other side effects.

Swift:
private var cancellables: Set<AnyCancellable> = Set()

cancellables.collect {
    // Using sink to call functions when you need side effects.
    dataObject.publisher(for: \.albumTitle).sink(receiveValue: updateTitle)
    dataObject.publisher(for: \.albumArtist).sink(receiveValue: updateArtist)
}

func updateTitle(title: String?) { ... }
func updateArtist(artist: String?) { ... }

And I agree with the source of truth aspect. There's even a whole philosophy (one followed by Redux/React projects) around making a single source of truth: https://github.com/pointfreeco/swift-composable-architecture

The thing I don't like about how Redux has been used in projects I've worked on though is that the store is usually one thing in the code. So the store's data objects all live in one spot in the code. Reducers sit in another. Actions in yet another. So trying to understand the flow of a part of the app, such as authentication, becomes tiresome as there isn't just code saying: Here's the authentication state and it's reducers/actions. It makes the code a lot harder to learn and digest as a new engineer to a team. I much prefer to be able to see whole verticals of the model more easily.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
Yeah, no worries. Please don't feel like I'm trying to prod at you here. I'm mostly just voicing my thoughts more to ensure I communicate effectively and avoid ambiguity.
No need to worry :) I was doing the same.

I'd like to add that I'm learning a lot from this conversation too. It helps that I can put a lot of what we talked about on this thread to practice at work (we target iOS 15+). There have been a lot of moments when I stumbled upon some piece of code and remembered something from this thread.

I'm surprised Apple doesn't already offer something like this as either part of @Published or another property wrapper. But they are pretty easy to create yourself.
Could it be that some of this 'missing parts' of the API is just Apple trying to steer developers to certain architectures / out of some patterns? Kind of an extreme example, but I remember people complaining about how you can't create a ViewModel in a parent view and set it as a @StateObject in a child view elegantly (see this SO post). After watching some of the SwiftUI talks (specially, Demystify SwiftUI), I feel like trying to do something like that is just fighting the system (if you want SwiftUI to persist that @StateObject between view inits, you shouldn't also want to initialize it every time the view body of the parent is executed).

One trick I ran across for when you need to use sink() and the like is to use a result builder to let you "collect" AnyCancellable into a collection like a set. I use this when I do need to pass the value to a function for processing or to trigger other side effects.

Swift:
private var cancellables: Set<AnyCancellable> = Set()

cancellables.collect {
    // Using sink to call functions when you need side effects.
    dataObject.publisher(for: \.albumTitle).sink(receiveValue: updateTitle)
    dataObject.publisher(for: \.albumArtist).sink(receiveValue: updateArtist)
}

func updateTitle(title: String?) { ... }
func updateArtist(artist: String?) { ... }
This is very nice too.

The thing I don't like about how Redux has been used in projects I've worked on though is that the store is usually one thing in the code. So the store's data objects all live in one spot in the code. Reducers sit in another. Actions in yet another. So trying to understand the flow of a part of the app, such as authentication, becomes tiresome as there isn't just code saying: Here's the authentication state and it's reducers/actions. It makes the code a lot harder to learn and digest as a new engineer to a team. I much prefer to be able to see whole verticals of the model more easily.
YES! That's the exact same feeling I had with VIPER codebases. Everything is very compartimentalized and that should be a positive thing, but trying to understand the app flow as a first-timer is an absolute nightmare because you have to hunt down every affected piece of code throughout all the codebase. If the app also had network calls or any other completion handler heavy user, code could become almost undecipherable. Additionally, as the number of affected files grows, it becomes harder to keep everything inside your 'mental model' of the code.
 
Top Bottom
1 2