Bush League, Apple, Bush League – Beware UserDefaults and Settings.bundle

Long story short, I wasted an hour debugging a problem with Settings Bundle.

I had saved a value as an Int in my UserDefaults, that is a rawValue for an Int-backed enum. For whatever reason, it was getting set to a value I never set.

You’re better off saving Strings as your raw values and using those strings as values in your Settings.plists. This worked fine.

I like being an iOS Developer, but sometimes I think “Trillion dollar company?” Given the way their resources rarely teach Test-Driven Development, and how some of their techs don’t focus on that aspect of the job, should we be surprised at all the little issues that arise

Development IS hard. I get that. But we rely on this stuff working. Nobody likes to debug the mothership.

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.

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.

Swift Error Handling

I often write blog posts for my own sake, but publish them in the event that others may find the information useful.
Today, I wanted to post a quick reference to Error handling in Swift and the syntax for casting and binding an error that is caught in a do-catch statement. I’m finding quite often what you’ll find on the internet is you can catch certain error types, but they don’t assign it to a variable, for example if you want to wrap that error into another Error type that makes your API more contstrained in terms of what kind of errors you should expect to handle in your UI.
For example, it’s not unusual for me to have an error type like this:

public enum SyncError: Error {
    
    /// if for example the pre-conditions for Cloud Sync to work are not met, this could be the result.
    /// actual use cases would be for example if you're not signed into iCloud.
    case initializationFailed(details: String)
    
    /// if you are performing an operation that expects there to be no file at a specific location but there is.
    case fileAlreadyExists(filename: String)
    
    /// if you try to upload a file with no data, or the file doesn't exist in remote
    case noContent(filename: String)
    
    /// wraps an error thrown from FileManager APIs
    case fileManager(error: Error)
    
    /// Just for situations that are unlikely to happen.
    case unexpected(details: String)
}

So there are times where you want to perform a do-catch but then catch certain error types and re-package them before finishing up. In this case below, it’s some body of a method that has to call a completion block on a completion queue with this signature:

(_ success: Bool, _ errors: [SyncError]?) -> Void
do {
      // DO SOME STUFF REQUIRING A try STATEMENT    

} catch let e as SyncError {
    // you see on the line above how you cast and assign the SyncError to a value
    completionQueue.async {
       completion(false, [e])
    }
} catch let e {
    completionQueue.async {
       completion(false, [SyncError.fileManager(error: e)])
    }
}

And that’s it! I only wrote this here because I didn’t want to google around again should I forget that syntax.

Recipe: Transform the JSON that your API Client decodes

I’m currently having a not-so-fun fight with the Firebase REST API while trying to bypass using the Firebase iOS SDK in a demo project whose purpose it is to not use Firebase, per se, but to implement a REST API Client on iOS. People say “Use Firebase” as it’s easy to get up and running. VERY debatable here. It does however highlight one aspect of my profession that almost seems to be a general rule:

“Mobile devs almost inevitably have to spend a lot of time figuring out why the backend never works as advertised.”

It’s been extremely rare in my career to be given an Backend API spec that can be implemented without any headaches, but I’ll save that for another post as to how best to tackle that. (In fact I did write the beginnings of such a post, many years ago).

Anyway, it seems that the firebase backend doesn’t return arrays of dictionaries. It just returns dictionaries. So if you request a collection, you’ll get a dictionary with a key count equal to the collection size.

This is a problem if you just want to parse an array of objects, because there is no array. And if you are not a backend developer like me, you just have to work with what you are given.

Here’s a use case coming from Firebase. I requested /users.json and got:

{
    "SomeRandomFakeUserId2": {
        "name": "Stephen the Admin"
    },
    "SomeRandomFakeUserId3": {
        "name": "Stephen the Tester"
    }
}

But ideally this would be an array of dictionaries with the key above embedded in the dictionary, like so:

[
    {
        "objectId": "SomeRandomFakeUserId2",
        "name": "Stephen the Admin"
    },
    {
        "objectId": "SomeRandomFakeUserId3",
        "name": "Stephen the Tester"
    }
]

