angelo on write.as

Building Swift things against the Write.as API

It feels like it was just yesterday when I shared some initial screenshots of the WriteFreely app prototype running on iOS and Mac.

(In fact, it was almost four weeks ago.)

I've been making a lot of progress on this, and I'd love to show you where the app is today.

Read more...

Work on the SwiftUI client for WriteFreely is coming along! The UI is well-enough in place that we can now start integrating the WriteFreely Swift package I wrote about earlier this summer. In fact, there's an open PR to kick that off — logging in and out of your WriteFreely instance (or, of course, Write.as).

But there's still a lot to be done! And as of today, there's a list of open issues under the milestone for the first beta.

These issues run the gamut between small enhancements like this one for changing the collection to which a post belongs, to big projects like implementing local storage.

And more work will come up as we go, because that's bug math for you.

But getting this list drafted is exciting. It means we've got a clear direction for building this thing, and —more importantly— it's publicly available, meaning that anyone can see what needs to be done, and makes it clear how to contribute.

So with that in place, I (again!) invite you all to have a look through the project's contributing guidelines, pick an unassigned issue, and mention it in the project's forum topic so we can help get you up and running with your contribution.

As I write this, you'll need Xcode 12β5 and SwiftLint installed on your Mac, and, if you want to be able to build and run the Mac app, you'll need to be running macOS 11β5. You don't need to be on the macOS 11 beta to work on the iOS app.

There's a lot of interesting work here, and we've gotten through a bunch of the growing pains that come with beta frameworks and operating systems. If you're interested in working on propagating state through a multiplatform SwiftUI app, we've got an issue for that. If you're happiest working on data persistence, we've got an issue for that. If async networking requests are your thing, we've got an issue for that.

(And as a reminder, the Swift package is also open to contributions!)


Enter your email to subscribe to updates:

You can also subscribe via RSS or follow @angelo@write.as on Mastodon.

I feel like I've been opening Apple's Feedback Assistant a whole lot this week while working on the SwiftUI client for WriteFreely.

That's normal! It's betas all the way down right now — building a new app from scratch, using a beta framework, in a beta IDE, running on a beta operating system. I'd be worried if everything worked normally, to be honest. Bug reports are part of life when you're this far along the bleeding edge.

But I'm getting ahead of myself here. Today, I want to talk a little bit about workarounds, so let me back up a little bit and talk about where we're at, today, in developing for Apple platforms.

In many ways, SwiftUI replaces the need for AppKit and UIKit. At least, that's the promise.

(It's not always the reality.)

The value of this, as I mentioned last week when discussing the promise vs. reality of SwiftUI multiplatform apps, is that SwiftUI's declarative nature lets us provide the semantics of a view, and the framework will essentially translate them into the actual presentation that works best for the particular platform it's running on.

I also mentioned how some things didn't quite work right, yet. That's normal! SwiftUI has been in developers' hands for barely over a year, so of course there's still work to be done. And while some would prefer to wait for the framework to mature, I'm going to go out on a limb here and say:

If you're starting work on a new app today, make it a SwiftUI multiplatform app.

I know, I know. I remember what it was like building an app in the early days of Swift and getting burned on the transition from Swift 2 to Swift 3, or trying to understand how to do something when even Apple's sample code was almost exclusively in Objective-C.

Five years later, I'm glad I'm all in on Swift, and five years from now, I'm willing to bet I'll be feeling the same way about SwiftUI.

In fact, the transition from AppKit and UIKit to SwiftUI will be even easier than from Objective-C to Swift, because this is more of a change in mindset than it is a change in language. Because most libraries were still written in Objective-C when Swift debuted, we needed (and still need) to use bridging headers to import them into an otherwise all-Swift codebase. The ecosystem is all-in on Swift now, and if SwiftUI doesn't do what you need —or do it reliably— yet, then Apple have made dropping into AppKit or UIKit from SwiftUI unbelievably easy.

This is the takeaway. As developers, we're used to finding workarounds to handle unexpected behaviour. We write our code defensively, fencing in a bug such that it's never exposed to the user. The reason I feel confident in recommending SwiftUI for your next project is because, while you'll invariably run into bugs, the workarounds are often trivial.

Let's get into an example.

"A segmented control with options for System. Light, and Dark appearance."

One common feature for modern apps is to be able to choose their colour scheme (i.e., light vs dark) independently of what the operating system is set to. At first blush, making this work in SwiftUI is really easy: just add the .preferredColorScheme() view modifier to your top-level ContentView and bind it to some state property:

import SwiftUI

@main
struct MyApp: App {
    // Set to .light for light scheme, or .none to follow the system setting.
    @State var selectedColorScheme: ColorScheme? = .dark

    var body: some Scene {
        WindowGroup {
            ContentView()
                .preferredColorScheme(selectedColorScheme)
            }
        }
    }
}

