Simple, Elegant Approach to parsing data with API Clients in Swift

Without going into too much detail here, I wanted to highlight an approach I’ve taken to managing parsing of data that comes back from a webservice. It leverages best practices, such as using Codable to parse JSON, but also allows for some flexibility to adapt to your backend implementation.

As they say “Adapt Early” when it comes to dealing with data that comes from outside your app, or even from a 3rd party framework.

As such, I came up with an approach that keeps a lot of business logic associated with an endpoint and less with the API client, and uses Swift generics, and associatedtype on protocols.

I have a hard time explaining this, so you can just copy-paste this into a playground and have a look for yourself:

/// What your request types need to support to work with the APIClient
public protocol APIRequest {
    var baseURL: URL { get }
    var path: String { get }
    
    // other properties here, such as parameters: [String: Any]
    
    func request(authToken: String?) -> URLRequest
}

// then provide a default implementation
extension APIRequest {
    func request(authToken: String?) -> URLRequest {
        let url = baseURL.appendingPathComponent(path)
        return URLRequest(url: url)
    }
}

/// we can group such enums by service.  Consider LoginEndpoint, UserManagementEndpoint, etc.
enum Endpoint {
    case home
}

/// ... then conform separately if you need to
extension Endpoint: APIRequest {
    var baseURL: URL {
        return URL(string: "http://www.google.com")!  // make sure this is a real URL for your backend.
    }
    var path: String {
        switch self {
        case .home:
            return "/home"
        }
    }
    
    // etc.
}

typealias APIResult<T> = Result<T, APIClientError>

enum APIClientError: Error {

    case decodingError(error: Error)
    case noDataToDecode
    case httpError(error: Error)
}

/// A protocol for being able to transform incoming json data before converting it via Codable
/// and finally allows you to pass it, or another type completely back via the result() function.
/// You might have a struct called UsersResponse, with a property .users: [User].
/// this JSONParsing instance could have a ResultType of [User], even though
/// if's the UsersResponse type that is decoded JSON.

public protocol JSONParsing: Codable {
    
    associatedtype ResultType
    
    /// This is where you return a value as a result of the parsing.  Sometimes it could be the instance itself,
    /// or sometimes some derived property from this oftentimes intermediate data model.
    /// it's also in this method that you could do some data operations or fire off some notifications
    func result() -> ResultType
    
    /// if JSON returned from a webservice should be altered somehow before decoding begins
    /// if `true` the method `transform(_ responseJSON: [String: Any], from request: APIRequest?) throws -> [String: Any]` will be invoked.
    /// The return value will then be serialized to data, then deserialized via the `Codable` protocol.
    static var requiresInputTransformation: Bool { get }
    
    /// basically you can massage this incoming data, or if it comes in an unexpected format,
    /// you can throw an error
    static func transform(_ responseJSON: [String: Any], from request: APIRequest?) throws -> [String: Any]
}

// Provide some default implementation for conformance that ultimately results in it working as before
extension JSONParsing {
    
    public static var requiresInputTransformation: Bool { return false }
    
    public static func transform(_ responseJSON: [String: Any], from request: APIRequest? = nil) throws -> [String: Any] {
        return responseJSON
    }
}


/// parse data that was returned from the given request. First it checks if Decoder needs transformation, and transforms it.
/// Then attempts to use the Codable support of the Decoder type, then convert that decoded value to the give result type
/// via the result(from decoded) method
func parseData<Decoder: JSONParsing>(_ data: Data?,
                                     from request: APIRequest?,
                                     decodeJSONWith decoder: Decoder.Type) -> APIResult<Decoder.ResultType> {
    
    do {
        var dataToDecode = data
        if decoder.requiresInputTransformation {
            // then create json from the data if possible.
            // if no data, empty dict.  otherwise make json out of it, if you can't... empty dict.
            let json: [String: Any] = (data == nil) ? [:] : try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any] ?? [:]
            
            // transform the payload
            let transformed = try decoder.transform(json, from: request)
            
            // re-serialize
            dataToDecode = try JSONSerialization.data(withJSONObject: transformed, options: [.prettyPrinted])
        }
        
        if let jsonData = dataToDecode {
            let decoded = try JSONDecoder().decode(decoder, from: jsonData)
            return .success(decoded.result())
            
        } else {
            throw APIClientError.noDataToDecode
        }
        
    } catch let e as APIClientError {
        return .failure(e)
    } catch {
        return .failure(.decodingError(error: error))
    }
}



// Example Implementation

struct NamesResponse: JSONParsing {
    typealias ResultType = [String]  // in most cases your conformance requires you to declare a return type, then implement result()
    let names: [String]
    
    func result() -> ResultType {
        return names
    }
}

