iCalvin

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!