So, that's the promise. The reality is that I filed two bug reports yesterday and had to find a workaround to make this work.

The first (FB8383053) is that once you set .preferredColorScheme in your app to either .light or .dark, you can no longer unset that to get the app to follow whatever the system is set to.

The second (FB8382883) is that on macOS 11, setting .preferredColorScheme to .light isn't reliable. The app continues to follow whatever the system setting is; only a .dark scheme works as expected.

There are more details on both bugs in the README for this sample project on GitHub. And while frustrating, it's also unbelievably easy to drop into AppKit or UIKit from SwiftUI to work around this.

The WriteFreely SwiftUI client app uses a PreferencesModel ObservableObject for app-level preferences. Currently, there's only one: selectedColorScheme, which publishes the ColorScheme chosen by the user. That model becomes a @StateObject in the main App struct, to set the preferredColorScheme on its ContentView.

As I mentioned, however, that's not reliable, and instead we can drop into AppKit and UIKit when the user selects an appearance in the Picker:

import SwiftUI

class PreferencesModel: ObservableObject {
    // If we're in iOS, we need to get the UIWindow to apply the color scheme.
    #if os(iOS)
    var window: UIWindow? {
        guard let scene = UIApplication.shared.connectedScenes.first,
              let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
              let window = windowSceneDelegate.window else {
            return nil
        }
        return window
    }
    #endif

    @Published var appearance: Int = 0 {
        didSet {
            switch appearance {
            case 1:
                // To replicate selectedColorScheme = .light
                #if os(macOS)
                NSApp.appearance = NSAppearance(named: .aqua)
                #else
                window?.overrideUserInterfaceStyle = .light
                #endif
            case 2:
                // To replicate selectedColorScheme = .dark
                #if os(macOS)
                NSApp.appearance = NSAppearance(named: .darkAqua)
                #else
                window?.overrideUserInterfaceStyle = .dark
                #endif
            default:
                // To replicate selectedColorScheme = .none
                #if os(macOS)
                NSApp.appearance = nil
                #else
                window?.overrideUserInterfaceStyle = .unspecified
                #endif
            }
        }
    }
}

That's it. No having to import AppKit or Cocoa, no bridging headers, nothing. Just call the AppKit/UIKit methods you need right there, and now selecting your colour scheme works flawlessly across your app, in iOS and macOS.

Here's the change to PreferenceModel in the repository. My hope is that this gets fixed before iOS 14 and macOS 11 are released, and that I can uncomment the cleaner, multiplatform preferredColorScheme calls instead, but if not, the workaround isn't especially painful.

Now, about that toolbar…


Enter your email to subscribe to updates:

You can also subscribe via RSS or follow @angelo@write.as on Mastodon.

Happy August, friends! How about an update to last week's post on the SwiftUI app for WriteFreely?

Work progressed pretty well on the shared code last week, to the point that we've got a preliminary UI that displays a list of posts, which you can filter by collection. Tapping on any post loads it into an editor, where you can make changes to the title and content.

On iPhone and iPad, there's also a new post button that will create a new draft. Why only on iPhone and iPad? Well, pull up a chair, and let's chat a little bit about the gap between the promise and the reality of multi-platform SwiftUI apps.

While I was working on the post, I tweeted that I managed to flesh out some of the key UI features for the app across iPhone, iPad, and Mac in a day, thanks to SwiftUI and live previews in Xcode 12, which show you what your views will look like next to your code, and update in realtime as you code.

