iCalvin

SwiftFest

On the train home after SwiftFest. What a great experience!

I got to deliver my first conference talk yesterday, and although I still wasn’t feeling 100% great about it, I walked off stage without feeling embarrassed and I talked to a handful of people who said they enjoyed or got some value out of my talk, which was all I was hoping for out of this experience.

It was really great to be able to spend some time with the speakers, many of whom I’ve known and respected for a long time without them ever knowing me before now. And it was really great to be able to talk to some of the attendees about their experiences getting into the iOS development community.

It was two and a half great days of learning, sharing, laughing. Everything I could have hoped for.

A Week With SwiftUI

Wow.

I can’t believe how packed this week has been. I was all prepared to get lots of new stuff to play around with, from Catalyst (née Marzipan) to MultiWindow support on iPad. And then some of the other stuff that I wasn’t anticipating like Sign In w/ Apple and some renewed opportunity of Today widgets were also catching my interest. But what really blew me away Monday was SwiftUI.

When Swift came out in 2014 I played around with it for a few hours but moved on to a more familiar language for iOS 8 work. Because of that I never really immersed myself in Swift until late 2016 and was playing catch up. That memory came up immediately while watching the SwiftUI announcement on Monday. Especially with some looming architectural changes coming up, and the potential for a SwiftUI development flow on iPadOS, this really seemed like where I wanted to focus my learning this week. And focus I did 😂

Once I finished the SwiftUI Tutorial provided by Apple I broke open my blog posting app project again. I wanted to migrate that to macOS this week, and SwiftUI seemed like as good a way to do that as any. This was the first of many times I wished I had read the release notes before getting started. They clearly state what I soon discovered, that Catalyst apps running on macOS using Swift UI aren’t rendering, I just saw an empty black window 🤷‍♂️. Easy enough to stick to iPad for now, and once that’s fixed run it on Mac and start playing around with windowing APIs.

Focusing pretty much exclusively on SwiftUI this week I was able to get a working port of my blog management app, and I’m actually pretty damn proud of it.

Screenshot of app in light and dark mode

So let’s look at some code! 👀


struct HomeView : View {
    @Binding var state: BlogEditingState
    var body: some View {
        return NavigationView {
            ComposeView(state: self.$state)
                .padding(.bottom)
                .navigationBarItems(
                    leading: ArchiveButton(archive: self.$state.archive),
                    trailing: SendButton(post: self.$state.writePost))
                .navigationBarTitle(
                    Text("@iCalvin")
                    displayMode: .large)
        }
    }
}

👆 This is the entire view for the main screen of the app. Obviously there’s more going around nested in that innocent looking ComposeView, but you can’t say this new framework doesn’t clean things up a bit. One struct with a body variable and some state, rather than a UIViewController with a handful of lifecycle functions.

The framework helped me think about how I wanted to organize stuff, but I did end up finding some blocking issues, which I wanna talk about in some more detail here. With a few design tweaks here and there though I ended up with something that worked with the current state of SwiftUI

First, I wanted to say that the initial documentation is pretty disappointing. Pretty much anything that wasn’t done in the tutorial had no documentation at all, which was really frustrating.

Screenshot of Archive


struct PostListView : View {
    var posts: [Post]
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                VStack() {
                    ForEach(self.posts) { post in
                        PostSummaryView(post: post)
                    }
                    }.padding(.horizontal)
                    .frame(width: geometry.size.width)
                }.frame(width: geometry.size.width)
        }
    }
}

The first issue I had came from multi-line text. I wanted a scrolling list of my post archive, but whenever I added a Text component to a ScrollView it wouldn’t show me more than one line. I experimented around a bit with the .lineLimit(_ limit: Length?) function but it didn’t seem to be having any effect. One thing that confused me was that the behavior of setting the limit to 0 was different from a UILabel, where 0 value means as many as needed. In SwiftUI, you get this behavior by passing nil. Eventually (after filing a Radar) I noticed that this was called out in the release notes, so again I could have saved myself some time there. For now I’m just hardcoding a height onto the PostSummaryView depending on which type it is.


struct PostSummaryView : View {
    var post: Post
    var body: some View {
        HStack {
            if !(self.post.title?.isEmpty ?? true) {
                Text(post.title ?? "")
                    .color(UIColor(named: "text-prominent")?.swiftColor)
                    .lineLimit(nil)
                    .multilineTextAlignment(.leading)
                    .font(.headline)
                    .frame(height: 60)
            } else {
                Text(post.body ?? "")
                    .color(UIColor.label.swiftColor)
                    .lineLimit(nil)
                    .multilineTextAlignment(.leading)
                    .font(.body)
                    .frame(height: 120)
                }
            Spacer()
            }
            .padding()
            .background(UIColor.secondarySystemBackground.swiftColor)
            .cornerRadius(10)
    }
}

