Swift / Apple Development Chat

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Yup, that's what I've ended up doing. However, since I'm running SwiftUI on top of Catalyst there's apparently no way to constraint the size of the window. Supposedly, setting the frame of the outermost view in the WindowGroup should cause the window on macOS not to resize outside those constraints, but it doesn't work when using Catalyst 🥲

Swift:
WindowGroup {
    SomeView()
        .frame(width: 512, height: 512)
}

Yup, the gotcha of Catalyst to be honest: not only are you helped by using UIKit, but you are limited by using UIKit. A more modern, but restricted API.

But I was thinking of something like this which is possible on iOS 16 and later, and gets you that "only one copy of the window" ability:

Swift:
// This ensures that each special window only has a single identifier.
// For windows that are true "singletons", this is fine as-is.
// For inspectors that track state, OpenWindowAction cannot be used to pass the state.
enum SpecialWindowKind: Hashable, Codable {
    case inspectorWindow
    case activityWindow
}

// So long as the value used to open the window matches an existing
// scene, it will get reused. Requires iOS 16 or later.
struct SpecialWindowScene: Scene {
    var body: some Scene {
        WindowGroup(for: SpecialWindowKind.self) { kind in
            switch kind {
            case .inspectorWindow: return InspectorView(...)
            case .activityWindow: return ActivityView(...)
            }
        }
    }   
}

// Button that opens the inspector window using an enum.
struct OpenInspectorButton: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Open In New Window") {
            openWindow(value: SpecialWindowKind.inspectorWindow)
        }
    }
}

I think I'm eventually going to transition my app to SwiftUI over AppKit (Apple seems to be favoring it nowadays anyway), but I have a bunch of code using UIDocumentPickerViewController that I'm not super-excited to port to AppKit. I was hoping Apple would add a SwiftUI document picker on iOS 16 / macOS 13 and avoid writing the AppKit code at all, but no luck.

I thought DocumentGroup was supposed to help with this scenario? Or is it more that you have the need to import/export rather than a document-based app?
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
But I was thinking of something like this which is possible on iOS 16 and later, and gets you that "only one copy of the window" ability:
Yeah, I ended up doing something very similar to this. Still wish they had brought Window to iOS so I didn't have to do workarounds though.

I thought DocumentGroup was supposed to help with this scenario? Or is it more that you have the need to import/export rather than a document-based app?
It's an app that can open (but not edit) a certain type of files (protein structure files), even several of them in the same window. DocumentGroup wasn't really fit for this kind of scenario. Also I believe it imposed a certain initial app state window when no file was open. But I wanted to give other options to open files (for example: directly download this files from the internet) rather than only being able to open local files.

BTW, I spent last weekend breaking down a large ObservableObject I had in my codebase into smaller objects as my UI was being redrawn all the time (it was starting to have a noticeable performance impact). Glad to say I could break it all down in a single day. I'm mentioning this because I noticed that Views that contained an @EnvironmentObject were redrawn every time the object changed even if none of its properties was being accessed on that View, which is a behavior of SwiftUI I hadn't yet picked up on. Coincidentally, a couple days later I ran into an article that explicitly mentioned this.
Finally, in case I forget again, remember an @EnvironmentObject will trigger a view update even if the view has no reference to any of its properties.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Yup, EnvironmentObject and ObservedObject follow the same rules, just differ in how they get injected into the view.

The project I need to embark on is to fix some old logic from when views were looking at CoreData objects directly. It's left some bad approaches where the observed object gets created when a view is. Messes with navigation in a couple instances, especially in the new NavigationStack. I need to finish isolating the model behind the view model and fetch child view models in a way that avoids object duplication.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
Yup, EnvironmentObject and ObservedObject follow the same rules, just differ in how they get injected into the view.
I didn't know @ObservedObjects also refreshed the view whenever they are updated if the view body doesn't reference any of the @ObservedObject's properties. I though SwiftUI kept track of which views actually used which objects.

The project I need to embark on is to fix some old logic from when views were looking at CoreData objects directly. It's left some bad approaches where the observed object gets created when a view is. Messes with navigation in a couple instances, especially in the new NavigationStack. I need to finish isolating the model behind the view model and fetch child view models in a way that avoids object duplication.
I don't mind using CoreData objects directly in views, but I agree that it's not a maintainable approach when the views can also create or delete objects. A legacy(ish) project I was assigned to had a massive database problem because the objects were being created/modified/deleted without care from all kinds of places (UI render loops could trigger object creation+deletion, many Codable objects triggered database creation+deletion when fetching the associated JSON files from network, CoreData object creation could trigger further CoreData object creations from different types...).