func print<T>(result: APIResult<T>) {
    switch result {
    case .success(let name):
        print("✅: \(name)")
    case .failure(let error):
        print("❌: \(String(describing: error))")
    }
}


let response = NamesResponse(names: ["Dave", "Steve"])
let responseData = try! JSONEncoder().encode(response)
let request = Endpoint.home
let result = parseData(responseData, from: request, decodeJSONWith: NamesResponse.self)
print(result: result)

print("eof")

I’m not certain many people read this blog, and that’s fine too; I put this here for my memory’s sake. 😉 Hit me up if you have questions.

WatchKit: A Coordinator pattern for Page-based Navigation

So, I’m very late to the watchOS party. I’m just now getting my hands dirty with this platform, and am having a lot of fun with it! It’s always great to be able to develop with a new piece of hardware while already knowing the fundamentals of doing so.

WatchOS isn’t that much different than iOS. You can tell they used this platform to promote SwiftUI; the scope of an app is small, and that includes the UI. I’m currently still using Storyboards and WatchKit, but you can tell how a lot of the WKInterfaceObjects have their analogue more in SwiftUI than in UIKit.

While getting accustomed to the platform, the first few apps I’ve knocked together favour the page-based navigation style. The thing about this is there’s no real controller for these pages, and not every use case will make Storyboard-based segue navigation possible. For example, you can’t have a next page segue from InterfaceController A to InterfaceController B, then from B back to A. At compile time it finds the circular reference.

No, basically all you have for managing which page is displayed are these two methods:

WKInterfaceController.reloadRootPageControllers(withNames names: [String], 
                            contexts: [Any]?, 
                         orientation: WKPageOrientation, 
                           pageIndex: Int)

and the instance method:

.becomeCurrentPage()

So that doesn’t really allow us to do much. Usually an app that displays pages displays them because they share something in common, so they probably have some common purpose. Enter the Coordinator.

Enter the Coordinator

The idea I applied here which is working brilliantly is the idea of a Coordinator pattern. The Coordinator is responsible for creating a “Scene”, and managing which WKInterfaceControllers will be in that scene. The nice thing here is your app can have many coordinators based on the current context the app is in.

For example, say I create a WatchApplicationController object, and organize it like this:

import WatchKit

class ExtensionDelegate: NSObject, WKExtensionDelegate {

    let applicationController = WatchApplicationController()
    
    func applicationDidFinishLaunching() {
        applicationController.interfaceMode = .roundOnCourse
    }
 
    // ... rest of implementation
}

Then let’s look at that class. (To give some more context, in this case my WatchApplicationController is for a watch app that is related to going golfing. Typically you either go practice at the driving range, or you go play a round of golf on a course.)

import WatchKit

class WatchApplicationController: NSObject {

    enum InterfaceContext: String {
        case modeSelect
        case drivingRange
        case roundOnCourse
    }
    
    var interfaceMode: InterfaceContext = .modeSelect {
        didSet {
            reloadUI(mode: self.interfaceMode)
        }
    }
    
    var rangeSessionCoordinator: RangeSessionCoordinator?
    var golfRoundCoordinator: GolfRoundCoordinator?
    
    func reloadUI(mode: InterfaceContext) {
        switch mode {
        case .modeSelect:
            break
        case .drivingRange:
            
            if rangeSessionCoordinator == nil {
                rangeSessionCoordinator = RangeSessionCoordinator()
            }
            
            rangeSessionCoordinator?.showMainScene()
            
        case .roundOnCourse:
            
            if golfRoundCoordinator == nil {
                let round = GolfRound.forTesting(numGolfers: 4)
                golfRoundCoordinator = GolfRoundCoordinator(golfRound: round)
            }
            
            golfRoundCoordinator?.showMainScene()
        }
    }
}

(Note here I haven’t yet included the aspect of the app that deals with the WatchConnectivity framework and passing data back and forth between the Watch and the iPhone, but you can tell that this class is likely going to be where those incoming messages from the phone will be handled.)

You can see that currently the app has 3 different “Scenes” (or Contexts) I want to show the user, based on the various features of the app. Ultimately, all these Coordinators are doing are owning a call to the method listed above WKInterfaceController.reloadRootPageControllers(...)

In the end, the basis for all my page-based navigation work with the following baseclasses, subclassed as required, likely to inject data models, and add methods to the coordinator as required for the UI. So here we go:

The Code Recipe: Your Copy-Paste job

Coordinator.swift

import Foundation
import WatchKit


extension Notification.Name {
    static let becomeCurrentPageRequest = Notification.Name(rawValue: "becomeCurrentPageRequest")  // the object is a String that matches the controller's identifier
}

protocol Coordinated {
    
    var coordinator: Coordinator? { get }
    // a unique identifier for addressing objects via notifications.
    var coordinationId: String { get }
    var pageIndex: Int { get }
    var totalPages: Int { get }
}