But ideally I would like to keep my paradigm that generally works on my APIClient, which generally works like:

    enum APIResult<T> {
        case success(T)
        case error(Error)
    }
    
    @discardableResult
    public func sendThenDecodeJSON<T: Codable>(_ request: URLRequest,
                                               completion: @escaping (APIResult<T>) -> Void) -> URLSessionTask? {
        // session: URLSession
        let task = session.dataTask(with: request) { (data, response, error) in
            
            do {
                // Omitted:  Handle errors, etc.
                
                // You see here, all I have to do is create a struct for my responses, based on the JSON spec on the server, and I'm good.  Except... How do I use the Codable approach to json if I don't know what the key names will be?
                let result = try JSONDecoder().decode(T.self, from: data)
                
                DispatchQueue.main.async {
                    completion(.success(result))
                }                
            } catch let error {
                // Omitted for brevity: DO BETTER ERROR HANDLING HERE
            
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
        task.resume()
        return task
    }

So what can be done here? I want to make my system flexible, but with a minimum of additional configuration. I know that on my endpoints, my existing solution gets the job done, but on a few, it doesn’t.

So I came up with the PayloadTransformable protocol:

protocol PayloadTransformable: Codable {
    static var requiresTransformation: Bool { get }
    static func transform(_ input: [String: Any]) -> [String: Any]
}

// Provide some default implementation for conformance that ultimately results in it working as before
extension PayloadTransformable {
    static var requiresTransformation: Bool { return false }
    static func transform(_ input: [String: Any]) -> [String: Any] {
        return input
    }
}

And then you have some data models, and just have to implement that:

struct Object: Codable {
    let objectId: String
    let name: String
}

struct ObjectsResponse: PayloadTransformable {
    let objects: [Object]
    
    static var requiresTransformation: Bool { return true }
    
    static func transform(_ input: [String: Any]) -> [String: Any] {
        
        var objectArray = [[String: Any]]()
        
        for key in input.keys {
            var newObject = [String: Any]()
            newObject["objectId"] = key
            guard let values = input[key] as? [String: Any] else {
                print("Failed.")
                return input
            }
            for (key, value) in values {
                newObject[key] = value
            }
            objectArray.append(newObject)
        }
        
        return ["objects": objectArray]
    }
}

And then you modify how the APIClient handles the incoming data:

@discardableResult
public func sendThenDecodeJSON<T: PayloadTransformable>(_ request: URLRequest,
                                               completion: @escaping (APIResult<T>) -> Void) -> URLSessionTask? {

        let task = session.dataTask(with: request) { (data, response, error) in

            do {
                // Omitted:  Handle errors, etc.

                var jsonData = data
                // Due to the default implementation listed in the Protocol extension, in general you have to do nothing in order to adopt this, other than make your relevant Codable types conform to PayloadTransformable (a quick find and replace)
                if T.requiresTransformation {
                    guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
                        fatalError()  // you'd throw an error here
                    }
                    let transformed = T.transform(json)
                    jsonData = try JSONSerialization.data(withJSONObject: transformed, options: [.prettyPrinted])
                }
                
                let result = try JSONDecoder().decode(T.self, from: jsonData)

                // we are using a delegateQueue on URLSession, so we want to complete on the main thread
                DispatchQueue.main.async {
                    completion(.success(result))
                }

            } catch let error {

                // Omitted for brevity: DO BETTER ERROR HANDLING HERE>

                // we are using a delegateQueue on URLSession, so we want to complete on the main thread
                DispatchQueue.main.async {
                    completion(.failure(error))
                }
            }
        }
        task.resume()
        return task
    }


And that’s it.

You could copy-paste this into a Playground and check it out:

protocol PayloadTransformable: Codable {
    static var requiresTransformation: Bool { get }
    static func transform(_ input: [String: Any]) -> [String: Any]
}

// Provide some default implementation for conformance that ultimately results in it working as before
extension PayloadTransformable {
    static var requiresTransformation: Bool { return false }
    static func transform(_ input: [String: Any]) -> [String: Any] {
        return input
    }
}
struct Object: Codable {
    let objectId: String
    let name: String
}

struct ObjectsResponse: PayloadTransformable {
    let objects: [Object]
    
    static var requiresTransformation: Bool { return true }
    static func transform(_ input: [String: Any]) -> [String: Any] {  
        var objectArray = [[String: Any]]()
        for key in input.keys {
            var newObject = [String: Any]()
            newObject["objectId"] = key
            guard let values = input[key] as? [String: Any] else {
                print("Failed.")
                return input
            }
            for (key, value) in values {
                newObject[key] = value
            }
            objectArray.append(newObject)
        }
        
        return ["objects": objectArray]
    }
}
func simulatedResponseHandler<T: PayloadTransformable>(_ data: Data) -> T {
        
        do {   
            var jsonData = data
            if T.requiresTransformation {
                guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
                    fatalError()
                }
                let transformed = T.transform(json)
                jsonData = try JSONSerialization.data(withJSONObject: transformed, options: [.prettyPrinted])
            } 
            let object = try JSONDecoder().decode(T.self, from: jsonData)
            return object
            
        } catch let e {
            fatalError("Failed!")
        }
}