My takeaway was that every database operation other than reads should be done with care, and the code should be structured in such a way that you can easily keep track of database modifications through a code path that is common to all database Create/Update/Delete operations. Sadly, the takeaway of my employer was that CoreData itself is bad, rather than the implementation the project had, so all new projects have been using Realm instead.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I didn't know @ObservedObjects also refreshed the view whenever they are updated if the view body doesn't reference any of the @ObservedObject's properties. I though SwiftUI kept track of which views actually used which objects.

Yeah, I learned that the hard way a while back. Only the objectWillChange notification is watched by SwiftUI. And the use of @ObservedObject or @EnvironmentObject is what tells the view to listen for that notification. There's a lot less black magic in SwiftUI than Apple suggests.

The subtle clue is that primitive views like Progress, Image, Text and Label don't take read-only bindings (i.e. a publisher), they take values. Those can't update themselves without the parent view being the one to update/redraw. And in views that use an ObservableObject, it makes sense they would use multiple properties to fill out a view. So, listening to objectWillChange reduces the noise generated for the common use case, and it makes for a good match with MVVM since the view model now has full control over when the view will redraw and can hold onto any complicated logic around that.

This is also why you see some of the patterns I suggested around Combine use earlier in the thread, since using those for bindings helps limit how frequently objectWillChange fires.

I don't mind using CoreData objects directly in views, but I agree that it's not a maintainable approach when the views can also create or delete objects. A legacy(ish) project I was assigned to had a massive database problem because the objects were being created/modified/deleted without care from all kinds of places (UI render loops could trigger object creation+deletion, many Codable objects triggered database creation+deletion when fetching the associated JSON files from network, CoreData object creation could trigger further CoreData object creations from different types...).

My takeaway was that every database operation other than reads should be done with care, and the code should be structured in such a way that you can easily keep track of database modifications through a code path that is common to all database Create/Update/Delete operations. Sadly, the takeaway of my employer was that CoreData itself is bad, rather than the implementation the project had, so all new projects have been using Realm instead.

In my case, the edits essentially have to be wrapped in the way you suggest. I send the edit up to a server, which tells me it was successful or not, and then I apply the edit locally to CoreData to keep the two sides in sync. This is all done on background threads with an editor CoreData context. So that's why I started hiding things behind a view model when I started to add editing functionality. But my naive implementation meant that I have some bad patterns in the code now when stuff gets passed around the view hierarchy. Mostly stuff that makes future changes more difficult, so I'm trying to get myself out of the corner I painted myself into.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
Engineering at Meta has shared this crazy review of the Facebook iOS app throughout the years:


I've seen this happen in other companies (at a smaller scale), mostly because of management wanting to add too many features in too little time and thinking that throwing more people will solve the problem. The whole "not every problem can be parallelized" concept is not something most managers think about. But this level of absurd and unbounded complexity is worse than anything I could have imagined.

Worst part, apparently they still haven't found out that this is a terrible idea, because (quote from the article):
There is no doubt that plugins led FBiOS farther away from idiomatic iOS development, but the trade-offs seem to be worth it.
No. Just no.

This also must make finding and onboarding iOS engineers in the Facebook app a near-impossible task. As good as internal documentation could possibly be, this development process is almost like switching to another different topic in Computer Science. The fact that this mention:
Plugin errors are not on Stack Overflow and can be confusing to debug."
Made its way into the public engineering post about the app architecture probably reflects that this was a massive problem internally.
 

Cmaier

Site Master
Staff Member
Site Donor
Posts
5,209
Reaction score
8,250
Engineering at Meta has shared this crazy review of the Facebook iOS app throughout the years:


I've seen this happen in other companies (at a smaller scale), mostly because of management wanting to add too many features in too little time and thinking that throwing more people will solve the problem. The whole "not every problem can be parallelized" concept is not something most managers think about. But this level of absurd and unbounded complexity is worse than anything I could have imagined.