struct PageTransition {
    enum Kind {
        /// Just transition to the destination
        case show
        /// transition to the destination, stay there for `duration`, then return to the source
        case flashTarget(duration: TimeInterval)
    }
    let kind: Kind
    let sourceCoordinationId: String
    let destinationCoordinationId: String
}

struct CoordinatedConfig {
    let coordinator: Coordinator?
    let controllerIdentifier: String // this is given in the storyboard
    let instanceId: String
    let pageIndex: Int
    let totalPages: Int
    let context: Any?  // i.e. a data model that you'll have to cast.
}

class Coordinator {
 
    // their order in the array denotes their page index.  You should set these in your init
    var pageInstanceIDs: [String] = []
    
    // these are set when a Coordinated InterfaceController didAppear()
    private(set) var currentPageIndex = 0
    private(set) var currentCoordinatedId = ""
    
    // if become current page requests don't respond timely, we reload the scene from a specific page.
    private var transitionNotificationTimeout: Timer?
    
    init() {
        
    }
    
    func showMainScene(startingPageIndex: Int = 0) {
        print("You should subclass this method in your specific coordinator.  Typically you'll create a CoordinatedConfig as your contexts, and make sure the controller identifiers correspond to controllers that conform to Coordinated")
    }
    
    func move(with pageTransition: PageTransition) {
        
        switch pageTransition.kind {
        case .show:

            transition(to: pageTransition.destinationCoordinationId)
            
        case .flashTarget(let duration):

            transition(to: pageTransition.destinationCoordinationId)
        
            Timer.scheduledTimer(withTimeInterval: duration,
                                 repeats: false) { [unowned self] (_) in

                self.transition(to: pageTransition.sourceCoordinationId)
            }
        }
    }

    private func transition(to destinationId: String) {
        
        guard let pageIndex = self.pageInstanceIDs.firstIndex(of: destinationId) else {
            print("ERROR: You wanted to transition to a page that isn't managed by this coordinator!")
            return
        }
        
        // should be long enough to let Notification work, but short enough to still
        // keep the UI looking good.
        let timeout = TimeInterval(0.5)
        transitionNotificationTimeout = Timer.scheduledTimer(
            withTimeInterval: timeout,
            repeats: false,
            block: {
                [weak self] (_) in
                print("Desired Destination page was not awake.  Reloading Scene to desired page.")
                self?.showMainScene(startingPageIndex: pageIndex)
            })
        
        // NOTE: If the intended controller has not been awoken, then this notification will not work.
        // we therefore have a backup, the transitionNotificationTimeout that will reload the scene to the intended page
        // we lose animation, but at least it will still work.  Also can play with the time inter
        NotificationCenter.default.post(name: .becomeCurrentPageRequest,
                                        object: destinationId)
    }
    
    func notifyCurrentPage(_ pageNumber: Int, _ controllerCoordinatedId: String) {
        transitionNotificationTimeout?.invalidate()
        self.currentPageIndex = pageNumber
        self.currentCoordinatedId = controllerCoordinatedId
    }
}

And then the base class InterfaceController.swift

import WatchKit
import Foundation


class InterfaceController: WKInterfaceController, Coordinated {

    // NOTE: You need to name your identifier in the Storyboard the same thing!
    static var identifier: String {
        let id = String(describing: self)
        return id
    }
    
    // Coordinated
    var coordinator: Coordinator?
    var coordinationId: String = "Not Coordinated"
    var pageIndex: Int = 0
    var totalPages: Int = 1
    
    override func awake(withContext context: Any?) {
        
        guard let config = context as? CoordinatedConfig else {
            print("WARNING: Possibly hooked it up wrong.  If it was nil, it's likely because this is the initial view controller.  Consider changing that to a splash screen.")
            
            self.coordinator = nil
            return
        }
        
        guard config.controllerIdentifier == Self.identifier else {
            fatalError("This controller received a config that is not intended for it.  The controller identifiers don't match.")
        }
        
        self.coordinator = config.coordinator
        self.coordinationId = config.instanceId
        self.pageIndex = config.pageIndex
        self.totalPages = config.totalPages
        
        registerForBecomeCurrentPageNotifications()
        
        print("Woke with context, page \(self.pageIndex)")
    }
    
    private func registerForBecomeCurrentPageNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleNotification(_:)), name: .becomeCurrentPageRequest, object: nil)
    }
    
    override func willActivate() {
        super.willActivate()
    }
    
    override func didAppear() {
        super.didAppear()
        self.coordinator?.notifyCurrentPage(self.pageIndex, self.coordinationId)
    }
        
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }
    
    @objc
    private func handleNotification(_ note: Notification) {
        if note.name == .becomeCurrentPageRequest {
            if let controllerCoordinationId = note.object as? String {
                if self.coordinationId == controllerCoordinationId {
                    self.becomeCurrentPage()
                }
            }
        }
    }
}

