Swift / Apple Development Chat

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Since the project is starting to sneak up on the 20k LOC mark, cleaning it up while fixing bugs seems like a good idea.
Mine is ~12k lines (8.5k lines of Swift and 1.8k lines of Metal) and it's already a pain to change or refactor some parts of it. I'll take that as my cue to spend some time cleaning code up :p

Fun story: I worked at a place (for just five days, I quit after seeing the code) that 'managed' a project with 90k lines of code for an iPad app that was relatively small. Turns out, every line of code was copy-pasted tens to hundreds of times throughout the code, often inside switch statements that spanned thousands of lines of code. I didn't even believe it was posible to write that much code that bad. It was WILD.

Most of the bugs they wanted fixed were features that literally had disappeared because someone had implemented something in a switch, then copy-pasted it to all other switch cases throughout the codebase that looked similar, and inadvertently pasted over a different implementation, effectively removing the feature from the code.
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
Mine is ~12k lines (8.5k lines of Swift and 1.8k lines of Metal) and it's already a pain to change or refactor some parts of it. I'll take that as my cue to spend some time cleaning code up :p

Just pure Swift code, but the iOS app by itself without any dependencies that I have written is around 15k right now. Add in the macOS/tvOS specific code, and one library I’ve written that underpins the whole thing and you can add a couple more thousand LOC.

But some of the complexity is in that I’ve done things like custom results builders to make components from UIKit feel more SwiftUI-like to use. So my custom UISplitViewController has a result builder that builds up the outline view on the sidebar, which is used to update the diffable data source under the hood. Seems to work pretty well, but it’s a good chunk of code to enable.

The trick I’m running into is dealing with a set of views that are all similar, but need to be defined differently, and currently depend on @AppStorage. So while the views themselves are getting well composed from shared components, the view models still share a good chunk of copy-paste. The common code between them amounts to something like 10 lines of code so maybe I’m over-optimizing, but at the same time, it means a lot of copy-pasted tests to ensure each place behaves correctly.

Fun story: I worked at a place (for just five days, I quit after seeing the code) that 'managed' a project with 90k lines of code for an iPad app that was relatively small. Turns out, every line of code was copy-pasted tens to hundreds of times throughout the code, often inside switch statements that spanned thousands of lines of code. I didn't even believe it was posible to write that much code that bad. It was WILD.

Most of the bugs they wanted fixed were features that literally had disappeared because someone had implemented something in a switch, then copy-pasted it to all other switch cases throughout the codebase that looked similar, and inadvertently pasted over a different implementation, effectively removing the feature from the code.

All I can say is: “oof”.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
The trick I’m running into is dealing with a set of views that are all similar, but need to be defined differently, and currently depend on @AppStorage. So while the views themselves are getting well composed from shared components, the view models still share a good chunk of copy-paste. The common code between them amounts to something like 10 lines of code so maybe I’m over-optimizing, but at the same time, it means a lot of copy-pasted tests to ensure each place behaves correctly.
I think I'm only using the @AppStorage wrapper for storing objects that are only part of the state of the view. Like to keep track of whether the sidebar was toggled on or off the last time the user used the app and things like that.