Worst part, apparently they still haven't found out that this is a terrible idea, because (quote from the article):

No. Just no.

This also must make finding and onboarding iOS engineers in the Facebook app a near-impossible task. As good as internal documentation could possibly be, this development process is almost like switching to another different topic in Computer Science. The fact that this mention:

Made its way into the public engineering post about the app architecture probably reflects that this was a massive problem internally.
Yeah, the fact that the author was apparently proud of this mess says a lot.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I've seen this happen in other companies (at a smaller scale), mostly because of management wanting to add too many features in too little time and thinking that throwing more people will solve the problem. The whole "not every problem can be parallelized" concept is not something most managers think about. But this level of absurd and unbounded complexity is worse than anything I could have imagined.

Sounds like you haven't spent much time in React Native development yet. It's the poster child of the "just how many different modules can I take dependencies on so I don't have to write code?" school of thought. Also a Facebook/Meta gift to the world. So, much, overhead.

The more I read into this, the more gems I find that make me facepalm.
  • No commentary on static initialization time spent during app launch. Large projects in C-like languages can be absolutely rife with this stuff.
  • No commentary about perhaps setting up some sort of management that can lazily initialize component modules when they are first needed instead of doing this.
  • Just happy to throw away the benefits of dead code stripping that static linking brings in for projects of this scale.
  • Just moving away from C++ in the shared code instead of layering Obj-C++ or C APIs. Or hell, C++ interop in Swift is something the Swift team wants to tackle today. Donate some dev time to the effort, perhaps?
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
So one thing on SwiftUI that I've found little to no resources on, but am curious about is understanding how Apple builds components that are designed to be re-used across different platforms. Button for example, which behave differently depending on Style and the like applied to it, and different platforms set a default style as well as allowing different styles depending on the platform. While you can find out how to do various things in SwiftUI, nobody seems to bring it together into how to build good reusable components at the same quality level of what Apple provides.

As best as I can figure, Button owns the content/state, but then delegates the final rendering to the active style that conforms to ButtonStyle. This is just an environment value at the end of the day, I think. Since environment settings have defaults, there's that as well. So it's probably more straight-forward than I fear. Any ButtonStyle can even just provide an NSViewRepresentable in the case of wanting the standard Mac button style rather than a custom button.

Since my app uses a custom ProgressView with drag/click functionality, and I've now got 3-4 different styles I want to implement, this seems like a pretty good control to learn this strategy on.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
Sounds like you haven't spent much time in React Native development yet. It's the poster child of the "just how many different modules can I take dependencies on so I don't have to write code?" school of thought. Also a Facebook/Meta gift to the world. So, much, overhead.
Thankfully I never got into React / React Native much. But a couple of my coworkers came into native iOS development from React Native, and it does show in things like how many and what kind of dependencies they add to projects (also in the way they structure the projects, typically with heavily reactive and massive architectures, but that's for another day). I'm usually the opposite: I try to roll in-house solutions unless there's clear benefit in using a pre-existing library. I'm particularly averse of libraries from small developers. Not because they're bad (they aren't) but because the documentation is usually lacking and it's much harder to find people with the same issues in Google (purely because the user base is orders of magnitude smaller).
I had a problem with this last week, with a little library called XCoordinator. The idea behind the library is not a bad one, but the API names are so similar to Apple's own navigation APIs that finding any info on Google about minimal issues is incredibly difficult. And, because it's a less known library, there's a much slimmer chance that anyone new to the project knows how this library works.