And with these 2 very useful implementations, I’m very pleased with what I can create, and it shows that it is possible to have a little more programmatic control over page-based navigation, especially if you want to animate moving to the next page and/or back again.

You should look at how CoordinatedConfig gets passed around and how it is a pretty crucial aspect of the Coordinator. It ultimately is what keeps a reference to your data.

In Practice: An Example Subclass



It might also be worth showing just how one sets up their scene. For example, I have 3 screens involved in my visit to the driving range: One to specify which club I am using, another screen to give feedback on each shot I take, and a third that can show some quick stats about my driving range session so far. This is how I set up that Coordinator:

import Foundation
import WatchKit

class RangeSessionCoordinator: Coordinator {
    
    var statsPageIndex: Int = 2  // TODO: Set this when you are loading the scene
    var shotPageIndex: Int = 1 // TODO: Do this differently.  This is placeholder.
    
    let rangeSession: DrivingRangeSession
    
    var pages: [CoordinatedConfig]!
    
    init(rangeSession: DrivingRangeSession = DrivingRangeSession()) {
        let controllerNames = [
            ClubSelectionController.identifier,
            RangeSessionShotController.identifier,
            RangeSessionStatsController.identifier
        ]
        
        self.rangeSession = rangeSession
        
        super.init()
        
        var configs: [CoordinatedConfig] = []
        for (index, controllerIdentifier) in controllerNames.enumerated() {
            let config = CoordinatedConfig(coordinator: self,
                                           controllerIdentifier: controllerIdentifier,
                                           instanceId: UUID().uuidString,
                                           pageIndex: index,
                                           totalPages: controllerNames.count,
                                           context: rangeSession)
            configs.append(config)
        }
        self.pages = configs
        self.pageInstanceIDs = configs.map { $0.instanceId }
        self.statsPageIndex = self.pageIndex(of: RangeSessionStatsController.identifier)
        self.shotPageIndex = self.pageIndex(of: RangeSessionShotController.identifier)
    }
    
    // will return 0 if cannot be found.
    private func pageIndex(of controllerId: String) -> Int {
        guard let index = self.pages.firstIndex(where: { (config) -> Bool in
            return config.controllerIdentifier == controllerId
        }) else {
            return 0
        }
        
        return index
    }
    
    override func showMainScene(startingPageIndex: Int = 0) {
        // don't need to call super
        
        WKInterfaceController.reloadRootPageControllers(
            withNames: self.pages.map { $0.controllerIdentifier },
            contexts: self.pages,
            orientation: .horizontal,
            pageIndex: self.pageIndex(of: RangeSessionShotController.identifier)
        )
    }
    
    func flashStatsPage(returnId: String) {
        let statsId = self.pageInstanceIDs[statsPageIndex]
        let transition = PageTransition(kind: .flashTarget(duration: 2),
                                        sourceCoordinationId: returnId,
                                        destinationCoordinationId: statsId)
        
        self.move(with: transition)
    }
    
    func moveToShotController() {
        let shotsId = self.pageInstanceIDs[shotPageIndex]
        let transition = PageTransition(kind: .show,
                                        sourceCoordinationId: shotsId,
                                        destinationCoordinationId: shotsId)
        self.move(with: transition)
    }
}

So there you have it! I’m quite happy with this approach and can see it playing a role in future WatchOS apps. Please write me if you have comments, questions, feedback!

SwiftUI: iPad-style Popovers on iPhone

One aspect of iOS development that always struck me as funny is how Apple’s default of Popover is different on iPad and iPhone. Basically on iPhone the standard popover is shown like an Action Sheet.

But there are certainly times where you want a popover on iPhone, like a simple delete confirmation, for example. You tap on a trash can icon, and you’d like a quick and tiny confirmation button to appear in context without the jarring experience of the entire screen being taken over by an action sheet. Or even the bottom 1/4 of the screen, especially when that button was on the navigation bar at the top right.

There are plenty of solutions to this in UIKit, but sadly none (yet) with SwiftUI. The .popover view modifier decides for you whether it will be a sheet or a popover, so basically here’s another example of SwiftUI being powerful but not yet fully filled out in terms of components available to us.

Full disclosure: I feel inclined to write a bit of a blog post about this, even though all I or anyone cares about is a solution to the problem. So here we go. I admit I didn’t write this, and so want to give a huge shout out and thank you to Chase Wasden, located here. I’ve taken his gist located here, then modified it to support being able to specify where you’d like your arrows to be, and here it is below:

//
//  WithPopover.swift
//  PopoverSwiftUI
//