I further stated:

This is building a native app with the speed of iteration that I've only ever encountered when building for the web.

(I'm looking at you, HTML and CSS. I am not looking at you, React.)

So here's what that latest iteration looks like so far, on iPhone and iPad:

And, after I installed the macOS 11 (a.k.a. “Big Sur”) beta, I was able to build the Mac app to compare:

This is in no way a final UI! There's plenty of work still to be done, including replacing the unusual “picker” view for selecting the Collection with a proper sidebar, and adapting the spacing a little better on the Mac app (you can't preview anything for a Mac target, even in Xcode, until you upgrade your own machine to the macOS 11 beta).

The DX of SwiftUI, as illustrated by ToolbarItemPlacement

One thing that's very helpful in moving quickly with SwiftUI is how its declarative nature lets you specify the semantics of an element's placement in your view, and letting the framework figure out what that should mean on iPhone vs iPad vs Mac. A great example of this focus on developer experience (DX) is the ToolbarItemPlacement API, which lets you semantically state that a particular item that you add to a toolbar is a .primaryAction, or a .status element, and so on.

This is used in the TextEditor view to place a post status badge in the .status area and a publish button in the .primaryAction area. You can see in the screenshots I shared above how these elements are placed differently depending on whether it's being shown on an iPhone, an iPad, or a Mac — and I didn't have to write a single line of code to customize the difference.

The Letdown of SwiftUI, as illustrated by broken buttons

But life in SwiftUI is not all roses. And that's where the new-draft feature isn't quite working fully, yet. Building the iPhone and iPad versions of the app, tapping the new-draft button above the list of posts works beautifully — it slides a new draft into the list, and then opens it in the text editor for your next great essay. I was thrilled to have this working by the end of my workday on Friday.

I finished installing the Big Sur beta on my Mac this morning, excited to see how everything looked and worked in the Mac app. I expected to have some little UI refinements (that list of posts, for example, needs a healthy dose of padding), but otherwise figured that it'd be a similar experience to, at the very least, the iPad app.

And mostly, it is. The picker is replaced by a popup menu, and you can choose a collection to narrow down the list of posts. You can tap a post in the list, and it'll load it in the text editor, ready for changes. If the post's status changes, it'll instantly update the badge both in the toolbar and in the post list.

And then I tried to create a new draft. On the Mac, clicking on that new-draft button (which appears, interestingly enough, next to the post-status badge in the screenshot) does… nothing. Not a thing. No errors are thrown, no views are drawn, nothing.

The Reality of Multiplatform

I suppose I shouldn't use the word letdown, because “multiplatform” is not a panacea: the reality is that an app expects different things between an iPhone, an iPad, and a Mac. The issue I'm facing with that new-draft button isn't some failing of SwiftUI; it's that the paradigm for how a Mac app treats navigation vs buttons vs instantiating new windows is very different from that of an iPhone, and I haven't structured things the way the framework expects.

So, fine. I'll need to spend a little more time on adapting the Mac client. I would have liked it if it all Just Worked™, but I've got to say: despite a couple of little issues that I need to fix, I'm really impressed with just how much of it worked right out of the box, with no extra work on my part.

Native-app development may not yet happen at the pace of web development, but it's closer than ever.


Enter your email to subscribe to updates:

You can also subscribe via RSS or follow @angelo@write.as on Mastodon.

Hot on the heels of the developer-preview release of the [WriteFreely Swift package], I'm happy to announce that work has started on a new (but related) project: a WriteFreely multiplatform SwiftUI client app.

Read more...

Finding a tool that generates documentation from code comments has proven to be trickier than I expected.

Read more...

Releasing something as a “Developer Preview” has always given me a bit of anxiety, and not just for the usual “they're going to laugh at my code” impostor-syndrome stuff.

Read more...

Over the last few days, I've been working on wrapping the WriteFreely API in a Swift Package for use in your iOS and macOS apps. I'm happy to announce that an alpha version is now available!

Read more...

Enter your email to subscribe to updates.