// All that was definitions.  Now the code you execute:

do {
    
    let untransformed =  [
        "SomeID" : ["name": "SomeName"],
        "SomeOtherID" : ["name": "SomeOtherName"]
    ]
    
    let transformed = ["objects": [
        ["objectId": "SomeID", "name": "SomeName"],
        ["objectId": "SomeOtherID", "name": "SomeOtherName"],
    ]]

    let jsonInput = try JSONSerialization.data(withJSONObject: untransformed, options: [.prettyPrinted])

    let response: ObjectsResponse = simulatedResponseHandler(jsonInput)
    print(String(describing: response))
    
} catch let e {
    print(e.localizedDescription)
}



UILabel when inactive, UISlider when active

I’m working on a new app and I’m trying to keep the interface minimal in terms of screen elements.  Also, design-wise I’m a huge fan of finding ways to only use space *when* you need it, and finding novel ways to conceal elements that you don’t need (when you don’t need them).  On the flipside, one should also be conscious of hiding too much, thus alienating some users who don’t immediately have a good sense for such things.  That addresses the need for good onboarding but that’s a longer discussion.

So I thought, (the new app is a music player), “it’s not often you need to seek to a certain time in the song, so why does this element get to take up so much space?”

Since my app’s UX generally involves interacting with labels, I thought it would be interesting to hide a slider behind a label, but when you interact with it, it’s the underlying control that becomes active.

I think I should just show the code.  The key is in how one overrides the method -hitTest:withEvent:  Because of that, this solution is very flexible in terms of what kind of UISlider you use, and what kind of UILabel you use.  It’s the technique worth noting because it could have a few applications.

Shoot me a message if you don’t quite understand why this does what it does, or why a different approach might / might not work for you.

(Oh yeah, the assumption is that the subviews are pinned to their parent’s edges… i.e. same frame.size)

/  LabelScrubber.swift
//  LabelScrubber
//
//  Created by Stephen O'Connor on 03.11.20.
//  Copyright © 2020 HomeTeam Software. All rights reserved.
//

import UIKit

class LabelScrubber: UIView {

    let showDelayShort = 0.7
    let showDelayLong = 1.2
    
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var slider: UISlider!
    
    var labelTimer: Timer?
    
