Swift / Apple Development Chat

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
I wish this was more common. I've found that it's much more difficult to convince other developers that things should be simpler than it is to convince them that things should be more complex. Two weeks ago, I spent several hours trying to fight another developer that had devised an incredibly overengineered architecture for the models: they wanted models to be immutable (okay, I guess), but instead of using a struct (and letting the compiler figure everything else out, they had implemented every model as a class where every single property was declared let, plus a builder pattern on top of it.

It depends a lot on the team.

I always get a bit lost with these things. I don't know much about static vs dynamic linking, other than the very basics. I've never had to worry about that (yet?). I'll watch the talk.

A lot of this boils down to two things: app launch time, and app binary size. Being able to pre-build and still keep size and launch time down adds up quick for complex apps like Photoshop, Office, etc. It can make the difference between an app you can (or are willing to) download over cellular, vs one you can’t/won’t.
 

casperes1996

Power User
Posts
185
Reaction score
171
RE Macros:

The Swift macros are a lot like the declarative and procedural macros you find in something like Rust. They are implemented differently and macro authors need to do more to get them to work than in Rust, but with the added flexibility of not needing to distribute them in source form necessarily. This also all harkens back to Lisp macros; The original code in, code out macro system.
Macros are not just expansions. They (can) take in Swift and output Swift by modifying the AST (Abstract Syntax Tree). Or I should say, they take in parsed source code. Now I'm still not entirely sure, given the macro declarations' requirements, if you can implement full DSLs with this yet like you can in Rust. It seems you may need to be able to express the inputs and outputs of a Macro with the regular Swift type system, so the #stringify macro says its input is Int when in reality it operates on the Expression. But if you can just say the input is a token stream, you can get Macros that statically check usage of HTML to produce an HTML struct or something like that. Imagine
Code:
var validHtml = #html(<p> This could produce an HTML struct containing this paragraph and further tools for adding and building a document </p>)
var invalidHtml = #html(<p> This is an unclosed paragraph and will yell at me at compile time)
These would not just be strings passed into the macro, but tokens. Rust macros exist that allow this. Rich compiler diagnostics can be produced by macros.

And the ability to debug it isn't too hurt by this. You can unfold the macros, always. Even closed source macros cannot produce closed source outputs because their expansions live in your code.


When I asked what on earth was that, basically rolling out their own flavor of value semantics, I discovered that they had made it like that to enforce models being passed around by reference instead of by value, as they considered the cost of copying (the stack size of) structs unacceptable. I couldn't convince them that this was all unnecessary (and actually worse in a few more important things: it's non-Sendable as it's not declared final, it force-unwraps optionals...). They took great pride in what they had architected.
Well, now they can add ~Copyable to the struct and avoid that. With Rust-like ownership and borrow constructions.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Well, now they can add ~Copyable to the struct and avoid that. With Rust-like ownership and borrow constructions.
Theoretically, yes, but then you need explicitly use borrowing/consuming for every function that handles those types. If you do it for all application models, it will become unwieldy. From what I understood, the ownership and noncopyable enhancements to the language came from a need to improve Swift's performance predictability: you don't get unexpected copies of variables. As such, it does make sense to use them in things like tight loops or things that are in a hot path, but they're not designed to be the default setting. Besides, and while this will change in the future, non-copyable types as of now can't be used with Swift Generics nor conform to protocols. You can't even do var someVar: NonCopyableType? because that's syntactic sugar for Optional<NonCopyableType> and, as I said, generics are not yet supported.

Realistically, you don't need to use any of that by default for a typical iOS app. You may even find out that true value types (like structs) are more performant than the "optimized", reference-only type, even for medium-sized models. There are good WWDC talks on this (both Understanding Swift Performance and Optimizing Swift Performance). In fact, the issue with big struct types is not (usually) the stack size as they feared, but rather the increased Automatic Reference Counting (ARC) traffic you get (for each reference-counted variable you have in a struct, like Array or String, you have to do a retain cycle, while if your model is class based you retain the entire class instead of the individual properties). The copying of the stack contents themselves is negligible unless you're building an exotic data structure.

For some big models you may see some benefit from implementing Copy on Write (CoW), like the Swift Standard Library does for String or Array, which would have also ensured that the models were always passed by reference, like Swift maintainers suggest here. This would have kept the API of the model unchanged for external users, like a regular struct, while potentially reducing ARC traffic. But this is something that should be done IF the model has poor performance with the off-the-shelf solution (a regular struct), AND profiling/benchmarking shows that it's beneficial to adopt CoW. Implementing it by default in all models is a premature optimization that might not even turn out to be net-positive in performance (small structs are really fast, and you'd be adding another level of indirection (+ more ARC) by using a class instead).
Plus the compiler is smart enough to know not to copy entire structs if some properties are not needed (through function specialization), or to modify them in place if it can check at compile time that no other piece of code is using the same struct. Generally speaking, it's not something you need to worry about, IMHO.
 