Another good call out is the GeometryReader component. It allows you to access the explicit size of the containing component, which helped a lot for configuring the scroll view with the correct width 👍. Even though it feels like a little bit of a hack, this is a much easier way of dealing with scroll views than on UIKit 💯

Screenshot of Tag View


struct TagViewer: View {
    @Binding var tags: [String]
    
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                HStack {
                    ForEach(self.tags.identified(by: \.self)) { tag in
                        return Text(tag)
                            .padding()
                            .background(UIColor.secondarySystemBackground.swiftColor)
                            .cornerRadius(44)
                            //.animation(.basic())
                            .transition(.move(edge: .leading))
                    }
                    }
                    .frame(height: geometry.size.height)
            }
        }
    }
}

The Tag editor was a bit tricky as well. I wanted the tag pills to have a nice bouncy feel about them, and for the new tag to animate in on commit. The animation itself was easy enough using the build in transitions, but I ended up finding another issue that seemed to result from the same autosizing bug in ScrollViews.

Screenshot of text bug

The animation itself works great, but the text rendering in the pill goes a bit crazy. For now I’ve just disabled the animation until a future build where hopefully this gets fixed.


var body: some View {
        func applyState() {
            self.tags.insert(self.draftTag, at: 0)
            self.draftTag = ""
        }
        
        return GeometryReader { geometry in
            HStack {
                HStack {
                    CommitImage()
                    TextField(self.$draftTag,
                              placeholder: Text("addTag")
                                .bold()
                                .color(UIColor.systemGray.swiftColor),
                              onEditingChanged: { editing in
                                // Show Close Button
                    },
                              onCommit: applyState)
                        .font(.footnote)
                        .frame(width: min(125, geometry.size.width / 4.0), height: rowHeight)
                        .clipped()
                }
                    .padding(.horizontal)
                    .background(UIColor.secondarySystemBackground.swiftColor)
                    .cornerRadius(8)
                Button(action: applyState) {
                    Image(systemName: "arrow.right.circle.fill")
                        .imageScale(.large)
                        .accessibility(label: Text("Add Tag"))
                }
                TagViewer(tags: self.$tags)
                    .frame(height: rowHeight)
            }
                .frame(height: rowHeight)
        }
    }

One issue I had was dealing with the @State variables. The compiler would complain if the state gets edited in any way from outside the body variable. But I remembered that you can define functions inline, so any state modifying functions I needed could just go in the body! This helped a lot with consolidating logic between multiple triggers. In this code tapping the Add Tag button or hitting return on the keyboard will trigger the applyState() function and save the new tag.

The core of this app is a big text field to let me draft posts, but because of the multi-line bug TextFields in SwiftUI will only show one line, which wasn’t gonna work. What I ended up doing until this gets fixed is wrapping a UITextView with a SwiftUI UIViewRepresentable view. It’s exposed with the very simple TextEditor View, and wraps the TextView_UI view containing and managing the UITextView.


public struct TextEditor: View {
    @Binding public var text: String?
    
    public var body: some View {
        TextField_UI(text: $text)
    }
}
struct TextField_UI : UIViewRepresentable {
    typealias UIViewType = UITextView
    
    @Binding var text: String?
    var onEditingChanged: ((String) -> Void)?
    var onCommit: (() -> Void)?
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.backgroundColor = nil
        textView.text = text ?? ""
        if let font = context.environment.font {
            textView.font = font.font_ui
        } else {
            textView.font = .preferredFont(forTextStyle: .body)
        }
        textView.textContainerInset = .zero
        
        textView.delegate = context.coordinator
        
        return textView
    }
    
    func updateUIView(_ textView: UITextView, context: Context) {
        if let font = context.environment.font, font.font_ui != textView.font {
            textView.font = font.font_ui
        }
        textView.text = text ?? ""
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        var field: TextField_UI
        
        init(_ field: TextField_UI) {
            self.field = field
        }
        
        func textViewDidChange(_ textView: UITextView) {
            field.text = textView.text
        }
    }
}

This relied on a few extensions, which map UIColor and UIFont values to SwiftUI’s Color and Font, which I used heavily throughout the app. UIColor now exposes helpful color scheme appropriate colors, and I wanted to use those heavily in the iOS 13 redesign.