// THANK YOU to this wonderful human:  https://gist.github.com/ccwasden/02cbe25b94eb6e844b43442427127e09

import SwiftUI

// -- Usage
/*
struct Content: View {
    @State var open = false
    @State var popoverSize = CGSize(width: 300, height: 300)
    
    var body: some View {
        WithPopover(
            showPopover: $open,
            arrowDirections: [.down],
            popoverSize: popoverSize,
            content: {
                // The view you want to anchor your popover to.
                Button(action: { self.open.toggle() }) {
                    Text("Tap me")
                }
            },
            popoverContent: {
                VStack {
                    Button(action: { self.popoverSize = CGSize(width: 300, height: 600)}) {
                        Text("Increase size")
                    }
                    Button(action: { self.open = false}) {
                        Text("Close")
                    }
                }
            })
    }
}
 */


// -- Source

struct WithPopover<Content: View, PopoverContent: View>: View {
    
    @Binding var showPopover: Bool
    var popoverSize: CGSize? = nil
    var arrowDirections: UIPopoverArrowDirection = [.down]
    let content: () -> Content
    let popoverContent: () -> PopoverContent
    
    var body: some View {
        content()
            .background(
                Wrapper(showPopover: $showPopover, arrowDirections: arrowDirections, popoverSize: popoverSize, popoverContent: popoverContent)
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
            )
    }
    
    struct Wrapper<PopoverContent: View> : UIViewControllerRepresentable {
        
        @Binding var showPopover: Bool
        var arrowDirections: UIPopoverArrowDirection
        let popoverSize: CGSize?
        let popoverContent: () -> PopoverContent
        
        func makeUIViewController(context: UIViewControllerRepresentableContext<Wrapper<PopoverContent>>) -> WrapperViewController<PopoverContent> {
            return WrapperViewController(
                popoverSize: popoverSize,
                permittedArrowDirections: arrowDirections,
                popoverContent: popoverContent) {
                self.showPopover = false
            }
        }
        
        func updateUIViewController(_ uiViewController: WrapperViewController<PopoverContent>,
                                    context: UIViewControllerRepresentableContext<Wrapper<PopoverContent>>) {
            uiViewController.updateSize(popoverSize)
            
            if showPopover {
                uiViewController.showPopover()
            }
            else {
                uiViewController.hidePopover()
            }
        }
    }
    
    class WrapperViewController<PopoverContent: View>: UIViewController, UIPopoverPresentationControllerDelegate {
        
        var popoverSize: CGSize?
        let permittedArrowDirections: UIPopoverArrowDirection
        let popoverContent: () -> PopoverContent
        let onDismiss: () -> Void
        
        var popoverVC: UIViewController?
        
        required init?(coder: NSCoder) { fatalError("") }
        init(popoverSize: CGSize?,
             permittedArrowDirections: UIPopoverArrowDirection,
             popoverContent: @escaping () -> PopoverContent,
             onDismiss: @escaping() -> Void) {
            self.popoverSize = popoverSize
            self.permittedArrowDirections = permittedArrowDirections
            self.popoverContent = popoverContent
            self.onDismiss = onDismiss
            super.init(nibName: nil, bundle: nil)
        }
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
            return .none // this is what forces popovers on iPhone
        }
        
        func showPopover() {
            guard popoverVC == nil else { return }
            let vc = UIHostingController(rootView: popoverContent())
            if let size = popoverSize { vc.preferredContentSize = size }
            vc.modalPresentationStyle = UIModalPresentationStyle.popover
            if let popover = vc.popoverPresentationController {
                popover.sourceView = view
                popover.permittedArrowDirections = self.permittedArrowDirections
                popover.delegate = self
            }
            popoverVC = vc
            self.present(vc, animated: true, completion: nil)
        }
        
        func hidePopover() {
            guard let vc = popoverVC, !vc.isBeingDismissed else { return }
            vc.dismiss(animated: true, completion: nil)
            popoverVC = nil
        }
        
        func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
            popoverVC = nil
            self.onDismiss()
        }
        
        func updateSize(_ size: CGSize?) {
            self.popoverSize = size
            if let vc = popoverVC, let size = size {
                vc.preferredContentSize = size
            }
        }
    }
}

This is also an excellent example for learning how to make UIKit and SwiftUI play with each other.

SwiftUI: Animating Panel Content

I’m finally getting my hands dirty with SwiftUI and generally find it more motivating to build components that I actually need. So I thought perhaps that it’s time I re-write my Songbook Simple and Recipebox Simple apps to leverage all the experience I’ve gained with Swift, Design Patterns, Test-driven development, and to see how I might build the app in SwiftUI.

Most of Songbook’s UI is pretty simple and straightforward, with one exception: the Main Container UI. Basically Songbook has 3 principal UI components: The Song / Content View (rendering text or images), a Menu panel that slides in/out, and a toolbar that can show/hide.