    /// this gets called on a touch down, so in other words, at the beginning of an interaction
    /// but you tell it which view you want to be 'the view' for the interaction.
    /// super cool; so you can override and say it's the slider that matters
    /// and thus you'll interact with that.
    /// nice though that we can make preparations before that interaction,
    /// like snapping the slider to where your touch is!
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.bounds.contains(point) {
            self.slider.isHidden = false
            self.label.isHidden = true
            
            let percentX = point.x / self.bounds.size.width
        
            let targetValue = self.slider.minimumValue + Float(percentX) * (self.slider.maximumValue - self.slider.minimumValue)
            
            self.slider.setValue(targetValue, animated: true)
            
            showLabel(after: showDelayLong)
            return self.slider
        }
        return super.hitTest(point, with: event)
    }
    
    override var intrinsicContentSize: CGSize {
        return label.intrinsicContentSize
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        commonInit()
    }
    
    private func commonInit() {
        self.label.isUserInteractionEnabled = false
        self.label.isHidden = false
        self.slider.isHidden = true
        self.slider.addTarget(self, action: #selector(tracking(_:)), for: .touchDown)
        self.slider.addTarget(self, action: #selector(tracking(_:)), for: .valueChanged)
        self.slider.addTarget(self, action: #selector(finishedTracking(_:)), for: .touchUpInside)
        self.slider.addTarget(self, action: #selector(finishedTracking(_:)), for: .touchUpOutside)
    }
    
    @objc
    func finishedTracking(_ slider: UISlider) {
        showLabel(after: showDelayShort)
    }
    
    @objc
    func tracking(_ slider: UISlider) {
        invalidateShowTimer()
    }
    
    private func showLabel(after delay: TimeInterval) {
        print("willShowLabel")
        invalidateShowTimer()
        labelTimer = Timer.scheduledTimer(withTimeInterval: delay,
                                          repeats: false,
                                          block:
            { [weak self] (_) in
                print("showLabel\n\n")
                self?.returnToDefaultState()
                
        })
    }
    
    func invalidateShowTimer() {
        print("invalidate")
        labelTimer?.invalidate()
    }
    
    func returnToDefaultState(duration: TimeInterval = 0.3) {

        self.slider?.alpha = 1.0
        UIView.animate(withDuration: duration,
                       animations: {
                        //self.label?.alpha = 1.0
                        self.slider?.alpha = 0.0
        }) { [weak self] (_) in
            self?.label?.isHidden = false
            self?.slider?.isHidden = true
            self?.slider?.alpha = 1.0
        }
    }
}

Hacking MPMediaQuery for better album shuffling

So I’m working on a Music app.  I’m a little frustrated with Apple’s assumption that we all have massive iCloud limits and/or live in a country with great mobile reception.  I live in Germany and as soon as you leave the cities, your mobile reception gets really sketchy unless you are a subscriber of the state “monopoly” Deutsche Telekom.  German bureaucracy dictates that anything with “Deutsche” in the business name will be grossly overpriced and half as effective, while customer service being usually horrible.

This is a post about programming!  😀  So yeah, I’m working on an app that’s to be a lot like the original Music app on the iPod touch: Just show me the songs I have on this device, and make it easy for me to navigate around.

One thing that became apparent was MPMusicPlayerController‘s inability to pick random albums.  It can pick random songs, but not random albums and then play the album.  It can first shuffle through the songs in an album before moving on to another album, but really?  Why would I insult an artist by listening to their album in the wrong order?  Can you imagine listening to Pink Floyd’s Dark Side of the Moon with the tracks shuffled?  Makes no sense, except perhaps a Greatest Hits album…

So I thought “why don’t I just hack with the MPMediaQuery class?  And I found a solution that works.  At least if you aren’t using this query for UITableView and populating all that, as you’d require itemSections, and collectionSections, etc.

So, without going into too much more detail, if you have already got a bit of experience with using the MediaPlayer framework on iOS, you’ll know that it’s a little quirky, as most of the audio folks tend to be over at Apple / everywhere (former Audio Designer here)…

import UIKit
import MediaPlayer

class MPHackMediaQuery: MPMediaQuery {

    private var modifiedItems: [MPMediaItem]?
    private var modifiedCollections: [MPMediaItemCollection]?
    
    init(with query: MPMediaQuery) {
        super.init(filterPredicates: query.filterPredicates)
        self.groupingType = query.groupingType
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override var items: [MPMediaItem]? {
        if modifiedCollections == nil {
            if let collections = super.collections {
                modifiedCollections = self.randomize(collections)
            }
        }
        if modifiedItems == nil {
            if let collections = self.modifiedCollections {
                var items = [MPMediaItem]()
                for collection in collections {
                    items.append(contentsOf: collection.items)
                }
                modifiedItems = items
                
            } else {
                return nil
            }
        }
        
        return modifiedItems
    }
    
    override var collections: [MPMediaItemCollection]? {
        
        if modifiedCollections == nil {
            if let collections = super.collections {
                modifiedCollections = self.randomize(collections)
            }
        }
        
        return modifiedCollections
    }
    
    private func randomize(_ collections: [MPMediaItemCollection]) -> [MPMediaItemCollection] {
        
        var indices: [Int] = []
        for i in 0.. 0
        
        return shuffledCollections
    }
}

So as you can see, you can set a queue on your MPMusicPlayerController using a MPMediaQuery and this will work… IF you set your controller’s shuffleMode to .off, which makes sense because we aren’t shuffling the songs of the album, we just “pre-shuffled” the order in which the albums are chosen.

[Swift] Detect Touches on Attributed Text in UILabel

It’s always funny when you google “how do I…” and Google shows you a result that you actually wrote.

I wanted to solve this problem, and it looks like I did it years ago in Objective-C.

So, I re-wrote it in Swift that you can basically just copy-paste-use.  You’re welcome.

//
//  TappableLabel.swift
//
//  Created by Stephen O'Connor on 07.10.20.
//  MIT License.  You will send no lawyers here.  Have fun.
//  taken from here:  https://horseshoe7.wordpress.com/2015/12/10/detect-touches-on-attributed-text-in-uilabel/

import UIKit

typealias LabelLink = (text: String, link: Any?)

protocol TappableLabelDelegate: class {
    func didTapOnLink(_ link: LabelLink, in tappableLabel: TappableLabel)
}

extension NSAttributedString.Key {
    static let custom = NSAttributedString.Key("CustomAttribute")
}

class TappableLabel: UILabel {
    
    weak var delegate: TappableLabelDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        commonInit()
    }
    
    private func commonInit() {
        self.textAlignment = .left // has to be left for this to work!
        addGestureRecognizers()
    }
    
    private func addGestureRecognizers() {
        let tap = UITapGestureRecognizer(target: self, action: #selector(tappedLabel(_:)))
        self.isUserInteractionEnabled = true
        self.addGestureRecognizer(tap)
        
    }
    
    @objc
    private func tappedLabel(_ tap: UITapGestureRecognizer) {
        guard let label = tap.view as? TappableLabel, label == self, tap.state == .ended else {
            return
        }
        let location = tap.location(in: label)
        processInteraction(at: location, wasTap: true)
    }
    private func processInteraction(at location: CGPoint, wasTap: Bool) {
        
        let label = self
        
        guard let attributedText = label.attributedText else {
            return // nothing to do
        }
        
        let textStorage = NSTextStorage(attributedString: attributedText)
        let textContainer = NSTextContainer(size: label.bounds.size)
        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)
        
        textContainer.lineFragmentPadding = 0.0
        textContainer.lineBreakMode = label.lineBreakMode
        textContainer.maximumNumberOfLines = label.numberOfLines
        
        
        let characterIndex = layoutManager.characterIndex(for: location,
                                                          in: textContainer,
                                                          fractionOfDistanceBetweenInsertionPoints: nil)
        if characterIndex < textStorage.length {
            log.info("Character Index: \(characterIndex)")
            let range = NSRange(location: characterIndex, length: 1)
            let substring = (attributedText.string as NSString).substring(with: range)
            
            log.info("Character at Index: \(substring)")
            if let labelLink = attributedText.attribute(.custom,
                                                        at: characterIndex,
                                                        effectiveRange: nil) as? LabelLink {
                
                log.debug("You \(wasTap ? "tapped" : "pressed") on \(labelLink.text) and the value is: \(String(describing: labelLink.link))")
                self.delegate?.didTapOnLink(labelLink, in: self)
            }
        }
    }
    
    // will set the label's text to the given text argument, but for any callbackString it will search the text for that and embed it.
    func setText(_ text: String, withCallbacksOn callbackStrings: [LabelLink] = []) {
        
        self.text = text
        
        let attributedString = NSMutableAttributedString(string: text)
        let coreAttributes: [NSAttributedString.Key: Any] = [
            .foregroundColor : self.textColor!,
            .font: self.font!
        ]
        attributedString.setAttributes(coreAttributes,
                                       range: NSRange(location: 0, length: text.count))
        
        for labelLink in callbackStrings {
            
            let range = (text as NSString).range(of: labelLink.text)
            if range.location != NSNotFound {
                var additionalAttributes = coreAttributes
                additionalAttributes[.custom] = labelLink
                attributedString.setAttributes(additionalAttributes, range: range)
            }
        }
        self.attributedText = attributedString
    }
}

Why is FileManager so unforgiving?

This is more of a reminder for me as to how to do simple things.

Apple keeps modifying the FileManager API, but doesn’t actually make it obvious as to how to do simple things.  I have Data that I want to save somewhere.  You think it would be as easy as getting a path, then writing it.  Nope.

So here’s a recipe to show how to simply write some data somewhere, overwriting as you do it:

func testSerialization() {
        do {
            let data = try NSKeyedArchiver.archivedData(withRootObject: self.dates, requiringSecureCoding: false)
            let url = writeLocation()  // currently just the caches directory / SomeFolder / SomeFilename.dta
            let fm = FileManager.default
            if fm.fileExists(atPath: url.path) {
                try fm.removeItem(at: url)
            }
            let folder = url.deletingLastPathComponent()
            try fm.createDirectory(at: folder, withIntermediateDirectories: true, attributes: nil)
            let success = fm.createFile(atPath: url.path, contents: data, attributes: nil)

            XCTAssertTrue(success, "Should have written the file!")
            XCTAssertTrue(fm.fileExists(atPath: url.path), "Should have written something here")

        } catch let error {
            XCTFail("Failed with error: \(error.localizedDescription)")
        }
    }