Swift / Apple Development Chat

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
On the Combine front, I’ve been finding more places where it is useful for drastically cutting down code.

My app has a central “engine” doing the bulk of the work but that engine needs to update various APIs that go beyond SwiftUI. So it exposes state via publishers. UI stashes the result in @State, and “plug-ins” update those other APIs as well. But each plugin can chain the publisher with other details/context to make their job easier. So supporting things in iOS like the now playing details for the Lock Screen is just a subscriber to the engine. Uploading playback state to Plex is also a subscriber.

It’s not a bad way to attach consumers of state to a producer/owner of state with minimal coupling. And is really easy to mock the publisher for whatever testing you want to do.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
I got to work with a new setup for autogenerating network code from a backend schema (similar to what I was used to with apollo-client). Totally different setup, this time based on OpenAPI Generator instead of apollo-client.

Still thinking it's not worth it. Autogenerated models end up having poor type safety (lots of String uses in places where you could have used an enum, or a Date). The Swift code that interacts with this autogenerated layer ends up being awkward to use.

For example: I try to avoid adding a default clauses to switches like the plague. I've been saved so many times by Swift's enforcement of switches being exhaustive that I try to use them wherever possible. But the autogenerated model uses a String for the field instead of an enum, so there's no way around it:
Swift:
switch userType {
case "GUEST_USER":
    // ...
case "REGULAR_USER":
    // ...
case "PREMIUM_USER":
    // ...
default:
    // I wish I could avoid this one!
}
Now if a new field is added I'll have to go hunt down all other instances of this kind of switch. I'd rather have the compiler do it.

Another example: some important fields are sometimes generated as optionals. I have control over this if I use my own Codable model, but not with the autogenerated code, so I can end up with something like this:
Swift:
struct User_autogenerated {
    let userData: UserData_autogenerated?
}
What good is it to have a User_autogenerated object if its userData property is nil? I'd rather have it fail on decoding in that case. This adds a lot of unnecessary optional unwrapping.

And there's another issue with having many indirection levels until you get to the fields you want to use:
Swift:
struct UserAttributes_autogenerated {
    let id: String
    let name: String
}

struct UserData_autogenerated {
    let attributes: UserAttributes_autogenerated
}

struct User_autogenerated {
    let userData: UserData_autogenerated?
}

// ...

let name = user.userData?.attributes.name
Getting the name there is a bit awkward.

No one would model data like that if doing it by hand because of the issues I mentioned (among others). Yet I've seen this kind of thing in two different companies, despite both places having generally great software quality otherwise, which leads me to believe that this is not uncommon.
 

casperes1996

Power User
Posts
185
Reaction score
171
I got to work with a new setup for autogenerating network code from a backend schema (similar to what I was used to with apollo-client). Totally different setup, this time based on OpenAPI Generator instead of apollo-client.

Still thinking it's not worth it. Autogenerated models end up having poor type safety (lots of String uses in places where you could have used an enum, or a Date). The Swift code that interacts with this autogenerated layer ends up being awkward to use.

For example: I try to avoid adding a default clauses to switches like the plague. I've been saved so many times by Swift's enforcement of switches being exhaustive that I try to use them wherever possible. But the autogenerated model uses a String for the field instead of an enum, so there's no way around it:
Swift:
switch userType {
case "GUEST_USER":
    // ...
case "REGULAR_USER":
    // ...
case "PREMIUM_USER":
    // ...
default:
    // I wish I could avoid this one!
}
Now if a new field is added I'll have to go hunt down all other instances of this kind of switch. I'd rather have the compiler do it.

Another example: some important fields are sometimes generated as optionals. I have control over this if I use my own Codable model, but not with the autogenerated code, so I can end up with something like this:
Swift:
struct User_autogenerated {
    let userData: UserData_autogenerated?
}
What good is it to have a User_autogenerated object if its userData property is nil? I'd rather have it fail on decoding in that case. This adds a lot of unnecessary optional unwrapping.

And there's another issue with having many indirection levels until you get to the fields you want to use:
Swift:
struct UserAttributes_autogenerated {
    let id: String
    let name: String
}

struct UserData_autogenerated {
    let attributes: UserAttributes_autogenerated
}

struct User_autogenerated {
    let userData: UserData_autogenerated?
}