casperes1996

Power User
Posts
185
Reaction score
171
Theoretically, yes, but then you need explicitly use borrowing/consuming for every function that handles those types. If you do it for all application models, it will become unwieldy. From what I understood, the ownership and noncopyable enhancements to the language came from a need to improve Swift's performance predictability: you don't get unexpected copies of variables. As such, it does make sense to use them in things like tight loops or things that are in a hot path, but they're not designed to be the default setting. Besides, and while this will change in the future, non-copyable types as of now can't be used with Swift Generics nor conform to protocols. You can't even do var someVar: NonCopyableType? because that's syntactic sugar for Optional<NonCopyableType> and, as I said, generics are not yet supported.
Pretty sure bowstring is default and you only explicitly need to state consuming functions, but otherwise yes, agree with everything you wrote.
Plus the compiler is smart enough to know not to copy entire structs if some properties are not needed (through function specialization), or to modify them in place if it can check at compile time that no other piece of code is using the same struct. Generally speaking, it's not something you need to worry about, IMHO.
Yeah the compiler is really smart about all this and non-mutating, non-shared structs sometimes just automatically get passed by reference when it is guaranteed to maintain value semantics. It's pretty cool
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Pretty sure bowstring is default and you only explicitly need to state consuming functions, but otherwise yes, agree with everything you wrote.
You’re right that, by default, all functions have borrowing parameters (except initializers, which default to consuming). I thought there would still be many situations where you’d need to add explicit keywords, but I now think I overestimated that a bit. You would need to create explicitly borrowing inits for some objects, and I’m not sure if you’d need anything else for returns (as they’re consuming), but most places may not need changes due to the implicit behavior being already what you want.
 
Last edited:

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
Some limitations or other things I've been learning while playing with SwiftData so far:

- If you want to do background work, you do it with an actor conforming to ModelActor. The actor should create/own a DefaultModelExecutor created from a new ModelContext, then do your manipulations within the actor code to ensure isolation. This was not properly documented.

- Predicate macros provide unhelpful errors if you do something it doesn't like, such as implicitly capturing self. So if I want to do a fetch of an entity and get all children of a parent, then you have to assign the PersistentIdentifier to a local variable first and then capture that. The Predicate macro tends to generate code that doesn't compile otherwise.

- There's nothing like NSFetchedResultsController at present. Without this, there's even fewer options to offer different sorting options in SwiftUI. I guess I could declare multiple @Query properties and then use a state variable to decide which query to present, but that seems a tad wasteful as the size of the results grows. I could also do my own fetching with some sort of @Observable or @State, I suppose, but the fetches would be more frequent, I believe.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Some limitations or other things I've been learning while playing with SwiftData so far:
Funnily enough, I was also writing about SwiftData on here when I reloaded the page and saw your post :p