extension UIColor {
    var swiftColor: Color {
        var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0
        getRed(&red, green: &green, blue: &blue, alpha: nil)
        return Color(red: Double(red), green: Double(green), blue: Double(blue))
    }
}

For the Font extension, it bothered me that UIFont and Font have totally different values in their TextStyle options, so I made an extension that will do a best effort of finding the right counterpart for the other platform.


extension Font {
    var font_ui: UIFont {
        return UIFont.preferredFont(forTextStyle: assertStyle().style_ui)
    }
    private func assertStyle(_ fallback: TextStyle = .body) -> TextStyle {
        guard let key = TextStyle.allValues.filter({ self == $0.value })
	  .map({ $0.key }).first else { return fallback }
        return key
    }
}
extension Font.TextStyle {
    static var allValues: [Font.TextStyle: Font] {
        return [
            .largeTitle: .largeTitle,
            .title: .title,
            .headline: .headline,
            .subheadline: .subheadline,
            .body: .body,
            .callout: .callout,
            .footnote: .footnote,
            .caption: .caption
        ]
    }
    var style_ui: UIFont.TextStyle {
        switch self {
        case .largeTitle: return .largeTitle
        case .title: return .title1
        case .headline: return .headline
        case .subheadline: return .subheadline
        case .callout: return .callout
        case .footnote: return .footnote
        case .caption: return .caption1
        default: return .body
        }
    }
}

There is obviously a lot more going on in this code, and I may be sharing it on GitHub so people can have a learning resource. I know I appreciated every bit of reference code I could find while putting this app together! But for now the initial impression I have of SwiftUI is that it’s a really clean, modern rethinking of Apple’s platform tools, and one I am very much going to be prioritizing in the next few years.

As for the app, still lots I wanna do obviously. Improving large screen layout, adding the new context menus to the Archive to handle some quick actions, etc. There will be plenty of time for me to explore that, and I can’t wait to get started!

Have a fun summer everyone! It’s gonna be a busy one 😅

Next up to explore is the new SceneDelegate!

WWDC ‘19 Plans: Remote Edition

It seems like everyone else is talking about what they’re doing all week, and just because I’m home during WWDC I don’t wanna miss out on all the fun. My plans are essentially to lock myself in my office for the bulk of the week, and try to immerse myself with new APIs and the week’s session streams as much as possible. I’ve been warning my wife that I’d be little to no help around the house this week, and so I took the last few days to clean up to a fresh state before my week of neglect.

Monday morning I’ll be heading into Cambridge to meet up with CocoaHeads Boston at MIT, where they’ve booked a room to watch the Keynote and the State of the Union. It’ll be fun to have people there to react and be excited about stuff during and in-between the events.

For the rest of the week aside from a few walks or lunch trips I’ll probably be in my office for the bulk of it, so while everyone else was traveling to San Jose this weekend I’ve been cleaning my office and prepping my workstation for a week of learning and tinkering. I haven’t yet settled on a a desk configuration that I’ve stuck with for more than a month, but fingers crossed this new one satisfies me long enough to get through Dub Dub.

Wide view of office

I’ve been using a spare bedroom as my office since we moved to our new house last fall, and bit by bit it’s been coming together into a really confortable space that I love spending time in, which is great considering I work from home these days and spend a lot of time in here.

Front view of desk

I got the Jarvis standing desk in January, which gave me a much needed bigger desk space. I use it standing when I’m doing podcasts and sometimes when I’m working, but most of my programming happens seated, so I tuck away the standing mat. Repainting was the big job I wanted to tackle before I started working from home, and my main goal was to brighten up the naturally dark room, which pretty much everyone I’ve video-chatted with has confirmed worked. The bright orange walls experiment thankfully haven’t overwhelmed me, and I’ve really enjoyed playing around with the color scheme.

MacBook Pro w/ many, many stickers

My primary machine is a 2016 MacBook Escape (13” non-TouchBar model), which I got soon after launch. I’ve lucked out and never had any keyboard issues and actually really like the butterfly keys, but I primarily use it in clamshell mode which may color my opinion. This was the first machine I’ve ever covered with stickers, and it definitely started a bit of a habit. Right now it’s backing up to a Samsung T5, which is an impresssively cool little drive. I’ve got a second one all ready to run the macOS beta, assuming it works fine booting from an external drive.

Display view

Keyboard & Trackpad view