// ...

let name = user.userData?.attributes.name
Getting the name there is a bit awkward.

No one would model data like that if doing it by hand because of the issues I mentioned (among others). Yet I've seen this kind of thing in two different companies, despite both places having generally great software quality otherwise, which leads me to believe that this is not uncommon.
My take is that the OpenAPI generated code is great for prototyping and quickly iterating on an idea, especially for things that need to work across several languages, but for a long-term solution, something hand-crafted is more ergonomic. But nothing is more permanent than a temporary solution that works
 

dada_dave

Elite Member
Posts
2,163
Reaction score
2,148
I was watching the following video by an Apple engineer and member of the Swift team on Swift as a successor language and migrating to it in a large code base and the presenter said something interesting, namely that objective-C was really good for building stable APIs (especially in comparison to C/C++). I wasn’t really sure what he meant by that. I was wondering if anyone here might know?

Note I know very little about objective C/swift - I was just watching this video out of curiosity - and I also thought some here might likewise find it interesting, so I posted the link below:

 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
I was watching the following video by an Apple engineer and member of the Swift team on Swift as a successor language and migrating to it in a large code base and the presenter said something interesting, namely that objective-C was really good for building stable APIs (especially in comparison to C/C++). I wasn’t really sure what he meant by that. I was wondering if anyone here might know?

Might help to know when it was said in the talk for context. Skimming an hour long talk for a single reference is pretty time consuming. I didn’t catch him saying that during my skim. I did catch him saying that C++ was a poor language for stable APIs, which is generally true. And he comments that Apple ships frameworks using Obj-C and C because of this.

It’s been a while, but C++ has some rather ugly aspects to OOP that make it painful to use for dynamically linked libraries of the kind Apple want to be able to ship with the OS. Specifically, the type (and thus the memory layout of data structures) is defined in a header in C/C++, but functions on the type in C++ live in the linked library. As you can imagine these two must be in sync, or things go downhill. So if I use the header for a type in “myGreatOS 13” and the memory layout of the type in “myGreatOS 15” differs at all, the libraries aren’t actually compatible, and thus I need to ship multiple copies of of the C++ library to be compatible if I even touch the memory layout of a system type. Function inlining only makes this harder. It’s much like the issue where different apps require different versions of the .NET SDK and runtime, only worse.

Objective-C doesn’t have this problem though, as the type is fully hidden behind messages to the object that get resolved at runtime (minus some optimizations that happen). So I am free to manipulate the internals of a type in Objective-C, as well as add new methods to the type, without breaking compatibility of users on different versions of the SDK. I can build my app using the version of UIKit in iOS 13, and on iOS 15, my app loads the iOS 15 version of UIKit. I get all the nice bugfixes and improvements that don’t require adopting new APIs without having to rebuild and deploy a new version.

C doesn’t have this problem quite so badly either, if you approach your API design in particular ways. CoreFoundation and similar low level APIs on Apple platforms use opaque data types created by functions. So Apple is again free to make changes to data structures, add new functions, and not break compatibility in a way that prevents existing app builds from dynamically linking against the new versions of the frameworks. It’s a little more work to do the same in C++, but the C API is generally useful in more situations anyways so there’s not much reason to do opaque types in C++. Swift can interop with C but not C++ (yet), which happens to make it easier to drop down to these lower level APIs than if they were C++.

But the thing is, Swift didn’t freeze the ABI until Swift 5 or so. This step is critical for being able to build an app with Swift 5 and then being able to link against libraries included in a newer OS built with Swift 6 in the future. It was only then that Apple could start shipping Swift frameworks in the OS. So Swift’s suitability for these sort of frameworks is relatively new. But it was an original goal of the language from the start, just one that was kicked down the road until they could nail down other key things they wanted to get right before freezing the ABI.
 

dada_dave

Elite Member
Posts
2,163
Reaction score
2,148
Might help to know when it was said in the talk for context. Skimming an hour long talk for a single reference is pretty time consuming. I didn’t catch him saying that during my skim. I did catch him saying that C++ was a poor language for stable APIs, which is generally true. And he comments that Apple ships frameworks using Obj-C and C because of this.