I basically took this blog post, married it with this other blog post by the same author, to get an understanding of how to make a side panel that animates in and out, then take this approach and see if it can work in 2 dimensions. It wasn’t as straightforward to do the bottom panel, but it did teach me about the importance of GeometryReader and gave me some insights as to how SwiftUI handles state changes, and how to build a view hierarchy that takes that into consideration.

I am grateful to be able to stand on someone else’s shoulders to get me far enough along, after which I synthesized the rest from the knowledge gained. So I’ll just jump to how I got to the result.

It ultimately yielded these components:

//
//  ControlsOverlayView.swift
//  BookItemsSuite
//
//  Created by Stephen O'Connor on 11.03.21.
//

import SwiftUI

struct ControlsOverlayView<ToolbarContent: View, MenuContent: View>: View {
    
    let menuWidth: CGFloat
    let isMenuActive: Bool
    let onMenuHide: () -> Void
    
    let menuContent: MenuContent
    
    let toolbarHeight: CGFloat
    let isToolbarActive: Bool
    let onToolbarHide: () -> Void
    
    let toolbarContent: ToolbarContent
    
    init(menuWidth: CGFloat = 270,
         isMenuActive: Bool = true,
         onMenuHide: @escaping () -> Void,
         toolbarHeight: CGFloat = 44,
         isToolbarActive: Bool = true,
         onToolbarHide: @escaping () -> Void,
         @ViewBuilder menuContent: () -> MenuContent,
         @ViewBuilder toolbarContent: () -> ToolbarContent) {
        
        self.menuWidth = menuWidth
        self.isMenuActive = isMenuActive
        self.onMenuHide = onMenuHide
        self.menuContent = menuContent()
        
        self.toolbarHeight = toolbarHeight
        self.isToolbarActive = isToolbarActive
        self.onToolbarHide = onToolbarHide
        self.toolbarContent = toolbarContent()
    }
    
    var body: some View {
        ZStack {
            GeometryReader { _ in
                EmptyView()
            }
            .background(Color.gray.opacity(0.3))
            .opacity(self.isMenuActive ? 1.0 : 0.0)
            .animation(Animation.easeIn.delay(0.25))
            .onTapGesture {
                self.onMenuHide()
            }
            
            GeometryReader { geometry in
                VStack {
                    let toolbarHeight = isToolbarActive ? self.toolbarHeight : 0
                    HStack {
                        Spacer()
                        
                        let space: CGFloat = 0.0 //geometry.size.width - self.menuWidth
                        let offset = self.isMenuActive ? space : space + self.menuWidth
                        self.menuContent
                            .frame(width: self.menuWidth, height: geometry.size.height - toolbarHeight, alignment: .center)
                            .background(Color.red)
                            .offset(x: offset)
                            .animation(.default)
                    }
                    .frame(width: geometry.size.width,
                           height: geometry.size.height - toolbarHeight,
                           alignment: .center)
                    
                    let offset = self.isToolbarActive ? 0 : self.toolbarHeight/2
                    
                    self.toolbarContent
                        .frame(width: geometry.size.width,
                               height: self.toolbarHeight,
                               alignment: .center)
                        .background(Color.yellow)
                        .offset(y: offset)
                        .animation(.default)
                    
                }
            }
        }
    }
}

struct ControlsOverlayView_Previews: PreviewProvider {
    static var previews: some View {
        ControlsOverlayView(menuWidth: 270,
                            isMenuActive: true,
                            onMenuHide: {},
                            toolbarHeight: 44,
                            isToolbarActive: true,
                            onToolbarHide: {}) {
            Text("Menu Content")
        } toolbarContent: {
            Text("Toolbar Content")
        }
    }
}

Which is then a part of the ApplicationView (in UIKit terms, this would equate to a root view controller):

//
//  ApplicationView.swift
//  SidePanelSandbox
//
//  Created by Stephen O'Connor on 10.03.21.
//

import SwiftUI

struct ApplicationView<MainContent: View, MenuContent: View, ToolbarContent: View>: View {
    
    @State var isMenuActive: Bool = true
    @State var isToolbarActive: Bool = false
    
    var menuWidth: CGFloat = 270
    
    let mainContent: MainContent
    let menuContent: MenuContent
    let toolbarContent: ToolbarContent
    
    init(@ViewBuilder mainContent: () -> MainContent,
                      menuContent: () -> MenuContent,
                      toolbarContent: () -> ToolbarContent) {
        self.mainContent = mainContent()
        self.menuContent = menuContent()
        self.toolbarContent = toolbarContent()
    }
    