Btw, I think I get why Apple made the @FetchRequest wrapper for views instead of ViewModels, breaking the MVVM pattern (I think we talked about this at some point, right?). Turns out, since they all use the same NSManagedObjectContext (as it's injected to the environment) + the properties are always accessed from the main thread (as all Views execute in the main thread) + there's some under-the-hood magic to keep the results updated (so you don't try to retrieve properties from a NSManagedObject that no longer exists in the database), you basically avoid all likely causes of a CoreData crash unless you execute additional logic in the view. So for the typical, basic use case, you can forget about the many quirks of CoreData and avoid crashes as long as you only access that in the View.

If the wrapper were available in the ViewModels, even a single call to Task that later accessed the fetched results would result in the properties being accessed from a different thread/context leading to a crash. Or maybe receiving data from a server (which must use a background thread) and trying to update the CoreData objects from there.

Even if you kept track of the context, you could try to retrieve properties from an object that no longer exists in the database because other thread has deleted the object from the context.

Basically, there is a lot of underlying complexity that is hidden under the @FetchRequest wrapper as long as you use its entities as a read-only and main thread only, which is something that Views obviously encourage (but ViewModels wouldn't). I must say I'm not a big fan of disguising complex topics as simple like this wrapper does by implicitly assuming things.
 

DT

I am so Smart! S-M-R-T!
Posts
6,405
Reaction score
10,455
Location
Moe's
Main Camera
iPhone
Just some random thoughts-of-the-day :D

I really dislike when there's too much syntactic fluff / sugar, especially if comes by way of a framework and/or libraries, that hide too much of the implementation details (when people ask about "Should I learn X" where X is a framework, I always say, "No, learn, Y first" where Y is the language itself).

I don't think you need nuts-and-bolts all the time, but one place where I really can't stand it is in a data access layer. Some of modern executions are horrific in their inefficient data access. I come from an old school DB type background ("You kids get off my data model !"), where the DB was carefully designed, performance and execution plan analysis was done, JFC, I've seen developers - in particular people using Python + Django / Flask or Ruby + Rails, who rely on the ORM-type wrapper, implement some of the most god awful data access because it's happening under the hood and there's zero awareness.

I worked with a startup and outsourced some of the dev work, we were running on dev level Heroku instances for testing/soft launch, and the DB kept squawking about number of queries per X time, and I was a bit confused as the data access as I would've designed it, wouldn't be causing so much noise. Well, I was able to finally watch some real time logging from the app as it made DB calls, and HFC, I've never seen such painfully poor data access logic (that of course, they never saw, because they were just using data access wrappers that hid the DB level implementation).
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
I think I'm only using the @AppStorage wrapper for storing objects that are only part of the state of the view. Like to keep track of whether the sidebar was toggled on or off the last time the user used the app and things like that.

I’m doing something similar. But consider the scenario where you have a number of views that display a collection of items, and each one can be sorted. That sort can and should be consistent across uses of the app. But not all collections support the same sorts. Maybe one collection only really makes sense with a single sort.

If the wrapper were available in the ViewModels, even a single call to Task that later accessed the fetched results would result in the properties being accessed from a different thread/context leading to a crash. Or maybe receiving data from a server (which must use a background thread) and trying to update the CoreData objects from there.

Oh, I get the risks involved. But a chunk of it is that FetchRequest has also been a bit wonky and hard to debug once you get into the realm of trying to do sorting and search that I kinda gave up and dropped down to the next layer in the stack: NSFetchedResultsController.

The upside is that I now have a composable, generic component that supports search and sorting, and is displayed by another generic component that can exist in all these views. So at the very least all these views that have similar structure are really just saying they compose/configure this generic component. But they also configure the type being fetched. Fun.

I really dislike when there's too much syntactic fluff / sugar, especially if comes by way of a framework and/or libraries, that hide too much of the implementation details (when people ask about "Should I learn X" where X is a framework, I always say, "No, learn, Y first" where Y is the language itself).

I don't think you need nuts-and-bolts all the time, but one place where I really can't stand it is in a data access layer. Some of modern executions are horrific in their inefficient data access. I come from an old school DB type background ("You kids get off my data model !"), where the DB was carefully designed, performance and execution plan analysis was done, JFC, I've seen developers - in particular people using Python + Django / Flask or Ruby + Rails, who rely on the ORM-type wrapper, implement some of the most god awful data access because it's happening under the hood and there's zero awareness.

Maybe because I like digging in, I don’t mind if there is syntactic sugar so long as I can either get around it as needed, and I understand what it does.

One thing I’ve been seeing while building this is Apple’s approach of building things that are well layered and let you operate at the level you actually need. When one layer isn’t sufficient, you can drop down as needed, trading off simplicity for control. So I’ve generally let myself use the sugar as the first step to get something working and stable, and then building the specific components I need at lower levels when the simpler approach isn’t enough. It’s not a bad way to develop when you are the only engineer on the project, IMO.

That said, I do think SwiftUI is a little too opaque at times. Their approach of “invalidate, rebuild, render” in three different passes isn’t well documented, and crashes occurring in the render pass aren’t fun to debug. “Oh, there’s an NSToolbar frame in the call stack, I guess this is toolbar related? Hmm, what change do I need to make to mine to stop it from crashing on me?”
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Oh, I get the risks involved. But a chunk of it is that FetchRequest has also been a bit wonky and hard to debug once you get into the realm of trying to do sorting and search that I kinda gave up and dropped down to the next layer in the stack: NSFetchedResultsController.

The upside is that I now have a composable, generic component that supports search and sorting, and is displayed by another generic component that can exist in all these views. So at the very least all these views that have similar structure are really just saying they compose/configure this generic component. But they also configure the type being fetched. Fun.
Oh, I've seen those problems with FetchRequest in a project too. Sometimes it fired on its own and returned empty data for valid requests and the UI jumped around as a result. The solution the engineer that was working in the project at that time was to remove the FetchRequest from the view, fetch the entities from the ViewModel, and store them in a @Published array in the ViewModel for the view to use. While this solved the janky UI, some hard to debug crashes appeared as a result, since the arrays were not being updated to match the actual state of the context, or the objects were being accessed from the wrong thread. That's why I ended up having to spend most of the last week reading about CoreData :p And that, ultimately, gave me the idea that Apple probably made the FetchRequest only available to views as a way to hid a lot of the complexity of CoreData under a nice easy wrapper for the basic use cases, even if it doesn't follow the MVVM architecture they suggest.

Maybe because I like digging in, I don’t mind if there is syntactic sugar so long as I can either get around it as needed, and I understand what it does.

One thing I’ve been seeing while building this is Apple’s approach of building things that are well layered and let you operate at the level you actually need. When one layer isn’t sufficient, you can drop down as needed, trading off simplicity for control. So I’ve generally let myself use the sugar as the first step to get something working and stable, and then building the specific components I need at lower levels when the simpler approach isn’t enough. It’s not a bad way to develop when you are the only engineer on the project, IMO.

That said, I do think SwiftUI is a little too opaque at times. Their approach of “invalidate, rebuild, render” in three different passes isn’t well documented, and crashes occurring in the render pass aren’t fun to debug. “Oh, there’s an NSToolbar frame in the call stack, I guess this is toolbar related? Hmm, what change do I need to make to mine to stop it from crashing on me?”
I think the problem is that a lot of the new syntactic sugar doesn't clearly state what it does under the hood (not even in the docs), nor what assumption it's implicitly working with. That lowers the bar for the knowledge required to use some frameworks, but can be misguiding. And sometimes, specially while debugging, it's a bit of a nightmare since you may have no idea about what's happening underneath.
 

Cmaier

Site Master
Staff Member
Site Donor
Posts
5,317
Reaction score
8,498
Oh, I've seen those problems with FetchRequest in a project too. Sometimes it fired on its own and returned empty data for valid requests and the UI jumped around as a result. The solution the engineer that was working in the project at that time was to remove the FetchRequest from the view, fetch the entities from the ViewModel, and store them in a @Published array in the ViewModel for the view to use. While this solved the janky UI, some hard to debug crashes appeared as a result, since the arrays were not being updated to match the actual state of the context, or the objects were being accessed from the wrong thread. That's why I ended up having to spend most of the last week reading about CoreData :p And that, ultimately, gave me the idea that Apple probably made the FetchRequest only available to views as a way to hid a lot of the complexity of CoreData under a nice easy wrapper for the basic use cases, even if it doesn't follow the MVVM architecture they suggest.


I think the problem is that a lot of the new syntactic sugar doesn't clearly state what it does under the hood (not even in the docs), nor what assumption it's implicitly working with. That lowers the bar for the knowledge required to use some frameworks, but can be misguiding. And sometimes, specially while debugging, it's a bit of a nightmare since you may have no idea about what's happening underneath.

I’ve mentioned before that a huge problem I am having with SwiftUI is the implicit behavior of stuff. I much prefer spelling everything out.
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
Oh, I've seen those problems with FetchRequest in a project too. Sometimes it fired on its own and returned empty data for valid requests and the UI jumped around as a result. The solution the engineer that was working in the project at that time was to remove the FetchRequest from the view, fetch the entities from the ViewModel, and store them in a @Published array in the ViewModel for the view to use. While this solved the janky UI, some hard to debug crashes appeared as a result, since the arrays were not being updated to match the actual state of the context, or the objects were being accessed from the wrong thread. That's why I ended up having to spend most of the last week reading about CoreData :p And that, ultimately, gave me the idea that Apple probably made the FetchRequest only available to views as a way to hid a lot of the complexity of CoreData under a nice easy wrapper for the basic use cases, even if it doesn't follow the MVVM architecture they suggest.

While I can understand the desire to make CoreData “simpler“, I don’t think it buys a whole lot with something this complex. CoreData is one of Apple’s most complex frameworks that isn’t a UI framework, and there’s a lot going on. It’s also a framework where there’s a lot of ways to do something because of the nature of CoreData, but so many of them aren’t a good way to do it, as that previous engineer discovered.

Two things I’ve learned through my CoreData trip which might be relevant:
- NSArray Swift bridging can break the custom _PFArray class Apple uses to keep fetched results performant when they are potentially unbounded and large.
- ForEach doesn’t like RandomAccessCollection classes, and will wander out of bounds if items are removed from the collection and triggers an update.

So my FetchedResultsController delegate has to preserve the NSArray nature of the results, but also wrap it in a struct so that ForEach doesn’t throw a fit. Fun.

I think the problem is that a lot of the new syntactic sugar doesn't clearly state what it does under the hood (not even in the docs), nor what assumption it's implicitly working with. That lowers the bar for the knowledge required to use some frameworks, but can be misguiding. And sometimes, specially while debugging, it's a bit of a nightmare since you may have no idea about what's happening underneath.

The lack of docs is the real killer. The AppKit/UIKit way is documented enough that you can get a good feel for what’s going on between it and experimentation. I feel like SwiftUI lacks any description of the operating model that the other UI frameworks provide to developers.

I’ve mentioned before that a huge problem I am having with SwiftUI is the implicit behavior of stuff. I much prefer spelling everything out.

How much of that is the fact that SwiftUI is using Swift itself as a descriptive language for the UI, compared to something more purpose-built like JSX? Honestly, it feels almost like a bit of a learning trap to have your view description and your view bindings all be described in the same place. Makes it easier to get business logic all mixed into it as well. Nothing like a storyboard or nib.

Of course, switching over to a reactive paradigm doesn’t help existing devs much. I think I’m finally starting to settle in and really appreciate how it works. And this is after spending the pandemic in a React Native app…
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
I’ve mentioned before that a huge problem I am having with SwiftUI is the implicit behavior of stuff. I much prefer spelling everything out.
I agree, but it's not only limited to SwiftUI. Swift concurrency comes to mind... I'd argue that undocumented implicit behaviours matter less in UI frameworks than in other frameworks.

While I can understand the desire to make CoreData “simpler“, I don’t think it buys a whole lot with something this complex. CoreData is one of Apple’s most complex frameworks that isn’t a UI framework, and there’s a lot going on. It’s also a framework where there’s a lot of ways to do something because of the nature of CoreData, but so many of them aren’t a good way to do it, as that previous engineer discovered.

Two things I’ve learned through my CoreData trip which might be relevant:
- NSArray Swift bridging can break the custom _PFArray class Apple uses to keep fetched results performant when they are potentially unbounded and large.
- ForEach doesn’t like RandomAccessCollection classes, and will wander out of bounds if items are removed from the collection and triggers an update.

So my FetchedResultsController delegate has to preserve the NSArray nature of the results, but also wrap it in a struct so that ForEach doesn’t throw a fit. Fun.
Oh, looks like I'm in for a world of pain.

How much of that is the fact that SwiftUI is using Swift itself as a descriptive language for the UI, compared to something more purpose-built like JSX? Honestly, it feels almost like a bit of a learning trap to have your view description and your view bindings all be described in the same place. Makes it easier to get business logic all mixed into it as well. Nothing like a storyboard or nib.
XIBs had other (worse) problems though. The generated XML code was effectively unreadable, so you couldn't do proper version control and pull request reviewing with that. Now the UI can be reviewed on pull requests too, and it's much more immediately clear what's wrong with the code in case of a bug. Some of the worst silly bugs I've seen were caused by having two sources of truth for view layouts (XIB + code in the ViewController), where the XIB had some forgotten checkbox checked somewhere and was interfering with whatever the ViewController was doing.

Also, merge conflicts, which used to be a huge pain point with XIBs, are now trivially solved with SwiftUI views in most cases.

And, personally, I like having views simply react to changes in state, without having to keep track of how/when/why state changed.
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
I agree, but it's not only limited to SwiftUI. Swift concurrency comes to mind... I'd argue that undocumented implicit behaviours matter less in UI frameworks than in other frameworks.

Hmm, I’ve had less trouble with concurrency, other than where it is simply not supported or has unhelpful adapters (a byte iterator for a HTTP request? Really?).

Oh, looks like I'm in for a world of pain.

This blog post from a former Apple engineer helped me refine my own implementation: https://davedelong.com/blog/2021/04/03/core-data-and-swiftui/

XIBs had other (worse) problems though. The generated XML code was effectively unreadable, so you couldn't do proper version control and pull request reviewing with that. Now the UI can be reviewed on pull requests too, and it's much more immediately clear what's wrong with the code in case of a bug. Some of the worst silly bugs I've seen were caused by having two sources of truth for view layouts (XIB + code in the ViewController), where the XIB had some forgotten checkbox checked somewhere and was interfering with whatever the ViewController was doing.

Also, merge conflicts, which used to be a huge pain point with XIBs, are now trivially solved with SwiftUI views in most cases.

Oh yeah, very familar with XIBs and the workarounds we had to do in a very large project that used them. Code reviews with before/after screenshots, warning the team when we were touching them, etc. But I also remember when it was the nib that was checked in. I more bring it up since XIBs provide cleaner separation of view description and logic.

I do think the benefits outweigh the downsides here, but I can also pontificate a little on what could have been done better. JSX I think does provide a clearer delineation between the description and the logic, and is used in another reactive programming UI framework.

And, personally, I like having views simply react to changes in state, without having to keep track of how/when/why state changed.

As I said in my previous post, I am starting to appreciate reactive programming. But it does take a mental model adjustment coming from MVC land.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Hmm, I’ve had less trouble with concurrency, other than where it is simply not supported or has unhelpful adapters (a byte iterator for a HTTP request? Really?).
Maybe it's just that I'm not always entirely sure about how some of the async/await code I'm writing is scheduled later, in which thread I should expect it to run, those kind of things. I understand GCD better, I have a clearer picture of what's happening underneath.

This blog post from a former Apple engineer helped me refine my own implementation: https://davedelong.com/blog/2021/04/03/core-data-and-swiftui/
Oh god that's going to be extremely helpful. Thanks! I can't believe I didn't stumble upon that post myself earlier. Very interesting read. It's not all that different to how the code we have is implemented. I though most of the problem was due to context issues, but after reading that post I'm starting to think that the actual problem is that some NSManagedObjects are declared with non-optional properties (like here). But after reading the article *I think* we got the context related code right, so the only other possible culprit is the fake non-optionals.

Interesting that he made a property wrapper for the Views too, after saying that he didn't like mixing UI code and CoreData too.

Oh yeah, very familar with XIBs and the workarounds we had to do in a very large project that used them. Code reviews with before/after screenshots, warning the team when we were touching them, etc. But I also remember when it was the nib that was checked in. I more bring it up since XIBs provide cleaner separation of view description and logic.

I do think the benefits outweigh the downsides here, but I can also pontificate a little on what could have been done better. JSX I think does provide a clearer delineation between the description and the logic, and is used in another reactive programming UI framework.
Sure, I just meant that I think it's been a step in the right direction. And I believe most companies now have moved from XIBs/Storyboards to writing UIs in code (using frameworks like SnapKit or even totally by hand) where the logic is arguably even more coupled to the UI since they're part of the same class. SwiftUI is more ergonomic for that too.

I don't know enough about JSX to have an opinion on how it compares against that.
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
Maybe it's just that I'm not always entirely sure about how some of the async/await code I'm writing is scheduled later, in which thread I should expect it to run, those kind of things. I understand GCD better, I have a clearer picture of what's happening underneath.

And the answer is: it depends. Because you have the single concurrent pool, there’s no answer as to what thread things happen on. It’s either the current thread, or some other thread. And when that happens depends on the true suspension points that exist, rather than the potential ones.

I have noticed that it will avoid trying to jump threads until it really has to. So it is possible to run an async function on the main thread and hang it.

Oh god that's going to be extremely helpful. Thanks! I can't believe I didn't stumble upon that post myself earlier. Very interesting read. It's not all that different to how the code we have is implemented. I though most of the problem was due to context issues, but after reading that post I'm starting to think that the actual problem is that some NSManagedObjects are declared with non-optional properties (like here). But after reading the article *I think* we got the context related code right, so the only other possible culprit is the fake non-optionals.

Yeah, this is something where I kinda wish CoreData emitted implicitly unwrapped optionals for non-optional fields, as it’s the best description of a non-optional CoreData property (IMO). And when there is a problem with a property turning out to be nil, at least you get a more sensible crash event you can debug versus just asserting it will never be nil.

I still use auto-generated property accessors so I have some ugly unwrapping logic to handle the auto-generated optionals, but I think I might move away from that in the near future now that the object graph is getting to be stable and start using implicitly unwrapped optionals for the core properties.

Interesting that he made a property wrapper for the Views too, after saying that he didn't like mixing UI code and CoreData too.

Yeah, I think it was a bit odd. I just created something that can function as a stand-alone view model, but is easier to hold onto references for, compose, etc.

But there were some real nuggets of useful information that helped me understand good practices that really kept performance up.

Sure, I just meant that I think it's been a step in the right direction. And I believe most companies now have moved from XIBs/Storyboards to writing UIs in code (using frameworks like SnapKit or even totally by hand) where the logic is arguably even more coupled to the UI since they're part of the same class. SwiftUI is more ergonomic for that too.

I don't know enough about JSX to have an opinion on how it compares against that.

I agree that it’s an improvement, yes.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Yeah, this is something where I kinda wish CoreData emitted implicitly unwrapped optionals for non-optional fields, as it’s the best description of a non-optional CoreData property (IMO). And when there is a problem with a property turning out to be nil, at least you get a more sensible crash event you can debug versus just asserting it will never be nil.

I still use auto-generated property accessors so I have some ugly unwrapping logic to handle the auto-generated optionals, but I think I might move away from that in the near future now that the object graph is getting to be stable and start using implicitly unwrapped optionals for the core properties.
I have to disagree on this one. Too many scenarios where a non-optional CoreData NSManagedObject can return nil. Firing a fault in a NSManagedObject that has been removed from the context (but to which you still hold a reference) returns nil for its properties, for example. Regular optionals are the way to go IMO, unless you're not usually removing objects from the database that are also used by the UI. And even then... I think I prefer dealing with cumbersome logic to handle many optional properties than have unexpected crashes that are hard to trace.

BTW (unrelated), I read this article from the Asahi Linux project recently, it was very interesting.
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
I have to disagree on this one. Too many scenarios where a non-optional CoreData NSManagedObject can return nil. Firing a fault in a NSManagedObject that has been removed from the context (but to which you still hold a reference) returns nil for its properties, for example. Regular optionals are the way to go IMO, unless you're not usually removing objects from the database that are also used by the UI. And even then... I think I prefer dealing with cumbersome logic to handle many optional properties than have unexpected crashes that are hard to trace.

You are probably right. I saw the pattern in the view models of handling this translation, and was wondering how to address it. That said, it may actually be the right pattern, especially since it’s not difficult to observe managed object properties with Combine and chain them to the relevant view model properties.

Considering I did just get a crash report of an out-of-bounds issue that can only really happen when a child object disappears from the parent, this is likely related to the scenario you are describing.

BTW (unrelated), I read this article from the Asahi Linux project recently, it was very interesting.
Interesting that it’s the driver that’s responsible for managing the load/unload behavior of state that the tiles need. Makes sense in hindsight.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
And the answer is: it depends. Because you have the single concurrent pool, there’s no answer as to what thread things happen on. It’s either the current thread, or some other thread. And when that happens depends on the true suspension points that exist, rather than the potential ones.

I have noticed that it will avoid trying to jump threads until it really has to. So it is possible to run an async function on the main thread and hang it.
I couldn't think of any good examples back when I posted that, but now I've thought of a couple. For example, if you call a async network function from a @MainActor context, are all parts of the network call executed on main thread, just yielding and hopping around when results come?

And —and this seems like a good one, since it breaks Swift playgrounds— what happens if you call async let (which is meant to spawn tasks concurrently) from an actor isolated method? :p

I never had so many questions about weird corner cases with GCD. Though I've heard the idea is to slowly make async and concurrent code "safe" at compile time, so maybe it'll all be worth it in the end.
 
Last edited:

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
I couldn't think of any good examples back when I posted that, but now I've thought of a couple. For example, if you call a async network function from a @MainActor context, are all parts of the network call executed on main thread, just yielding and hopping around when results come?

Yeah, Swift doesn’t explicitly specify the behavior of non-isolated async calls when called from an actor’s isolated context. MainActor is a bit interesting since it’s the only actor with an executor that is bound to a specific thread as well. There is a SE proposal to explicitly define the behavior rather than leave it as an internal implementation detail, though.

In cases where you are calling into system APIs like URLSession, then you are actually awaiting a continuation which is resumed by a callback happening outside of Swift concurrency. So the rules for concurrency don’t even really apply as the system library has “escaped” the Task and is doing work in other ways, and will update the task later.

But to get to the core of the question itself, it does depend on when the thread suspends and if it’s in an isolated context when it does (and odds are it won’t be). The current behavior is outlined in the motiviation section of the evolution proposal.

And —and this seems like a good one, since it breaks Swift playgrounds— what happens if you call async let (which is meant to spawn tasks concurrently) from an actor isolated method? :p

Since “async let“ is short-hand for creating a Task, you can use Task as an analogue for exploring this behavior. The results are interesting as there’s inference involved:

- If the child task doesn’t require actor isolation, you’re fine, it’s run on the global executor in parallel.
- If the child task does require actor isolation because it accesses an isolated property, it will wind up waiting for the actor executor to become free at the next suspension point before it executes.
 
Last edited:

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
So it looks like Local Network Permissions might be the ugly bit that my app will have to figure out how to deal with. Apple doesn’t provide a way to query for the permissions in advance, and will instead just fail URLRequests with ”no connectivity”. To check for permissions, you have to use either Bonjour (which the service I’m talking to doesn’t advertise itself on, so it’s not a great approach), or attempt a lower level connection using NWConnection to something local.

So I have testers that are having networking issues that look related to this, but the app currently has no way to differentiate between these failure modes to say “hey, this thing needs network access”, and I’m learning the hard way that having a “send logs” button in error dialogs doesn’t seem to actually encourage them to tap on it.

Fun.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Since “async let“ is short-hand for creating a Task, you can use Task as an analogue for exploring this behavior. The results are interesting as there’s inference involved:

- If the child task doesn’t require actor isolation, you’re fine, it’s run on the global executor in parallel.
- If the child task does require actor isolation because it accesses an isolated property, it will wind up waiting for the actor executor to become free at the next suspension point before it executes.
Oh that makes sense. I'm not sure how can it infer whether actor isolation is required or on in a Task spawned from a MainActor context though, since some things are only implicitly requiring to run on that thread (but don't enforce it, or only enforce it via crash at runtime, not at compile time).

So it looks like Local Network Permissions might be the ugly bit that my app will have to figure out how to deal with. Apple doesn’t provide a way to query for the permissions in advance, and will instead just fail URLRequests with ”no connectivity”. To check for permissions, you have to use either Bonjour (which the service I’m talking to doesn’t advertise itself on, so it’s not a great approach), or attempt a lower level connection using NWConnection to something local.
That's... terrible. I hope Apple adds a way to check for those permissions on iOS 16. There shouldn't be any API that requires special permissions but offer no way to check if you have them.

and I’m learning the hard way that having a “send logs” button in error dialogs doesn’t seem to actually encourage them to tap on it.
😂😂😂
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
Oh that makes sense. I'm not sure how can it infer whether actor isolation is required or on in a Task spawned from a MainActor context though, since some things are only implicitly requiring to run on that thread (but don't enforce it, or only enforce it via crash at runtime, not at compile time).

It might help to read the Global Actors proposal here. While an actor type has both object and actor intrinsicly linked, global actors are not linked to anything without being explicitly marked as having affinity with the global actor. This applies both to the built-in MainActor and any other global actor I create. So when I annotate properties, closures or classes with @MainActor, it will bring isolation to that global actor to the annotated bit of code. For example:

Swift:
@MainActor MyViewController: UIViewController {
    /* ... */
}

This gives MyViewController affinity to the MainActor and will isolate properties to that actor. But without this annotation, code does not have any affinity to the MainActor. Just because something starts on the main thread doesn‘t mean it’s been isolated to the MainActor, and it’s generally safe to assume it isn’t isolated to any global actor if you haven’t annotated it to make it isolated.

But getting back to another bit of code example:

Swift:
extension MyViewController {
    // This function picks up isolation from MyViewController’s @MainActor annotation.
    func doesAThing() async {
        self.someProperty = true
        let task = Task {
            // No references to isolated properties infers nonisolated task
            return callSomeNonisolatedFunction()
        }
        let isolatedTask = Task {
            // Reference to isolated self infers isolated task.
            // In this case, because self is MainActor isolated, this should
            // also be isolated to MainActor.
            self.someCounter += 1
        }
        // await the tasks here.
        self.someProperty = false
    }
}

What I noticed in playgrounds is that the first Task in the example will just run as early as possible, suggesting concurrent execution. While the second task waits for the isolated function to suspend so it can have access to the actor. I tested this by using print statements and was able to see how behavior become rigidly deterministic when the Task picked up the isolation implicitly via referencing an isolated property. But if the actor’s isolation isn’t required for the Task, it seems to not get it and will run concurrently, making it less deterministic on exactly when it will run as it runs on the global executor’s thread pool.

That's... terrible. I hope Apple adds a way to check for those permissions on iOS 16. There shouldn't be any API that requires special permissions but offer no way to check if you have them.

My workaround right now is to perform a dummy NWConnection to the server and check if I can reach the server, or if it returns that the connection is blocked or rejected, and closing the connection if it is accepted without doing anything. Since odds are that dozens or hundreds of requests will follow, the extra latency doing this isn’t a huge problem, but it is silly.

But since it’s been this way since iOS 14, I‘m not holding out much hope. This has been a recurring ask to Apple since it was introduced.
 

Nycturne

Elite Member
Posts
1,137
Reaction score
1,484
Got my CarPlay entitlement yesterday. Nice. Played around with the simulator some more. Not nice. The simulator's Now Playing screen is basically useless for testing to see if you've hooked things up right. You are better off buying an aftermarket CarPlay display and using that as a test harness in your work office. Time to go shopping I think…

That framework is rather interesting. It acts a little like SwiftUI in how you build the UI via templates, but without all the necessary state management, so you get to build that yourself.
 
Top Bottom
1 2