It’s been a while, but C++ has some rather ugly aspects to OOP that make it painful to use for dynamically linked libraries of the kind Apple want to be able to ship with the OS. Specifically, the type (and thus the memory layout of data structures) is defined in a header in C/C++, but functions on the type in C++ live in the linked library. As you can imagine these two must be in sync, or things go downhill. So if I use the header for a type in “myGreatOS 13” and the memory layout of the type in “myGreatOS 15” differs at all, the libraries aren’t actually compatible, and thus I need to ship multiple copies of of the C++ library to be compatible if I even touch the memory layout of a system type. Function inlining only makes this harder. It’s much like the issue where different apps require different versions of the .NET SDK and runtime, only worse.

Objective-C doesn’t have this problem though, as the type is fully hidden behind messages to the object that get resolved at runtime (minus some optimizations that happen). So I am free to manipulate the internals of a type in Objective-C, as well as add new methods to the type, without breaking compatibility of users on different versions of the SDK. I can build my app using the version of UIKit in iOS 13, and on iOS 15, my app loads the iOS 15 version of UIKit. I get all the nice bugfixes and improvements that don’t require adopting new APIs without having to rebuild and deploy a new version.

C doesn’t have this problem quite so badly either, if you approach your API design in particular ways. CoreFoundation and similar low level APIs on Apple platforms use opaque data types created by functions. So Apple is again free to make changes to data structures, add new functions, and not break compatibility in a way that prevents existing app builds from dynamically linking against the new versions of the frameworks. It’s a little more work to do the same in C++, but the C API is generally useful in more situations anyways so there’s not much reason to do opaque types in C++. Swift can interop with C but not C++ (yet), which happens to make it easier to drop down to these lower level APIs than if they were C++.

But the thing is, Swift didn’t freeze the ABI until Swift 5 or so. This step is critical for being able to build an app with Swift 5 and then being able to link against libraries included in a newer OS built with Swift 6 in the future. It was only then that Apple could start shipping Swift frameworks in the OS. So Swift’s suitability for these sort of frameworks is relatively new. But it was an original goal of the language from the start, just one that was kicked down the road until they could nail down other key things they wanted to get right before freezing the ABI.
It is a very quick general statement at the beginning, just a “why (pre-swift) does Apple use 4 languages in its ecosystem” kinda thing. The relevant part is 2:52-4:10 but it’s more of a couple of offhand comments about C/C++ vs ObjC/C++, that former are really bad at creating stable APIs while the latter a really good for doing so. There was little other context and no reasons given and wasn’t really the point of the talk (which was about gradually replacing C++ with Swift in a huge codebase). The comments just piqued my interest as to why. With your post, I think I understand now.

Thanks!

On a different topic I was watching a talk by Herb Sutter about creating a C++ successor language and he was extremely impressed with and complementary of Swift’s approach with respect to ObjC (and TypeScript vs JavaScript). Basically holding it up as a model for how C++ 2.0 whether it be CPPFront/Carbon/etc … should be designed.
 
Last edited:

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
It is a very quick general statement at the beginning, just a “why (pre-swift) does Apple use 4 languages in its ecosystem” kinda thing. The relevant part is 2:52-4:10 but it’s more of a couple of offhand comments about C/C++ vs ObjC/C++, that former are really bad at creating stable APIs while the latter a really good for doing so. There was little other context and no reasons given and wasn’t really the point of the talk (which was about gradually replacing C++ with Swift in a huge codebase). The comments just piqued my interest as to why. With your post, I think I understand now.

Another trick Swift has up it’s sleeve is that @inlinable is controllable, so you can specify if it is safe to inline across modules or not. Rather important.

On a different topic I was watching a talk by Herb Sutter about creating a C++ successor language and he was extremely impressed with and complementary of Swift’s approach with respect to ObjC (and TypeScript vs JavaScript). Basically holding it up as a model for how C++ 2.0 whether it be CPPFront/Carbon/etc … should be designed.

Swift is a surprisingly flexible language. It provides a lot of high-level guarantees, while at the same time being usable for low-level code.

For example, I wrote a small service for Raspberry Pi devices to control aquarium lighting. It is written in Swift, and uses a couple small libraries that effectively add Swift ”drivers” for some of the hardware. I took the approach of trying to make the core library that exposed hardware registers type safe, and it worked.