    var body: some View {
        ZStack {
            GeometryReader { geometry in
                self.mainContent
                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                   
            }
            if !self.isMenuActive {
                GeometryReader { _ in
                    VStack {
                        Button(action: {
                            self.toggleMenu()
                        }, label: {
                            Text("Menu")
                        })
                        
                        Button(action: {
                            self.toggleToolbar()
                        }, label: {
                            Text("Toolbar")
                        })
                    }
                }
            }
            
            ControlsOverlayView(menuWidth: 270, isMenuActive: isMenuActive, onMenuHide: {
                self.toggleMenu()
                print("did hide menu")
            }, toolbarHeight: 44, isToolbarActive: isToolbarActive, onToolbarHide: {
                print("did hide toolbar")
            }, menuContent: {
                self.menuContent
            }, toolbarContent: {
                self.toolbarContent
            })
        }
    }
    
    func toggleMenu() {
        if self.isMenuActive {
            // if we're about to close the menu, close the toolbar too.
            $isToolbarActive.wrappedValue = false
        }
        self.isMenuActive.toggle()
    }
    
    func toggleToolbar() {
        self.isToolbarActive.toggle()
    }
}

struct ApplicationView_Previews: PreviewProvider {
    static var previews: some View {
        ApplicationView {
            Text("Main Content")
        } menuContent: {
            Text("Menu Content")
        } toolbarContent: {
            Text("Toolbar Content")
        }
    }
}

And that’s essentially how it works. I was and still am a little confused as to why the ControlsOverlayView is rendering as expected, given the way I offset the toolbar, but all in all I think this approach is pretty clean and I’m happy with it.

[Recipe] Get the current iOS Simulator App’s location on disk

I’m currently doing SwiftUI tutorials, and found this useful command for Terminal. When your app is running in a simulator (make sure you only have 1 simulator window open!) and you want to get close to its Documents folder on disk, put this in a terminal:

xcrun simctl get_app_container booted <my_bundle_identifier_without_brackets>

And this will take you to the folder containing the AppName.app file. From there you’ll want to go up a few folders back to Container level, and then go into the Data folder, and ultimately find your app’s identifying folder. (Hint: Look at the Date Modified field… it will likely indicate which folder corresponds to your app)

I enjoyed this post because I used to accomplish this goal but not as elegantly (and quickly, I suppose) has here. I wrote it here to be able to refer to it again someday.

Core Data Migrations – Woes…

So thankfully I won’t go into a misogynistic blog post a la frustrated Mark Zuckerberg in the movie “The Social Network”, but sometimes you just need to blog to let your technical frustrations out!

I’m doing some complicated Core Data migrations where the lightweight ones won’t work.  I’m migrating from one Core Data model to a completely different one, and I’m trying to map those properties over.

I’ve already discovered a wealth of information either through google, trial and error, and a bit of both, which I will share once I’ve got the entire migration code running.

I just have to say here however… I think it’s probably best to avoid these Xcode tools like a mapping model.  Because they’re buggy as hell.  For example, if you define a mapping model and provide source and destination models, but then change either of those source or destination models, the Mapping Model will no longer work, and even if you re-select the source and destination, it doesn’t matter:  You’ve just killed your Mapping Model.  Is there any info about this?  No.  Just “could not find a suitable mapping model”.

Ugh.

I’ve been trying to debug something for 3 HOURS because all you get is an EXC_BAD_ACCESS crash, and ZERO information as to what it’s all about, and NSZombies do nothing.

So I’m sitting here, taking shots into the dark and hope I discover something.

I question whether I should just give up on TOOLS, which are supposed to make your life easier, and just write the whole heavyweight migration in code.  If I had started on that 3 hours ago, I’d probably be done.  My data model is like 8 entity types.

Is it just me or is the quality of Apple’s tools getting worse?  It’s like they produce stuff that’s 80% good.  But it’s a tool chain, so 0.8 * 0.8 = 0.64.  So you see where this is going…

Regular Expressions on iOS

I’ve decided to take on the masochistic task of getting my hands dirty with NSRegularExpression. Here are some of my thoughts and findings:

– Regex is voodoo. What’s worse, it’s not even standardized. So all the tutorials on the web are not platform agnostic. I could use a recipe to match the things I’m interested in, but when creating an instance of NSRegularExpression on iOS, it fails as “invalid pattern”.
– Because of this, you WILL become a test driven developer. 🙂
– Start with a simple regex pattern that detects PARTS of what you’re looking to match, then add complexity. This way you can see what part of your pattern is failing.
– Don’t use the Number Sign # as a matchable character. It is protected but not documented! Use \u0023 instead. e.g. “\\b
– When you map/inspect your NSTextCheckingResult objects, it should have the same number of ranges as you have capture groups. You use these to get more specific about which capture groups were involved in the match. (Provides more granularity/control). The NSTextCheckingResult.range represents the range in the text associated with that one result!
– NSRegularExpression really doesn’t deal well with the hash symbol (sharps).