I’m really pleased with my single-cable workstation. I have the 21” LG UltraFine 4K which I got with the laptop in 2016 when Apple was discounting all of their USB-C accessories, and I’ve been all in on USB-C ever since. I keep a Magic Keyboard and Trackpad wired with extra long Lightning to USB-C cables, which allows me to easily swap the computer part of the workstation with a single USB-C cable and not have to re-pair the input accessories. Turns out wired accessories are pretty nice after all 🤷‍♂️. Most of the time I’m swapping between my personal and work MacBook Pro, but I can also plug in my iPad Pro and use the external screen and keyboard (and hopefully after tomorrow the trackpad!) with iOS, which is helpful when doing lots of writing.

iPad Pro

Otherwise I keep my iPad to the side in a Viozen Stand with it’s own Magic Keyboard. I’ve been saying since I bought this device in November that I bought it in hopes of a great iPad update in iOS 13. As much as I love it today and use it for lots of day-to-day stuff that isn’t Xcode related, I can’t wait to see what it’s able to do come tomorrow.

Microphone on arm mount

H4n Pro Audio Interface

For audio I have an Audio Technica mic and a use a Zoom H4n Pro as a USB audio interface to the workstation. My only gripe with this set up is every time I connect to a new computer I have to go through the menus on the interface to enable USB mode with the new computer, but otherwise it’s been working great.

Charging Station

To the right I have a little charging station with an inductive charger and a Lightning stand. This is also where I have the only USB-A connector on my desk, where I have my USB-C inductive charger powered.

Secondary desk view

Smart Keyboard Folio and many more stickers

In the window nook I keep a secondary desk, where I keep my 2017 iPad Pro and another small charging station. This desk is usually covered with mail and other papers, but I’ve made sure to clean it off for the week so I can keep spread out a bit if I need to.

Sonos Shelf

Finally, behind me I have a little table for my Dungeons and Dragons books and dice, where I also keep my Sonos One. My friend Bob won this at the ATP live show last year, and once it was updated with AirPlay 2 I picked it up for my office. I’ve kept a HomePod in here for music before, but I actually found myself wanting to be able to say “Hey Siri” to my iPhone rather than the HomePod, and I preferred just having a simple AirPlay 2 destination for my iPad where I control music or podcasts, rather than a “smart speaker” I had to use my voice to control. I’m pretty certain I have never once used this for anything other than as an AirPlay destination for an iOS device, and for that it works perfectly!

Side view of desk

So that’s the environment I’m hoping to keep my productive and sane this week while I dig through all the new goodies Apple is gonna drop tomorrow. I’ve got a laundry list of projects I can work on that I’ll be picking from once we see exactly what happens tomorrow, but hopefully I’ll have more to share here later this week!

Experimenting With Windowing on iPad With PanelKit

I’m typically pretty reluctant to lean on third party libraries in my code, especially when it wraps core UI interaction models, because they can make it even more difficult to prepare for new updates during the post-WWDC period of the summer. However, the first of a week long series of scoops at 9to5Mac called out a feature that has been long rumored for iOS, the ability take popover-like panels and move them around freely on screen in windows.

Explicitly, Gui Rambo called out that the feature that would ship with iOS 13 would be similar to the PanelKit project, which was archived a year ago when the creator got hired at Apple. Well that was enough for me to take a closer look at PanelKit and get a sense for what it could do, and what sort of considerations would be needed when we eventually move to a windowed paradigm on iOS.

I spent some time on CastKit this week talking about this project and I’m really excited to share in a bit more detail here. I maintain an app to post to this blog as well as my social accounts, and I’d been meaning to update that app anyway.

When I designed the current version of the app during the iOS 11 beta period I just went the easy route and put everything in a tableView, which worked out fine enough, but I didn’t give myself quite enough space in the Body field, instead opting for a focused BodyDetailViewController. The thinking was that it would be enough for a quick tweet and the detail was for anything more, but in practice that extra UI was filling up the screen and I used the detail view much more often than I would have liked.

Main Compose/Edit and Body Detail Views

But by creating ViewControllers for all of these PostInfoComponents and conforming them to PanelContentDelegate we can give PanelKit enough to present these view controllers in resizable, draggable windows.


extension PostTitleViewController: PanelContentDelegate {
    var preferredPanelContentSize: CGSize {
        return CGSize(width: 400, height: 150) }
    var minimumPanelContentSize: CGSize {
        return CGSize(width: 250, height: 100) }
    var maximumPanelContentSize: CGSize {
        return CGSize(width: 1000, height: 500) }