Because structs are value types, a lot of the same memory layout rules apply as they would in C. So a struct with a single 32-bit integer property is a single 32-bit integer in memory. This means that I can memory map the register’s location using UnsafeMutablePointer using the struct type and it would just work. I defined registers that used bitfield controls as OptionSets, and it all made things so much simpler to be able to union what was in the register with a couple more typed options and then write the whole thing back. The same trick worked well with I2C making it easier to send control words to devices that only allowed named, known settings to be used in code.

And ultimately the service I wrote still controls my aquarium lights today, 5 years later. I’ve fixed maybe 2 bugs in the last 3 years, and run it 24/7 except when the power fails. The SD card recently gave out and I had to reinstall the whole OS, but that was maybe 15-20 minutes of downtime after 5 years. I really like Swift for these sort of small services where the end goal is reliability.
 

dada_dave

Elite Member
Posts
2,163
Reaction score
2,148
Another trick Swift has up it’s sleeve is that @inlinable is controllable, so you can specify if it is safe to inline across modules or not. Rather important.



Swift is a surprisingly flexible language. It provides a lot of high-level guarantees, while at the same time being usable for low-level code.

For example, I wrote a small service for Raspberry Pi devices to control aquarium lighting. It is written in Swift, and uses a couple small libraries that effectively add Swift ”drivers” for some of the hardware. I took the approach of trying to make the core library that exposed hardware registers type safe, and it worked.

Because structs are value types, a lot of the same memory layout rules apply as they would in C. So a struct with a single 32-bit integer property is a single 32-bit integer in memory. This means that I can memory map the register’s location using UnsafeMutablePointer using the struct type and it would just work. I defined registers that used bitfield controls as OptionSets, and it all made things so much simpler to be able to union what was in the register with a couple more typed options and then write the whole thing back. The same trick worked well with I2C making it easier to send control words to devices that only allowed named, known settings to be used in code.

And ultimately the service I wrote still controls my aquarium lights today, 5 years later. I’ve fixed maybe 2 bugs in the last 3 years, and run it 24/7 except when the power fails. The SD card recently gave out and I had to reinstall the whole OS, but that was maybe 15-20 minutes of downtime after 5 years. I really like Swift for these sort of small services where the end goal is reliability.
I think what also impressed him was that they did all that while also maintaining interoperability with ObjC without requiring any shims or boilerplate. Basically there’s a lot of talk about successor languages for C++ and Herb held up two examples of languages Swift and Typescript as to how it should be done. And so far the most promising experiments seem to be following that path.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Objective-C doesn’t have this problem though, as the type is fully hidden behind messages to the object that get resolved at runtime (minus some optimizations that happen). So I am free to manipulate the internals of a type in Objective-C, as well as add new methods to the type, without breaking compatibility of users on different versions of the SDK. I can build my app using the version of UIKit in iOS 13, and on iOS 15, my app loads the iOS 15 version of UIKit. I get all the nice bugfixes and improvements that don’t require adopting new APIs without having to rebuild and deploy a new version.
To add a bit on @Nycturne's excellent comment: I believe this is the case ever since Objective-C 2.0 was released (2006).

While Objective-C used to hide everything under objc_msgsend before then, there was a painful limitation before Objective-C 2.0: when linking to a dynamic library (for example, you app trying to call to a system-provided framework like AppKit), the methods or properties for a given class were called using a hard-coded offset from the class' base address (the base class is provided by the dynamic linker, aka dyld).
So, in order for the frameworks to maintain ABI stability, it was not possible to remove or even rearrange methods in an existing class, or the apps compiled for previous versions of the framework would crash by trying to call the framework's functions at the wrong offsets.

This particular post was quite informative regarding how all this works under the hood. I had been just reading about all this a couple of weeks ago after going down some rabbit-hole at work, never thought I'd be discussing Objective-C any time soon 😂.
 

dada_dave

Elite Member
Posts
2,163
Reaction score
2,148
To add a bit on @Nycturne's excellent comment: I believe this is the case ever since Objective-C 2.0 was released (2006).

While Objective-C used to hide everything under objc_msgsend before then, there was a painful limitation before Objective-C 2.0: when linking to a dynamic library (for example, you app trying to call to a system-provided framework like AppKit), the methods or properties for a given class were called using a hard-coded offset from the class' base address (the base class is provided by the dynamic linker, aka dyld).
So, in order for the frameworks to maintain ABI stability, it was not possible to remove or even rearrange methods in an existing class, or the apps compiled for previous versions of the framework would crash by trying to call the framework's functions at the wrong offsets.