- If you want to do background work, you do it with an actor conforming to ModelActor. The actor should create/own a DefaultModelExecutor created from a new ModelContext, then do your manipulations within the actor code to ensure isolation. This was not properly documented.
A lot of things are still missing documentation in SwiftData. I'm under the impression that it's by far the roughest of the new APIs. Little documentation, several painful bugs... (i.e, #Preview is still broken in Xcode 15 beta 2 for views using SwiftData objects). For example (related to the documentation) I had stumbled upon changedObjectsArray, and it looks like it could be useful for one of the problems I was having, but it's not documented.

- Predicate macros provide unhelpful errors if you do something it doesn't like, such as implicitly capturing self. So if I want to do a fetch of an entity and get all children of a parent, then you have to assign the PersistentIdentifier to a local variable first and then capture that. The Predicate macro tends to generate code that doesn't compile otherwise.
Almost every error I got so far related to CoreData was unhelpful. Most of the time I only got [MyClass] does not conform to PersistentModel regardless of what the problem was.

To me, SwiftData looks like a promising preview of what their vision for the future of the persistency layer is, but it's still *very* rough as far as I could see. I still have to watch some talks, though, so I may not be seeing the full picture yet.

My biggest roadblock so far has been that, unlike with CoreData, it seems that you can't easily store Data objects in SwiftData (as Data is not Codable by default). Worse even, adding a Data property to a @Model does not raise a warning or error, the property is simply not persisted. That's a weird default option, I definitely would have expected some kind of warning. I could trivially convert Data to String and make it Codable that way, but it feels like a hacky and dirty solution for what I thought was a common scenario (wanting to persist images using SwiftData).

- There's nothing like NSFetchedResultsController at present. Without this, there's even fewer options to offer different sorting options in SwiftUI. I guess I could declare multiple @Query properties and then use a state variable to decide which query to present, but that seems a tad wasteful as the size of the results grows. I could also do my own fetching with some sort of @Observable or @State, I suppose, but the fetches would be more frequent, I believe.
Yup, I had a similar issue. @Query is very elegant if it covers the scope of what you want to do, but there isn't a lot of flexibility there. So far I'm happy to see how well the animations work, even with queries. If you use withAnimation { ... } to modify a SwiftData property used in several different views, I think all of them get animations too. That's cool.
 
Last edited:

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
A lot of things are still missing documentation in SwiftData. I'm under the impression that it's by far the roughest of the new APIs. Little documentation, several painful bugs... (i.e, #Preview is still broken in Xcode 15 beta 2 for views using SwiftData objects). For example (related to the documentation) I had stumbled upon changedObjectsArray, and it looks like it could be useful for one of the problems I was having, but it's not documented.

My assumption is that changedObjectsArray is akin to something like updatedObjects or some mix of that with insertedObjects/deletedObjects on the underlying object context.

My biggest roadblock so far has been that, unlike with CoreData, it seems that you can't easily store Data objects in SwiftData (as Data is not Codable by default). Worse even, adding a Data property to a @Model does not raise a warning or error, the property is simply not persisted. That's a weird default option, I definitely would have expected some kind of warning. I could trivially convert Data to String and make it Codable that way, but it feels like a hacky and dirty solution for what I thought was a common scenario (wanting to persist images using SwiftData).

And @Attribute(.externalStorage) doesn't help?

My issue is that enums are allowed in the @Model macro, but then asserts fire when trying to fetch entities that use enums from the context. Trying to create a little sync code using ModelActor to play with how to do updates in the background. That's just a bad experience...
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
And @Attribute(.externalStorage) doesn't help?
Weird. When I went to try this, I discovered what was triggering the issue. It seems like SwiftData only persists non-private variables? Just changing this:
Swift:
private var imageData: Data?
To this:
Swift:
var imageData: Data?
Solved my issue. But I want the property to be private :mad:
The thing is, I want to enforce that images are always read through the computed var image:
Swift:
var image: Image? {
    get {
        guard let imageData else { return nil }
        guard let uiImage = UIImage(data: imageData) else { return nil }
        return Image(uiImage: uiImage)
    }
}

I'm insisting on keeping imageData private because in my experience, it's easy to forget that image exists and end up writing the code to convert Data to Image in different places throughout the code (also, conceptually the fact that the image is stored using its Data is an implementation detail).

I wonder if this is a bug or the intended behaviour. In any case, persisting raw Data seems to be possible, but it's definitely not documented anywhere, documentation only mentions codable types.

My issue is that enums are allowed in the @Model macro, but then asserts fire when trying to fetch entities that use enums from the context. Trying to create a little sync code using ModelActor to play with how to do updates in the background. That's just a bad experience...
Hmm, codable enums should work. Maybe it's an Xcode beta problem? I remember reading that someone else was having issues with enums with Xcode beta 2.

Doing it like so has worked for me:

#Preview { MainActor.assumeIsolated { TestView() .modelContainer(model) } }
Oh, this works! :)
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
Weird. When I went to try this, I discovered what was triggering the issue. It seems like SwiftData only persists non-private variables? Just changing this:
Swift:
private var imageData: Data?
To this:
Swift:
var imageData: Data?
Solved my issue. But I want the property to be private :mad:
The thing is, I want to enforce that images are always read through the computed var image:
Swift:
var image: Image? {
    get {
        guard let imageData else { return nil }
        guard let uiImage = UIImage(data: imageData) else { return nil }
        return Image(uiImage: uiImage)
    }
}

I'm insisting on keeping imageData private because in my experience, it's easy to forget that image exists and end up writing the code to convert Data to Image in different places throughout the code (also, conceptually the fact that the image is stored using its Data is an implementation detail).

I wonder if this is a bug or the intended behaviour. In any case, persisting raw Data seems to be possible, but it's definitely not documented anywhere, documentation only mentions codable types.

Yeah, I think I'm going to bail on SwiftData for the moment. But at least in a couple years things will be good to go. Instead I'm probably going to focus more on cleaning up my approach to make it less "ViewModel" like.

Hmm, codable enums should work. Maybe it's an Xcode beta problem? I remember reading that someone else was having issues with enums with Xcode beta 2.

As I said, it works if I'm simply defining them, but if I create my own background fetch, the fetch is what asserts. So anyone doing stuff using @Query, it seems that doesn't necessarily trigger the assert.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
As I said, it works if I'm simply defining them, but if I create my own background fetch, the fetch is what asserts. So anyone doing stuff using @Query, it seems that doesn't necessarily trigger the assert.
Oh, I misunderstood. I thought even the basic setup wasn't working for you. In fact, I went to try it out and I can't even get the basic functionality to work, I found out that:
  • @Models with codable enums without a explicit raw type fail for me (no crash, but entities are not fetched, console outputs CoreData: error: Row (pk = 1) for entity 'FooEntity' is missing mandatory text data for property 'fooCaseA'.
  • @Models with codable enums with a explicit raw type String crash (this is a known issue in the release notes).
  • @Models with optional codable enums with a explicit Int raw type don't crash, but SwiftData always returns the first case of the enum, regardless of the value you input (even if you set it to nil).
So in any case I'm not exactly surprised to learn that more advanced features like background fetch also have problems. Hopefully they'll get the basics working before september, but I don't remember any other instance of an API starting this buggy.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
Yeah, I think I'm going to bail on SwiftData for the moment. But at least in a couple years things will be good to go. Instead I'm probably going to focus more on cleaning up my approach to make it less "ViewModel" like.

On this front, I found out today that you can add "(any Foo)" environment keys to SwiftUI. Brilliant. This will make injecting micro services that aren't themselves observable a little cleaner.

So in any case I'm not exactly surprised to learn that more advanced features like background fetch also have problems. Hopefully they'll get the basics working before september, but I don't remember any other instance of an API starting this buggy.

It's also a pretty big bite to take and chew. I'm more surprised just how much "it just works" they tried to cram in, and that's probably a good chunk of the buggy behavior.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
A random thought I had is why hasn't Apple done the SwiftUI treatment for CarPlay yet? It's more straight-forward than anything SwiftUI or SwiftData is trying to do. Probably just demand/resources, but seems a bit odd. I wrote up something simple, but the end result is that it can't hold any state or subscribe to Combine publishers, making it a limited approach.

I'm starting to play around with building something more fleshed out. It's not quite as nice as SwiftUI where it can very rapidly build out complex UI, but it does start seeming quite reactive, and does support subscribing to publishers and only having tasks/publishers active when a template is pushed to the screen:

Swift:
@CUScreenBuilder
func makeScreen() -> CUScreenTemplate {
    CUList {
        CUListSection {
            for title in ["First", "Second", "Third"] {
                CUListItem(text: title)
                    .task(perform: { item in
                        let fetchedImage = await fetchSomeImage()
                        item.image = fetchedImage
                    })
                    .onTap(perform: { item in
                        item.detailText = "I've Been Tapped"
                    })
            }
        }
    }
}

If only this wasn't a project onto itself.
 
Last edited:

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
On the topic of some of the discussion around concurrency, one pattern I've grown to use quite a bit and I think we discussed was the GCD-inspired approach:

Swift:
class MyObservable: ObservableObject {
    @Published var fetchedImage: Image?
    
    func fetchImage() {
        Task {
            let result = await fetchImageFromServer()
            await MainActor.run {
                fetchedImage = result
            }
        }
    }
}

You can also mark the task as @MainActor, but it means that the task has to wait for the main actor to be free to execute. But you can also use an async Future extension to allow the async code to be a little less aware of the state it will be hooked up to. But yeah, the general idea of using the async function to publish in a way that can be subscribed to on the main thread without you re-directing things back to the main actor manually is something I've started doing more often. Even with actors. So something on the UI thread just needs to subscribe to the actor's state changes and can be pushed updates from the actor as things change. Makes actors a bit easier for me to integrate into SwiftUI.

Swift:
class MyObservable: ObservableObject {
    @Published var fetchedImage: Image?
    
    func fetchImage() {
        // This could also be written as Future(asyncFunc: fetchImageFromServer) in this case.
        Future(asyncFunc: {
            return await fetchImageFromServer()
        })
        .assign(to: &$fetchedImage)
    }
}
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
I haven't been able to try much of the new APIs this year. I started a new job on the WWDC week, and between that and the holidays, I haven't seen anything yet. I hope to be able to take a look at the new Observation framework soon, I think that's going to be among the most interesting features moving forward (along with SwiftData, but that's going to take longer).

A random thought I had is why hasn't Apple done the SwiftUI treatment for CarPlay yet? It's more straight-forward than anything SwiftUI or SwiftData is trying to do. Probably just demand/resources, but seems a bit odd. I wrote up something simple, but the end result is that it can't hold any state or subscribe to Combine publishers, making it a limited approach.

I'm starting to play around with building something more fleshed out. It's not quite as nice as SwiftUI where it can very rapidly build out complex UI, but it does start seeming quite reactive, and does support subscribing to publishers and only having tasks/publishers active when a template is pushed to the screen:
Yeah, it's probably just too low priority to invest the engineering hours required for a rewrite, even though it'd be way easier than things like SwiftData. Some Apple frameworks end up in that place not long after their original release.

On the topic of some of the discussion around concurrency, one pattern I've grown to use quite a bit and I think we discussed was the GCD-inspired approach:

Swift:
class MyObservable: ObservableObject {
    @Published var fetchedImage: Image?
 
    func fetchImage() {
        Task {
            let result = await fetchImageFromServer()
            await MainActor.run {
                fetchedImage = result
            }
        }
    }
}

You can also mark the task as @MainActor, but it means that the task has to wait for the main actor to be free to execute.

My personal take on it would be to go the @MainActor route for the entire class:

Swift:
@MainActor class MyObservable: ObservableObject {
    @Published var fetchedImage: Image?
 
    func fetchImage() async {
        let result = await fetchImageFromServer()
        fetchedImage = result
    }
}

The concern of the task having to wait for the main actor to be free to execute is an interesting point. As long as fetchImageFromServer() is not @MainActor annotated itself, the code inside that function can run on any thread. But the fetchImage() function is @MainActor annotated (because the entire class is). So there's nothing that actually touches any @MainActor protected state until the fetchedImage = result line, other than the fetchImage() function itself. I honestly had to check what happens in this scenario, because I had no idea if the scheduler/compiler was smart enough to avoid doing this:
  • fetchImage() call site thread -> Main Actor -> fetchImageFromServer() thread -> Main Actor
And do this instead:
  • fetchImage() call site thread -> fetchImageFromServer() thread -> Main Actor
Basically, avoid jumping to the main thread just to dispatch a single function call that is not @MainActor itself, and only jump to the main actor for the fetchedImage = result line. Interestingly, it looks like it does not jump to the Main Actor at first! Everything is executed in the cooperative thread pool up until the fetchedImage = result line. So you should be able to call fetchImage() with the Main Actor busy, and the execution wouldn't be blocked until the fetchedImage property had to be set.

I had devised this alternative in case my version above did jump unnecessarily to the Main Actor at first:
Swift:
@MainActor class MyObservable: ObservableObject {
    @Published var fetchedImage: Image?
 
    nonisolated func fetchImage() async {
        let result = await fetchImageFromServer()
        Task { @MainActor in
            fetchedImage = result
        }
    }
}
Which is basically your first implementation + @MainActor for the full class, which I do like because we get nice compile-time warnings if we try to access @Published properties from outside the main thread. But this back and forth manual routing to the Main Actor is cumbersome and is markedly imperative, while I think the goal of Swift Concurrency is to declaratively check concurrency at compile time via annotations. Plus this kind on micromanagement of threads without profiling kinda feels like premature optimization (not saying that it couldn't become a problem, just that I'd prefer to profile it beforehand). It looks, however, like this second version is not needed anyway, since the compiler/scheduler is smart enough to avoid the first jump with the simpler code above (which I didn't know until I tested it today).

Swift:
class MyObservable: ObservableObject {
    @Published var fetchedImage: Image?
  
    func fetchImage() {
        // This could also be written as Future(asyncFunc: fetchImageFromServer) in this case.
        Future(asyncFunc: {
            return await fetchImageFromServer()
        })
        .assign(to: &$fetchedImage)
    }
}
Yep, this circumvents the issue altogether. Though I wonder how Combine fits into the future Apple is planning for its Swift ecosystem. The new Observation framework is no longer built on top of Combine, right? I'm kinda worried about Combine falling out of style.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
Yeah, it's probably just too low priority to invest the engineering hours required for a rewrite, even though it'd be way easier than things like SwiftData. Some Apple frameworks end up in that place not long after their original release.

I just find it interesting that it could have been used as a test bed for certain things where there's more interactivity than Widgets had at the time, but at the same time, not nearly as nuts as the monster they are still working to slay 4 years in. For now, I'm probably going to do some half-measure myself that leverages Combine. Later on, @Observable would be nice to move to.

And do this instead:
  • fetchImage() call site thread -> fetchImageFromServer() thread -> Main Actor
Basically, avoid jumping to the main thread just to dispatch a single function call that is not @MainActor itself, and only jump to the main actor for the fetchedImage = result line. Interestingly, it looks like it does not jump to the Main Actor at first! Everything is executed in the cooperative thread pool up until the fetchedImage = result line. So you should be able to call fetchImage() with the Main Actor busy, and the execution wouldn't be blocked until the fetchedImage property had to be set.

Yes, this is the behavior I've grown to expect, but when you produce an async function, you don't actually know what the call site is. So async functions that do a lot of work locally prior to splitting out to another async function that yields the thread can wind up blocking global actors unintentionally, so my example is more from the approach of thinking of it from that angle. Even though the example given should almost immediately yield.

Which is basically your first implementation + @MainActor for the full class, which I do like because we get nice compile-time warnings if we try to access @Published properties from outside the main thread. But this back and forth manual routing to the Main Actor is cumbersome and is markedly imperative, while I think the goal of Swift Concurrency is to declaratively check concurrency at compile time via annotations. Plus this kind on micromanagement of threads without profiling kinda feels like premature optimization (not saying that it couldn't become a problem, just that I'd prefer to profile it beforehand). It looks, however, like this second version is not needed anyway, since the compiler/scheduler is smart enough to avoid the first jump with the simpler code above (which I didn't know until I tested it today).

Yeah, this is a space where Swift can be doing "magic", but I'm finding that doing "magic" isn't what I actually want from frameworks. I want frameworks that are clear in their behavior so I am not having to think as hard about if I'm falling into a trap. Swift concurrency currently isn't quite there because too much important stuff happens under the covers where you have little visibility on what to expect as you write.

Same is true for SwiftUI to be honest. Too much just "happens" and so if you encounter a bug like touch targets somehow getting mis-sized, you have little idea why SwiftUI did what it did and if you should be filing a bug report or reciting some incantation to fix it.

Yep, this circumvents the issue altogether. Though I wonder how Combine fits into the future Apple is planning for its Swift ecosystem. The new Observation framework is no longer built on top of Combine, right? I'm kinda worried about Combine falling out of style.

They aren't specific how it's done, but it looks like observers have to register on the keys they are interested in, and provide a handler. It looks like withObservationTracking() is how the accessed properties are discovered and a handler attached to those keys. But depending on how threading is handled here, Combine could make some of this easier. It could very well that Combine vs SwiftUI is similar to AVFoundation vs AVKit. You only need to drop down if the higher level framework isn't enough. I'm fine with that. And as of today, it looks like we have two similar patterns depending on if you are working with values or references for state: @State/@Environment/@Binding for values, and @State/@Environment/@Bindable for references. And values still rely on Combine.

Combine has some uses beyond just observing within SwiftUI, and it's honestly a little nicer to use at lower levels in code where taking dependencies on SwiftUI gets weird. One thing it can do that @Observable/ObservableObject still can't is make it possible to subscribe to state changes published by an actor. Combine is also useful for turning a callback into a stream of values that works with both concurrency (awaiting a state change) and subscribers (updating multiple Observables), or producing timers for view refreshes. My network code has to do a lookup to discover the URL to use for HTTP requests. That URL depends on data from both NWPathMonitor and other state fetched from a server combined to determine which URL is appropriate for a given HTTP request. What network I'm on determines which URLs will work and which will timeout. So I can use an actor for holding this state which is subscribed to NWPathMonitor's updates. Code that is in an async context can simply yield to the actor for the lookup. Code that isn't (say, AVPlayer telling me it wants the next bits of a media file) can subscribe earlier on when it *is* possible to be async, but still defer the lookup until it's actually needed using cached state from the subscriber.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Yes, this is the behavior I've grown to expect, but when you produce an async function, you don't actually know what the call site is. So async functions that do a lot of work locally prior to splitting out to another async function that yields the thread can wind up blocking global actors unintentionally, so my example is more from the approach of thinking of it from that angle. Even though the example given should almost immediately yield.
Yes, that's a huge caveat to keep in mind. In fact, when I went to test this I mistakingly thought at first that the code was doing a jump to the Main Actor at first because the call site was incidentally the Main Actor too. Maybe what's missing is a way to tell Swift Concurrency that some code should *not* be run on the Main Actor. I think the Swift Forums had some talk around the idea of introducing a @BackgroundActor annotation? I think right now you can do something like that with Task.detached, which won't inherit (I think) the actor isolation context, but the (lack of) inheritance is in any case implicit, so it's a bit confusing to try to know what's running where.

Yeah, this is a space where Swift can be doing "magic", but I'm finding that doing "magic" isn't what I actually want from frameworks. I want frameworks that are clear in their behavior so I am not having to think as hard about if I'm falling into a trap. Swift concurrency currently isn't quite there because too much important stuff happens under the covers where you have little visibility on what to expect as you write.

Same is true for SwiftUI to be honest. Too much just "happens" and so if you encounter a bug like touch targets somehow getting mis-sized, you have little idea why SwiftUI did what it did and if you should be filing a bug report or reciting some incantation to fix it.
My counterpoint (for Swift Concurrency, at least) is that it's not that different to the "magic" compilers do with all kinds of optimizations, which are sometimes really hard to predict. But maybe some of those details are best left to the computer to figure out, rather than the programmer. I do agree however that some things that the programer definitely should know about are difficult to grasp in Swift Concurrency. An example of this would be, precisely, the question: Could this block a global actor (like the Main Actor)?. That's something that should be immediately clear to the programmer, to avoid having long-running code on the Main Actor, but isn't.

I'd argue that the plan with Swift Concurrency is to make a similar impact to concurrency as Automatic Reference Counting (ARC) did to memory management. There are still a ton of edge cases where it doesn't just work, but maybe in the not so far future we will just annotate what state needs to be protected by specific actors and it'll just work most of the time. IDK, I wasn't even a developer before ARC was introduced, but that's the closest comparison I could think of :p

Same is true for SwiftUI to be honest. Too much just "happens" and so if you encounter a bug like touch targets somehow getting mis-sized, you have little idea why SwiftUI did what it did and if you should be filing a bug report or reciting some incantation to fix it.
Well, over time you develop a intuition around what's happening underneath, but there are definitely some absurd scenarios at times. I couldn't find it again, but I read quite recently a thread on Twitter where a SwiftUI animation was fixed by adding a .scaleEffect(1.0) to one of the components. Apparently actual Apple engineers were consulted too and that was indeed the way to fix it. That sounds like reciting incantation to me.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
I think right now you can do something like that with Task.detached, which won't inherit (I think) the actor isolation context, but the (lack of) inheritance is in any case implicit, so it's a bit confusing to try to know what's running where.

“Detached“ is the naming of explicitly detaching the thread from the current async context. So that doesn’t worry me much.

It’s more that I want locality and deterministic behavior, so reviewing code and composition is easier. Concurrency is deterministic, but it is surprisingly non-local at times.

It’s still worlds better than other threading frameworks, but it feels like trade offs in comparison to GCD the more I use it.

Well, over time you develop a intuition around what's happening underneath, but there are definitely some absurd scenarios at times.

You do, but that’s a sign of a framework that could be better IMO. Part of it is that SwiftUI will aggressively undermine what you are doing in order to optimize things, and wind up making things worse.

Buttons changing shape because components are optimized away when they are transparent. Buttons ignoring hidden components for touch areas and ZStacks throwing buttons completely out of whack… etc.

It’s a language dialect onto itself, but poorly documented and not enough examples on how to create common styles Apple themselves use.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
“Detached“ is the naming of explicitly detaching the thread from the current async context. So that doesn’t worry me much.
I meant that the async context is inherited implicitly from the call site in regular async calls. Task.detached is probably the only case where you do know for sure what the async context is, as you point out.

It’s more that I want locality and deterministic behavior, so reviewing code and composition is easier. Concurrency is deterministic, but it is surprisingly non-local at times.

It’s still worlds better than other threading frameworks, but it feels like trade offs in comparison to GCD the more I use it.
The context may not be easier to reason about locally, but the flow of code has now much better locality (compared to closure-based async code), so at least there's that. IMHO the tradeoff is worth it, I spent more time trying to make sense of what-is-calling-what when working with closure-based or RxSwift code than I will ever spend on Swift Concurrency trying to figure out in which context my code will be executed. But yes, definitely a tradeoff.

It’s a language dialect onto itself, but poorly documented and not enough examples on how to create common styles Apple themselves use.
It would help a lot if they had open sourced it. The other day I was playing around with @main because I needed to run some code before UIApplicationMain and couldn't quite break down how exactly does @main invoke UIApplicationMain in a SwiftUI app, so I had to go a completely different route (used an Objective-C NSObject's load method, which runs before main).
In comparison, a couple months ago I had an argument with a colleague about how copy-on-write worked on Swift and I was able to just look up the code of basic types (I think it was String) to see how they had implemented it.

They obviously have their reasons not to open source SwiftUI, just like they didn't open source UIKit nor AppKit, but I feel like even with stellar documentation they wouldn't be able to cover all the edge cases just with documentation alone.
 
Top Bottom
1 2