A couple months before I had to remove a image caching library too from another project, because the image views the framework provided didn't animate correctly in SwiftUI, and there was nothing we could do about it. This was even worse, because matching the caching feature set the app was using required a grand total of 3 classes and less than two days of work, so the restrictions the 3rd party library was imposing on us were totally avoidable. I know there's another library in the project to access elements in the keychain (apparently, because Apple's API is 'too verbose'), which saves like 30 lines total vs Apple's API, and is going to be more confusing to everyone who gets into the project because it's almost guaranteed that the new people we get in haven't used this library.

So one thing on SwiftUI that I've found little to no resources on, but am curious about is understanding how Apple builds components that are designed to be re-used across different platforms. Button for example, which behave differently depending on Style and the like applied to it, and different platforms set a default style as well as allowing different styles depending on the platform. While you can find out how to do various things in SwiftUI, nobody seems to bring it together into how to build good reusable components at the same quality level of what Apple provides.

As best as I can figure, Button owns the content/state, but then delegates the final rendering to the active style that conforms to ButtonStyle. This is just an environment value at the end of the day, I think. Since environment settings have defaults, there's that as well. So it's probably more straight-forward than I fear. Any ButtonStyle can even just provide an NSViewRepresentable in the case of wanting the standard Mac button style rather than a custom button.

Since my app uses a custom ProgressView with drag/click functionality, and I've now got 3-4 different styles I want to implement, this seems like a pretty good control to learn this strategy on.
I hadn't though about it, but I'll keep this in mind when creating new UI components. I try to match Apple's APIs design wherever possible, but I hadn't ever done anything like .buttonStyle or .listStyle for custom components. The way I usually see it done is either by creating different, separate components (FooPrimaryButton, FooSecondaryButton...), which obviously hurts reusability (and often makes the UI and behavior diverge over time for components that should behave essentially the same), or by enforcing extra style arguments at initialization (FooButton(color: .primary)). But for a component with many different styles those kind of modifiers are probably best, as you minimize the amount of code duplication in both the view implementation and the call site.

For different iOS/macOS implementations, I don't know what's common in the industry, as I've only worked on iOS or iPadOS apps. For my own apps, I maintain separate implementations (when necessary) for the same custom SwiftUI component in iOS/macOS by using #if targetEnvironment in the implementation of the component, keeping a single view type externally. So I can do things like CustomComponent() throughout the app that translate to different views on different environments.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
matching the caching feature set the app was using required a grand total of 3 classes and less than two days of work, so the restrictions the 3rd party library was imposing on us were totally avoidable.

This is generally what I wish more devs saw and understood. It’s a fight I keep having. “We should own fewer lines of code” vs “this is more overhead than owning those lines of code.”

But for a component with many different styles those kind of modifiers are probably best, as you minimize the amount of code duplication in both the view implementation and the call site.

That’s kinda my thinking here. Take how playback progress bars are represented. There’s the two different styles that Apple has used on iOS music in iOS 15 and 16. There’s the compact style used in the macOS music app. There’s another more minor variant of the iOS style in the macOS mini player. And I could certainly customize this further if I want to get fancy in specific places like if I want to use a waveform on AppleTV. Ultimately, it’s just splitting out render from the definition of what a button is.

If I can say “give me the default playback progress”. I can define the default style per platform with a typealias and use composition to reuse much of the code (since it also builds on ProgressView).

My PlaybackProgressView control using styles is coming together nicely. Might post it to GitHub once I get a little further along.

But yes, I agree with keeping a single public type for all platforms. I like creating a sort of informal “protocol” where I can refactor out the platform specific bits of the view into an extension. Then it’s just a matter of looking at the correct extension for the platform behavior. Not a fan of in-line #if myself.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
This is generally what I wish more devs saw and understood. It’s a fight I keep having. “We should own fewer lines of code” vs “this is more overhead than owning those lines of code.”
Thankfully people at my (current) day job there's not much talk about owning fewer lines of code. The projects are all relatively small (~20k lines of code), so it's not like it's going to become unmanageable either way. But unnecessary libraries are introduced a lot. Simple things, like a resizable modal view or a list with draggable rows, often depend on third party libraries. There's not much overhead at the time it's implemented, but if something breaks or needs to be changed it takes longer to understand what you're looking at.


Another thing I've thinking of lately is just how important it is to let the compiler help you. I don't know exactly what have I changed in my coding style, but there's this one project I've been working for about a year and whenever I need to make a change (for example, because the REST API changed), it's often enough to change the signature of the function at the 'deepest' level (where the network call is actually performed) and then, by the time I fix the last compiler errors, everything works. In contrast, I'm on a RxSwift project where the opposite happens: whenever I make a change in the code, very few compiler errors appear, and after fixing all of then the new feature is still almost guaranteed to not be working yet.

I think it's mostly related to optionals, but I haven't yet pinpointed the cause. Many people/projects (particularly those using RxSwift, apparently) use optionals to work around difficulties getting a required object at the time of the initialization of a class that need it, so the property ends up marked as optional and set a few lines after initialization, like:
Swift:
let profileView = ProfileView()
// ...
profileView.bind(to: userPublisher)
IMHO, if a class/struct is expected to always have a certain property, the initializer should enforce the property being present, even if it's more cumbersome to get a hold of the data before initialization.
Swift:
let profileView = ProfileView(user: user) // or userPublisher, whatever is being used to access the user

The first pattern (I don't know how common it is, but I've seen it more than once by now) requires the programmer to remember to call the bind(to: ...) on every initialization of the class, and the compiler has no clue that this is required so it does not throw an error if you forget to call it. I like to write code in a way that the compiler doesn't let me forget me basic things.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Thankfully people at my (current) day job there's not much talk about owning fewer lines of code. The projects are all relatively small (~20k lines of code), so it's not like it's going to become unmanageable either way. But unnecessary libraries are introduced a lot. Simple things, like a resizable modal view or a list with draggable rows, often depend on third party libraries. There's not much overhead at the time it's implemented, but if something breaks or needs to be changed it takes longer to understand what you're looking at.

Exactly. And it's something that consistently slows velocity on certain teams I work with recently. The inability to quickly and reasonably debug JS code is one reason I honestly despise React Native. When I see devs using the JS equivalent of "printf debugging", I want to pull my hair out.

Another thing I've thinking of lately is just how important it is to let the compiler help you. I don't know exactly what have I changed in my coding style, but there's this one project I've been working for about a year and whenever I need to make a change (for example, because the REST API changed), it's often enough to change the signature of the function at the 'deepest' level (where the network call is actually performed) and then, by the time I fix the last compiler errors, everything works. In contrast, I'm on a RxSwift project where the opposite happens: whenever I make a change in the code, very few compiler errors appear, and after fixing all of then the new feature is still almost guaranteed to not be working yet.

This is also exactly why I've become a bit of a Swift junkie. By being able to be clear and expressive with your contracts, you avoid soooo many runtime bugs by making them compiler errors. The concept of immutability also saves a lot of hair pulling in this way too.
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
Exactly. And it's something that consistently slows velocity on certain teams I work with recently. The inability to quickly and reasonably debug JS code is one reason I honestly despise React Native. When I see devs using the JS equivalent of "printf debugging", I want to pull my hair out.
To be fair I’m getting the “the value may have been optimized out” in the Swift debugger a lot lately. Don’t know why, because every time it happens I find a workaround so I haven’t really bothered to find the root cause yet.

This is also exactly why I've become a bit of a Swift junkie. By being able to be clear and expressive with your contracts, you avoid soooo many runtime bugs by making them compiler errors. The concept of immutability also saves a lot of hair pulling in this way too.
And I bet Swift concurrency is going to be yet another step up in the amount of bugs that can be catched by the compiler instead of failing at runtime. I don’t know how smooth the transition of current async/await code is going to be though, I have tried enabling the strict concurrency checks on Xcode and it complains about a LOT of things, even in relatively new projects. When Swift 6 treats all of those as errors… some code bases might be up for a ton of refactoring.

Or maybe it won’t be that bad. I need to test it a bit more, but from what I’ve seen in the projects I’ve been able to test, a lot of those errors disappear if you let the ViewModels be annotated as @MainActor. Maybe it’ll just be that easy. And it does makes sense, you don’t want to be context switching all the time in and out of the main thread anyway, unless there’s some task that is really going to take some time.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
To be fair I’m getting the “the value may have been optimized out” in the Swift debugger a lot lately. Don’t know why, because every time it happens I find a workaround so I haven’t really bothered to find the root cause yet.

It seems like the compiler might be very aggressive when it comes to releasing references and limiting the scope, even with debug builds and that tends to foul up LLDB. I haven’t done much to try to fix this myself since usually stuff is still in scope when I need to poke at it.

With larger projects, LLDB has a tendency to fall over and fail to access variables, but I thought Apple was actively making that better. This is stuff in the million+ LOC range though.

Still nothing like the utter mess of trying to debug async JS. Try debugging async code when your callstack contains no useful context.

And I bet Swift concurrency is going to be yet another step up in the amount of bugs that can be catched by the compiler instead of failing at runtime. I don’t know how smooth the transition of current async/await code is going to be though, I have tried enabling the strict concurrency checks on Xcode and it complains about a LOT of things, even in relatively new projects. When Swift 6 treats all of those as errors… some code bases might be up for a ton of refactoring.

Or maybe it won’t be that bad. I need to test it a bit more, but from what I’ve seen in the projects I’ve been able to test, a lot of those errors disappear if you let the ViewModels be annotated as @MainActor. Maybe it’ll just be that easy. And it does makes sense, you don’t want to be context switching all the time in and out of the main thread anyway, unless there’s some task that is really going to take some time.

It will certainly help, but I do think the shift will be painful on this one. Specifically because of the fact that synchronous code that isn’t annotated will simply run on the same thread, and that’s by design. So you will need to be annotating things going forward. I personally expect @Sendable to be my bane as it’s not currently enforced in any real way.

Some of the strict checking should come with more implicit adoption of @MainActor going forward via inheritance and conformance. UIKit/AppKit/SwiftUI already mark some things as @MainActor, but I’m surprised ObservableObject isn’t marked this way?
 

Andropov

Site Champ
Posts
602
Reaction score
754
Location
Spain
It seems like the compiler might be very aggressive when it comes to releasing references and limiting the scope, even with debug builds and that tends to foul up LLDB.
Now that you mention it, there were several suspension points in-between the last usage of the variable that LLDB failed to print and the actual breakpoint.

It will certainly help, but I do think the shift will be painful on this one. Specifically because of the fact that synchronous code that isn’t annotated will simply run on the same thread, and that’s by design. So you will need to be annotating things going forward. I personally expect @Sendable to be my bane as it’s not currently enforced in any real way.

Some of the strict checking should come with more implicit adoption of @MainActor going forward via inheritance and conformance. UIKit/AppKit/SwiftUI already mark some things as @MainActor, but I’m surprised ObservableObject isn’t marked this way?
@Sendable warnings do appear with strict concurrency checking though. I guess I can see why ObservableObject is not marked with @MainActor (only publisher changes need to be on the main thread), but it's also the reason a lot of people are premature-optimizing so *only* the publisher changes execute in the main thread and leave trivial things without the @MainActor annotation. Things like:
Swift:
func updateStuff() async {
    let newFooValue = await getUpdatedFooValue()
    Task { @MainActor in
        self.fooValue = newFooValue
    }
}
Could probably be written as:
Swift:
@MainActor func updateStuff() async {
    let newFooValue = await getUpdatedFooValue()
    self.fooValue = newFooValue
}
It's true that on the first case getUpdatedFooValue won't implicitly inherit a @MainActor context, which is a good thing... if it's doing a significant amount of work synchronously, which is often not the case. At worst, if it does a trivial amount of work and you force a context switch, the overhead of changing threads is greater than the time it takes to just execute it on the main thread.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
Now that you mention it, there were several suspension points in-between the last usage of the variable that LLDB failed to print and the actual breakpoint.

I mostly bring it up because it seems that LLDB expects C-like scoping for variables, which should be true for value types. I’m not entirely sure that this applies for reference types due to ARC. I haven’t been able to dig into how the compiler inserts retain and releases. So it is still a guess on my part.

@Sendable warnings do appear with strict concurrency checking though. I guess I can see why ObservableObject is not marked with @MainActor (only publisher changes need to be on the main thread), but it's also the reason a lot of people are premature-optimizing so *only* the publisher changes execute in the main thread and leave trivial things without the @MainActor annotation.

I’m more thinking in terms of when you don’t have strict checking enabled (i.e. the defaults). I should probably start enabling it to see how bad I’ve got it.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
I started playing around with ChatGPT asking it questions on how to do things with SwiftUI and view models. There's been a big push internally to leverage ChatGPT-like tech at work, and so I was curious about asking it questions in a space where I knew how to detect any BS coming out. On one hand, it actually gives some surprisingly useful input for a chat model for someone new to a space. That said, it also produced some very derivative, simple, and sometimes dumb output.

In a CoreData-based app, each view model typically corresponds to a managed object, which represents a record in the app's database. Child view models can depend on their parent view models for access to their parent's managed object context, which is necessary to fetch and save data.

Hmm, last I checked, every NSManagedObject has a reference to its context.

Some things that I had to do:
  • Had to coax it to use NSFetchedResultsController for automatic refresh, instead favoring NSManagedObjectContextDidSave and manual refreshing.
  • Then it decided it was okay to recreate all child view models (which tends to break things in SwiftUI through over-invalidation)
  • When asked how to reuse the child view models, no commentary on Identifiable (which should be able to also address over-invalidation so long as the View Models aren't stateful) or even any code to reuse the view models, it just continues to remake the whole array on fetch:
Swift:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    children = controller.fetchedObjects?.compactMap { ChildViewModel(child: $0) } ?? []
}

(As an aside, I kinda hate this code as it uses trailing closures in a way that makes the code harder to read.)

The response was mind-breaking:

You can reuse the existing ChildViewModel instances by passing the fetched objects to the view model's initializer instead of creating new instances for each object.

Uh... what? No? That's not how initializers work?

This is an interesting tool for probing some existing knowledge and faster/easier than swimming through the SEO gray goo that search engines are quickly becoming, but it is also just as much possible to lead you into a different type of grey goo where there's this sort of air of authority to everything ChatGPT is putting out, but it's about as useful as a new hire out of college working in an unfamiliar language, trawling through Stack Overflow.
 
Last edited:

ArgoDuck

Power User
Site Donor
Posts
101
Reaction score
161
Location
New Zealand
Main Camera
Canon
^ Interesting. Not related to this thread but i checked out ChatGPT late last year (under my social sciences hat). Initial enthusiasm - ’gee, this is like having a quite good intern/research assistant on tap’ - rapidly drained away when i encountered errors and repetitive answers that missed important differences in questions. I found exactly what you did in your last paragraph

Thought about trying it on Swift/SwiftUI. Glad I didn’t.
 

Nycturne

Elite Member
Posts
1,108
Reaction score
1,417
^ Interesting. Not related to this thread but i checked out ChatGPT late last year (under my social sciences hat). Initial enthusiasm - ’gee, this is like having a quite good intern/research assistant on tap’ - rapidly drained away when i encountered errors and repetitive answers that missed important differences in questions. I found exactly what you did in your last paragraph

Thought about trying it on Swift/SwiftUI. Glad I didn’t.

It will improve, but I think the worry I have is that with the big recent publicity, there's folks (Microsoft being one of them) that are pushing to try to adapt this into their workflow. But if you aren't able to recognize that its output is inherently untrustworthy, then you can find yourself going down the wrong path and having to start over once you realize the answer is wrong. Even worse is that as people become dependent on tools like ChatGPT, the issue of citing sources gets harder, not easier, and so checking things gets more difficult. We make ourselves dependent on a tool that doesn't reason the way we do, in order to reason about topics and issues. Woof.

Now that you mention it, there were several suspension points in-between the last usage of the variable that LLDB failed to print and the actual breakpoint.


@Sendable warnings do appear with strict concurrency checking though. I guess I can see why ObservableObject is not marked with @MainActor (only publisher changes need to be on the main thread), but it's also the reason a lot of people are premature-optimizing so *only* the publisher changes execute in the main thread and leave trivial things without the @MainActor annotation.

Back on this topic, one thing I ran into: Trying to make an observable object that is annotated with @MainActor Identifiable (so I can use them in lists more easily), I get this bit of fun:

Main actor-isolated property 'id' cannot be used to satisfy nonisolated protocol requirement

Because identifiable is a non-isolated protocol, but it's being used in a main actor context, the compiler cannot resolve the conflict. This makes sense since Identifiable shouldn't be an isolated protocol in practice (it's a very generic concept), but it does mean I can't provide an implementation of Identifiable and mark the class as MainActor at the same time. This is the sort of pain I'm thinking of when it comes to fixing this stuff.

So realistically, you probably want to mark the async functions in an Observable Object as MainActor, rather than the whole type. Meanwhile things like UIView/View/etc should absolutely inherit MainActor for the whole type where possible.

It's true that on the first case getUpdatedFooValue won't implicitly inherit a @MainActor context, which is a good thing... if it's doing a significant amount of work synchronously, which is often not the case. At worst, if it does a trivial amount of work and you force a context switch, the overhead of changing threads is greater than the time it takes to just execute it on the main thread.

After thinking on this, I agree. In the scenario where there is a big chunk of synchronous work, that's a case where I'd create a child task and then wait on it to allow suspension to happen while the work happens on a background thread.
 
Last edited:
Top Bottom
1 2