This particular post was quite informative regarding how all this works under the hood. I had been just reading about all this a couple of weeks ago after going down some rabbit-hole at work, never thought I'd be discussing Objective-C any time soon 😂.
Thanks!
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
I think what also impressed him was that they did all that while also maintaining interoperability with ObjC without requiring any shims or boilerplate. Basically there’s a lot of talk about successor languages for C++ and Herb held up two examples of languages Swift and Typescript as to how it should be done. And so far the most promising experiments seem to be following that path.

Starting with OS X, Apple seemed to internally commit to the approach of iterative platform improvements. Always make it possible to adopt new things while still also being able to use the old. Then cut off the old at some point in the future. Carbon being a good example of this. It bridged Classic to OS X, made it possible for apps to start adopting Obj-C piecemeal, and then phased out with the 64-bit transition. Swift has followed the same approach. Although things like Obj-C compatibility only work on OS X. Linux Swift has no compatibility layer, IIRC. So I get it, but it's ultimately weirder if Apple didn't do it this way. This approach has let Apple slowly unify lower level frameworks across their platforms in a way that Microsoft failed to do multiple times (although UWP does seem to be getting some use).

Typescript I have a hard time thinking of as a successor language. It's transpiled from Typescript to Javascript and ultimately vulnerable to all the same things that Javascript is. It ultimately tries to bolt on functionality which is useful, but all too easy to ignore/defeat.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
So, in order for the frameworks to maintain ABI stability, it was not possible to remove or even rearrange methods in an existing class, or the apps compiled for previous versions of the framework would crash by trying to call the framework's functions at the wrong offsets.

Yeah, there's a lot of evolution in Obj-C and the compilers here that I'm glossing over. It gets even more funky because Obj-C uses hash table dispatch in the "slow" path which is immune the offset issues. But there's also vtable-style optimizations which lead to issues like you see here, which C++ also shares.

And yes, data structure layout (ivar placement) make things tricky for C, C++ and Obj-C. I've seen some creative solutions to this, especially in C.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
Yeah, there's a lot of evolution in Obj-C and the compilers here that I'm glossing over. It gets even more funky because Obj-C uses hash table dispatch in the "slow" path which is immune the offset issues. But there's also vtable-style optimizations which lead to issues like you see here, which C++ also shares.

And yes, data structure layout (ivar placement) make things tricky for C, C++ and Obj-C. I've seen some creative solutions to this, especially in C.
Compilers and language design is such a fascinating topic. I lack much of the context of how things were before Swift came around (I started programming for Apple platforms after Swift was well established). So many of Swift's language design decisions seem natural to me, to the point that I don't think about them much, only to later discover that those decisions aimed to solve a common issue that previous languages had. And I think that's a markedly positive thing, finding a solution so simple and elegant that it feels natural, when the status quo used to be something completely different, is the hardest thing to do.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
Compilers and language design is such a fascinating topic. I lack much of the context of how things were before Swift came around (I started programming for Apple platforms after Swift was well established).

I was the weird guy in class with a used Lombard PowerBook G3 in college running OS X for embedded systems classes. I built gcc cross compilers so I could do the homework and class projects on that machine. I did stuff from PIC to 286 assembler on that thing. While at the same time learning Obj-C in my spare time. Enough that I used Obj-C for client code in our final project to talk to the embedded system, as nobody else could code a GUI to save their lives. Let alone the jank I put together in OGL.

I still kinda miss it if I’m honest. But my M1 is every beat a beast in the same way.

Honestly if you can’t experiment with weird setups in college, when do you? :)

Swift's language design decisions seem natural to me, to the point that I don't think about them much, only to later discover that those decisions aimed to solve a common issue that previous languages had. And I think that's a markedly positive thing, finding a solution so simple and elegant that it feels natural, when the status quo used to be something completely different, is the hardest thing to do.

It’s another reason (as an engineer) I like Apple’s approaches. Iterate, learn, and then make measured leaps. But never sit still.

I still wonder why Win32 is still a thing these days. But then I have to remember that AppKit is about as old.
 