    var preferredPanelPinnedWidth: CGFloat {
        return 400 }
    var preferredPanelPinnedHeight: CGFloat {
        return 150 }

    var shouldAdjustForKeyboard: Bool {
        return true }
}

Main Compose/Edit and Info Panels

This viewController gets passed to a PanelViewController initializer, which also takes in a PanelManager. This manager is a protocol that deals with mainting the panels size, location, and ‘pinning’ state.

Once the PanelViewController is created I make sure to put it in the [Panel] array of the PanelManager (If your panel gets removed from this array while on screen, it will stop responding to drag or resizing events), and give that view controller to the PostContentViewController to present as a popover. The PostTitleViewController will be embedded within a navigation controller in the PanelViewController with a bar button to float the panel.


// PanelManager
private func addPanel(_ panel: PanelViewController) {
    panels = panels.filter({
        $0.contentViewController?.view.superview != nil })
    styleNavBar(panel.panelNavigationController.navigationBar)
    panels.append(panel)
}
func createTitlePanel() -> PanelViewController? {
    guard let titleViewController = createTitleViewController()
        else { return nil }
    let titlePanel = PanelViewController(with: titleViewController,
                                         in: self)
    addPanel(titlePanel)
    return titlePanel
}
// PostContentViewController
@IBAction func titleButtonTapped(_ sender: Any) {
    guard let titlePanel = panelManager?.createTitlePanel(),
        let button = sender as? UIBarButtonItem
        else { return }
    titlePanel.modalPresentationStyle = .popover
    titlePanel.popoverPresentationController?.barButtonItem = button
    present(titlePanel,
            animated: true,
            completion: nil)
}

One of the requirements of the PanelManager protocol, along with the managerViewController that will be presenting these panels, is the primary panelContentView. This is the main focused view you want to get out of your way if you pin another view controller. I want my PostContentViewController to be focused on the bodyField, so I let that fill the screen and define that as my panelContentView.

By doing this and setting allowPanelPinning = true on my PanelManager I’m able to pin view controllers to the edges of my content view.


class PostContentManager: PanelManager {
    var postContentViewController: PostContentViewController
    var managerViewController: UIViewController {
        return postContentViewController }

    var allowPanelPinning: Bool {
        return panelContentWrapperView.bounds.width > 350 }

    func maximumNumberOfPanelsPinned(at side: PanelPinSide) -> Int {
        switch side {
        case .right:
            return 3
        case .left:
            return 1
        default:
            return 2
        }
    }
}

Pinning Detail Views

I don’t allow floating or pinning panels on iPhone. I briefly experimented with that but wasn’t able to get it working, probably need to take a closer look at all the optional values on the PanelManager. What I do instead is put all of those PostInfoComponents view controllers within a MetaViewController panel, which just gets presented modally with a close button instead of an unpin button. And from the iPad that view can unpin any of those sub views into a new panel! There’s no limit to how deep you can go with presenting these panels from within each other, and believe me I tried 😆

Meta Details on iPhone

One thing I’m curious to try next, the date/time picker is really wide, and needs ~400pts to not clip the text. I’d like to play around with auto-adjusting constraints, curious to see how it looks if content dances around in the window while it’s being resized. More likely though I’ll just hide the year on the picker. I also want to do a lot more with the body field, make it a full featured editing view with Markdown preview.

This was only a weekend sprint, but already I’m very happy with the state of this, and it’s making me really excited to see how Apple implements this feature. I’m sure it will be pretty distinct from this library, but I really liked that this was almost entirely protocol driven and hope Apple at least brings that design pattern to production.

Either way it was a fun experiment into thinking with panels, and I’m glad I got a small preview of the work I’m gonna have ahead of me this June!

External Storage Rumors for iOS 13

New APIs in iOS 13 - Rambo @ 9to5mac

Man, these scoops from Rambo just won’t stop until June I guess?

Some interesting stuff in here, I’m particularly excited for a Search Siri Intent that will go a long way to actually making Siri Intents usable for lots of classes of apps.

One thing I’m concerned about however is the note about external device access APIs.

With a new API, apps will be able to capture photos from external devices such as cameras and SD cards, without having to go through the Photos app.

This doesn’t need to mean that there won’t be Files level access to external storage, but suddenly I’m concerned that they’ll just add an API to the same photo import access they already use in the Photos app and call that done until next year. Fingers crossed that this API is on top of general improvements to external storage access including allowing the contents to show in Files and be used by any document based app. 🤞