I ultimately reached a solution, but it never felt entirely ‘correct’ despite finding a way to produce the correct results.

Developing for iCloud sucks

iCloud is the thorn in Apple’s side.  They have been trying to get this right forever.  Maybe it works now from an end-user perspective, but as a developer, what a rough, confusing implementation.

I have to say, when you work with the Dropbox API, it is intuitive, it is easy, it is straightforward. Perhaps you have to write a lot of boilerplate code, but then again, do you?  It manages files.  This name, that path, that revision number, that modifiedDate.  You sort out the rest.  How hard can that be?

I’m currently writing a sync engine that allows users to decide which cloud service they want to use to backup their data.  So of course it would make sense to have one common API that wraps the various services.

It went so smoothly with Dropbox.  The only difficulty I had was realizing that the serialization of date strings sometimes results in small differences in the Date that emerges, meaning I had to write a .isTheSameAs(other: Date) method, which checks to see if they are within X seconds of each other, then call those ‘the same’.

Then I moved on to iCloud.  What a clusterf•ck.  Talk about over-engineering and complete inflexibility.  I just want a list of files that are in the cloud.  No, Apple has to distinguish whether you have them locally or not, and gives them a different URL.  And all sorts of other weird stuff.

So I tried to find a framework that makes iCloud a little more user-friendly, called iCloudDocumentSync which I found to be pretty straightforward, all things considered.  And yet, it’s still a pain.  Because iCloud uses multiple URLs to represent the same document.  In the end, I have to sync the entire iCloud folder’s contents before I can start working with these with a common URL.  (Sure I could extract the file name I need).  Moreover, iCloud uses a thread to open each document, so if I want to batch download a lot of data, suddenly I have a thread explosion.  Now I have to deal with managing that.

All in all it’s just been a pain to work with it.  It seems opaque in its verbosity.  Meaning, the person who talks a lot doesn’t get listened to and understood.  There are so many callbacks and all sorts of weird things going on, it’s very annoying.  Just because they’re trying to save the developer time, they’ve implemented something weird and unwieldy if you don’t use it according to their use case.  I personally just use a UIDocument as a means to save data.  I don’t work with UIDocuments because my app’s data model uses the data, but not the file.  This just seems silly with iCloud.

Autolayout and Self-sizing UITableViewCell

I’m not going to lie.  Autolayout is a massive pain.  But.  Ultimately it’s very powerful and you’re best to just go through the pain and learn it.

Even so, you should also get a bit familiar with it, then learn about the concept of self-sizing table view cells.  It’s quite important.  Basically, as long as there is a clearly defined way for a UITableViewCell to determine its own height via the auto-layout constraints, dynamic table view cells are pretty easy.

Unfortunately, there is a lot to learn:

This series

Then this

or even Apple

Quick Reference:

How to make sure a UITableViewCell autosizes when you have a label that you want to wrap around onto multiple lines.

1. Pin the Label at top left, bottom, right.

2. Edit the right constraint to be “Greater than or Equal to”, and then set the constant to be the right-most you want to allow that (probably view margin).

3. Set that right-most constraint’s priority to 750 (high), then set (on the Label!) it’s content compression resistance priority to 749.

Should be fine now.

Alamofire and nostalgic feelings for AFNetworking

So, in the Swift world, I presume a lot of people prefer to remain “Swifty”.  That is, why would you opt for old Objective-C libraries?  Out with the old, in with the new!

So, we adopt Alamofire.  Is it just me, or is this library a bit unwieldy?   I think it has a lot to do with Swift shorthand and often not knowing just what types are actually being passed around, especially given autocomplete.

On top of that, just when you get your head around a specific release, they make a new major release, breaking old APIs.  And if I’m going to have to keep updating my code to stay current because “out with the old, in with the new”, why don’t I just stay with AFNetworking?  I mean come on.  It’s networking.  For the mostpart it’s just “give me that data at that URL.  Here’s my auth info.”  Done.  Or “here’s some data for you.  Here’s my auth info.”  Done.

Anyway, it’s a rant.  I just don’t find Alamofire all that sexy.  It reminds me of dealing with civil servants.  The creators imagined this bureaucratic utopia that functions perfectly as long as everyone understands it.  Furthermore, we must not only understand it, but are fully on board with its vision.  Meanwhile, we the people are busy trying to write our own apps, and couldn’t really care less.  We just want to get data and post data and not have to deal with too much crap in the middle.

(Go ahead, snarky programmer.  Now tell me off, tell me to just use this, that, or the other.  Reject my feedback.  It’s fine.  All I’m saying is that AFNetworking seemed a lot easier to use.)