Andropov

Site Champ
Posts
617
Reaction score
776
Location
Spain
I was the weird guy in class with a used Lombard PowerBook G3 in college running OS X for embedded systems classes. I built gcc cross compilers so I could do the homework and class projects on that machine. I did stuff from PIC to 286 assembler on that thing. While at the same time learning Obj-C in my spare time. Enough that I used Obj-C for client code in our final project to talk to the embedded system, as nobody else could code a GUI to save their lives. Let alone the jank I put together in OGL.

I still kinda miss it if I’m honest. But my M1 is every beat a beast in the same way.

Honestly if you can’t experiment with weird setups in college, when do you? :)
Sounds like a fun experience :). I kinda wish I had lived through the PowerPC G3-G5 era as a computer enthusiast, but I was too young at the time to know much or be excited about computers. By the time I became interested in computers, I had an old PowerBook G4 (it was around 2008), and the PowerPC transition had been long completed. Which is a shame, because I really like some of the G4-era Macs Apple released (iMac G4, G4 Cube...), and it feels like in the "aluminum era" of Apple every computer was substantially more boring.

I still wonder why Win32 is still a thing these days. But then I have to remember that AppKit is about as old.
I have very few interactions with Windows, so I'm not very curious about its history (nor the million different frameworks they seem to have for building UIs now), but I am curious about the older Apple frameworks (like Carbon). It's a part of history I've heard very little about.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
Sounds like a fun experience :). I kinda wish I had lived through the PowerPC G3-G5 era as a computer enthusiast, but I was too young at the time to know much or be excited about computers. By the time I became interested in computers, I had an old PowerBook G4 (it was around 2008), and the PowerPC transition had been long completed. Which is a shame, because I really like some of the G4-era Macs Apple released (iMac G4, G4 Cube...), and it feels like in the "aluminum era" of Apple every computer was substantially more boring.

Yeah, the Titanium G4 is still a machine I wish I owned when it was relevant, but I was too young to make the cost work at that point. But the Lombard cost me 200$ US and a fresh battery. These days, it seems like the paint they used doesn't hold up well, and I'm a stickler for condition of anything I collect.

I have very few interactions with Windows, so I'm not very curious about its history (nor the million different frameworks they seem to have for building UIs now), but I am curious about the older Apple frameworks (like Carbon). It's a part of history I've heard very little about.

It's surprisingly boring to be honest. Win32 and Classic Mac APIs are very similar: you own your event loop which creates some problems, and there's a lot of low level APIs. Carbon is really the classic API cleaned up to fix things such that it could be a library that could be implemented on MacOS 9 and OS X. Surprisingly little changed beyond putting a lot more things behind opaque types, sealing up the event loop behind a messaging system similar to AppKit, and getting rid of the "LM" APIs (Low Memory calls that jumped straight into the kernel). But between it and the low level APIs being C on OS X, it made it rather easy to take your pure C/C++ classic app and turn it into something you would then build via Project Builder (Xcode) in the years to come. And once you were there, Objective-C taunted you with AppKit and Interface Builder.
 

Nycturne

Elite Member
Posts
1,139
Reaction score
1,488
It seems like every time I have to refactor a piece of code, I learn new SwiftUI tricks for cleaning it up and making it easier to read, and making better use of composition.

As an example, here's an actual view in my app's media browser which produces similar results on both iOS and macOS, and I'm quite happy with the readability for the code at this level:

Swift:
struct AlbumsView: View {
    @AppStorage("MediaBrowser.Albums.ViewMode") private var viewMode: ViewMode = .icons
    @AppStorage("MediaBrowser.Albums.ViewSort") private var sortMode: ViewSort = .title
    @Environment(\.activeLibrary) private var activeLibrary

    var body: some View {
        MediaBrowserChrome(title: "Albums") {
            MediaCollectionView<Album>(
                predicate: activeLibrary.predicate(),
                viewMode: viewMode,
                viewSort: sortMode,
                configuration: { item in
                    CoverViewConfiguration(
                        label: item.title ?? "<< ALBUM TITLE >>",
                        caption: item.albumArtist ?? "<< ALBUM ARTIST >>"
                    )
                }
            )
        }
        .lastUpdatedDate(publisher: activeLibrary.publisher(for: \.lastFetchedAlbumsAt))
        .browserViewModes($viewMode, allowed: [.icons, .list])
        .browserSortModes($sortMode.animation(), allowed: [.title, .artist, .releaseYear, .recentlyAdded])
        .librarySync(id: "fetchAlbums", action: { service, library in
            try await service.fetchAlbums(library: library)
        })
    }
}

This is the latest iteration, and some of the features:

- MediaBrowserChrome wraps platform specific bits that are re-used in many places. It also handles some iOS vs macOS specific behaviors.
- MediaCollectionView is my generic view that handles Grid/List views for pretty much everything. It's probably the least well-factored at the moment, but it does use @FetchedResults. Apple does allow modifying the FetchedResults wrapper these days, but they took it away with @Query. What they want folks to do is reconstruct the query (because it's cheap) and have a parent view own things like sorting / filtering state. So I've done that here. It's quite performant, and with the generic MediaCollectionView, I don't have to duplicate anything so long as it follows the rule of "Artwork, Title, Subtitle".
- browserViewModes/browserSortModes creates wrappers for both Environment and Preference. Environment makes it possible for child widgets in the Chrome view to change the state via the binding. Preference pushes the state up for the macOS menu bar, which also modifies this binding. The end result is that the experience is quite natural on iOS and macOS.
- librarySync is an async task that accesses environment on your behalf via a ViewModifier. It also handles providing performance and error diagnostics for the task, but it's used here to make it possible to describe a "fetch from service and update CoreData" task with very little code, while also being able to get diagnostics captured if it fails. Since it's a view modifier, I can hook it up to refresh logic in the future and everything that uses .librarySync() will pick up the behavior automatically.
- lastUpdatedDate is pure syntactic sugar for the Chrome. It composes a LastUpdatedLabel() view depending on platform in itself, and that label listens to the publisher. This makes it easy to listen to a CoreData object, or to the activeLibrary in this case, which is a custom wrapper, ensuring that the user knows when the last update to CoreData succeeded.
- activeLibrary is bog standard environment stuff. When the user selects a a library from the list of available ones, the activeLibrary is known to hold onto what that current library is, so that the UI can easily ensure it only shows the content of that library. So it has some utility functions around building predicates.

You really do need to approach building Views the same way Apple does, and things will work quite well. It's just a shame that Apple shies away from the more complex UI designs in their examples. It takes many hours to piece this sort of thing together in your spare time, and Apple at least in the past hasn't had great examples of how to compose this sort of thing.
 

Attachments

  • Simulator Screenshot - iPad Pro (11-inch) (4th generation) - 2023-09-23 at 14.43.50.png
    Simulator Screenshot - iPad Pro (11-inch) (4th generation) - 2023-09-23 at 14.43.50.png
    733.5 KB · Views: 16
  • Screenshot 2023-09-23 at 2.32.27 PM.png
    Screenshot 2023-09-23 at 2.32.27 PM.png
    799.4 KB · Views: 15

casperes1996

Power User
Posts
185
Reaction score
171
Apparently Apple has created a new open source data format language with conversions to JSON/YAML, seems to get positive reviews:


Unsurprisingly integrates with Swift. Thought it might be of interest!
What was most interesting to me was honestly how quiet Apple has been about it. Nothing in the Developer app, nothing on developer.apple.com that I noticed at least. I found out through The Primeagen on YouTube and subsequently read on their GitHub and webpage for it. Also the fact it's written in Java/Kotlin is a bit curious to me. And on their website (last I checked at least) they noted having an x86 macOS build and x86+AArch64 Linux builds. That said, they did have an AArch64 macOS build their documentation just didn't mention it.
Personally I'm not sure exactly how useful this will be; If it will actually simplify configuration and get your out of a pickle or if it'll just be another dependency without really adding enough value to matter. Time will tell - Certainly configuration validation can be useful at least
 

ArgoDuck

Power User
Site Donor
Posts
106
Reaction score
168
Location
New Zealand
Main Camera
Canon
^ yeah, maybe it’s just a ‘genius’ idea this team came up with and that they’ve been allowed to release and see how it flies…or could be part of some long term strategy in which Apple’s development teams expect to increasingly need something like this. As you say, time will tell.
 
Top Bottom
1 2