assignTo

Not sure if this is a good idea or a terrible one but here we go… It’s not uncommon to find yourself in situations where you need to add little bits of glue code to handle the optionality of instance variables at the point you construct their values.

Here’s an example where I need to assign an optional instance variable and also return the non optional result from the function that is doing the creation.

class Coordinator {
    private var controller: MyViewController?

    func start() -> MyViewController {
        controller = MyViewController()
        return controller!
    }
}

In the above it’s tempting to just use the instance variable but then to fulfil the contract we end up having to force unwrap. We can improve this by creating a local let that is non optional, which can then be assigned to the optional instance variable

class Coordinator {
    private var controller: MyViewController?

    func start() -> MyViewController {
        let controller = MyViewController()
        self.controller = controller
        return controller
    }
}

This works and removes the less than desirable handling of the optionality but it introduces the risk that we might forget to assign the instance variable.

In practice you see many different slight variations of this glue code e.g.

class Coordinator {
    private var controller: MyViewController?

    func start0() -> MyViewController {
        let controller = MyViewController()
        self.controller = controller
        return controller
    }

    // Making the contract weaker by returning the optional
    func start1() -> MyViewController? {
        controller = MyViewController()
        return controller
    }

    // Being more explicit and explaining the unwrapping
    func start2() -> MyViewController {
        controller = MyViewController()

        guard let controller else {
            fatalError("We should never get here")
        }

        return controller
    }
}

Of the above I think start0 is the most desirable as it models the optionality correctly and doesn’t resort to any unwrapping, it’s just unfortunate that it’s 3 lines and you can forget the assignment.

The above are simplified examples and in real code the functions will likely be longer so this juggling of optionality could be spread out or just be less clear. This isn’t just limited to cases where you need to assign and return there are lots of times where you need to create an object, assign it to an instance variable and perform some further configuration; one common example would be

func loadView() {
    view = UIView()
    view?.backgroundColor = .red
    view?.alpha = 0.5
}

Here I’m having to handle the safe unwrapping ? multiple times. I could use if let/guard let but it starts getting wordy again.


Let’s create a helper

Here’s an idea for a helper that can get us back to a single line of code in the simplest case and help avoid the additional unwraps in the loadView case.

By adding these helpers

func assignTo<T>(_ to: inout T?, _ value: T) -> T {
    to = value
    return value
}

@_disfavoredOverload
func assignTo<T>(_ to: inout T, _ value: T) -> T {
    to = value
    return value
}

We can now simplify the original start example to

func start() -> MyViewController {
    assignTo(&controller, .init())
}

and the loadView example loses all the optional handling

func loadView() {
    let view = assignTo(&view, UIView())
    view.backgroundColor = .red
    view.alpha = 0.5
}

@_disfavoredOverload

I could be wrong but my understanding that for my usage I expect the value and return to be non optional. The to parameter could be optional or non optional depending on requirements. Without the annotation Swift will try to make all the Ts align as Optional<Wrapped> which is not what I wanted. With the annotation Swift is now clever enough to know that it should really only have one optional and not try and force all T positions to be Optional.


Wrap up

No idea if this is a good idea or not but I took some inspiration from Combine having assign(to:) and assign(to:on:) methods.


A little more

The types do have to line up which can make things less ideal e.g. in the loadView example if I needed to configure things available to a subclass of UIView then I’m back to needing to do some reassignment and typecasting e.g.

func loadView() {
    guard let view = assignTo(&view, MyView()) as? MyView else { return }
    view.title = "title"
}

You can kind of work around this by composing other helper methods - a common one in Swift projects is with which would look like this

func loadView() {
    let view = assignTo(&view, with(MyView()) { $0.title = "title" })
    registerView(view)
}

In the above it’s only inside the scope of the with trailing closure that we know it’s an instance of MyView and I had to add another function call registerView that takes the view to make it worthwhile needing to use assignTo still.

Command line arguments with user defaults

You can use UserDefaults as a simple way to get the arguments passed to an app on launch without having to write any command line parsing. The basic capability is that you can pass an argument like -example some-string as a launch argument and this will be readable by defaults like this:

UserDefaults.standard.string(forKey: "example") //=> "some-string"

Supported types

UserDefaults supports a few types Array, Dictionary, Boolean, Data, Date, Number and String. It is possible to inject data and have them be understood in any of these types, the key is recognising that you need to use the same representation that plists use.

Array

Arrays are heterogeneous and can be represented like this

// -example <array><string>A</string><integer>1</integer></array>
UserDefaults.standard.array(forKey: "example") //=> Optional([A, 1])

Dictionary

Any key value pair

// -example <dict><key>A</key><integer>1</integer><key>B</key><integer>2</integer></dict>
UserDefaults.standard.dictionary(forKey: "example") //=> Optional(["B": 2, "A": 1])

Boolean

This can be represented by many variants. All of <true/>, 1 and true will count as true whereas <false/>, 0 and false will count as false.

// -example <true/>
UserDefaults.standard.bool(forKey: "example") //=> true

Data

// -example <data>SGVsbG8sIHdvcmxkIQ==</data>
UserDefaults.standard.data(forKey: "example")
    .flatMap { String(decoding: $0, as: UTF8.self) } //=> Optional("Hello, world!")

Date

Date doesn’t have a convenience function but still returns an honest date when encoded correctly

// -example <date>2024-03-10T22:19:00Z</date>
UserDefaults.standard.object(forKey: "example") as? Date //=> Optional(2024-03-10 22:19:00 +0000)

Number

For numbers you have the option to not wrap in any tags and hope for the best or to choose real or integer

// -example <real>1.23</real>
UserDefaults.standard.float(forKey: "example")   //=> 1.23
UserDefaults.standard.double(forKey: "example")  //=> 1.23
UserDefaults.standard.integer(forKey: "example") //=> 1

// -example <integer>1</integer>
UserDefaults.standard.float(forKey: "example")   //=> 1.0
UserDefaults.standard.double(forKey: "example")  //=> 1.0
UserDefaults.standard.integer(forKey: "example") //=> 1

Interestingly if you don’t provide a tag then integer(forKey:) doesn’t truncate it just returns 0

// -example 1.23
UserDefaults.standard.integer(forKey: "example") //=> 0

String

Strings can simply be passed directly unless you want to do anything more complex in which case you’d want to wrap in <string></string> tags.


Piecing things together

In some cases you might want to pass more complex data. One such example I came across was wanting to inject in a user profile that has multiple properties for UI testing. I could design the api so that the UI tests would pass multiple arguments but that would require validation and error handling in the app. It would be handy if I could just pass a JSON blob and then decode it inside the app. This is possible with two additional steps

  • Wrap the JSON blob in <string></string> tags
  • Escape the XML entities

Let’s imagine I have the following fictitious User type

struct User: Decodable {
    let name: String
    let token: String
    let isPaidMember: Bool
}

It might be handy in my UI tests to perform a login once at the beginning of all tests to get a token and then inject it into all the tests. I can also toggle the status of isPaidMember in each test. An appropriate argument that would work would look like this:

# {"name":"Paul","token":"some-token","isPaidMember":true}
-user <string>{&quot;name&quot;:&quot;Paul&quot;,&quot;token&quot;:&quot;some-token&quot;,&quot;isPaidMember&quot;:true}</string>

The corresponding code to parse this and fail silently in case of error would look like this

let user = UserDefaults.standard.string(forKey: "user")
    .flatMap { try? JSONDecoder().decode(User.self, from: Data($0.utf8)) }

Conclusion

UserDefaults can make launching your app and injecting data pretty simple. All your favourite primitive types are supported and once you get your head around using the plist markup you can start passing all kinds of data in just the right format for your needs.

Mobile UI testing with Maestro (Swift version)

In my last post I talked about building a DSL for Maestro using Kotlin. As an intersting exercise I asked some colleagues (Adam, Ellen and Saad) “could we build an equally nice DSL in Swift?”. Together we worked through a few ideas and this post expands on the findings we made.

TL;DR if you want to poke around and try out the result check out the repo here.


Working backwards from the target

Let’s look at the half way line of where we want to be.

try maestroRun("com.apple.MobileAddressBook") {
    LaunchApp("com.apple.MobileAddressBook")

    TapOn(.text("Add"))

    TapOn(.id("First name"))
    Input("John")

    if shouldEnterLastName {
        TapOn(.id("Last name"))
        Input("Appleseed")
    }

    TapOn(.text("Done"))
}

The above uses a @resultBuilder to collect an array of Commands, thanks to result builder we get the full expressivity of if/switch statements and loops.

The commands we collect are types that conform to Command, which is defined like this

public protocol Command {
    var data: Any { get }
}

The requirements from this protocol are pretty weak with a single getter of type Any but in this system this will represent a JSON encodable type String, Int, Object, Array, etc. Although the requirement is weak the assumption is that the framework will likely provide all implementations of Command. This could be strengthened by using an enum but I’ll leave that for a future tangent.

In order to build up a collection of these commands we can create a simple @resultBuilder with most the overloads required to get the various control flow operations to work.

@resultBuilder
public enum FlowBuilder {
    public static func buildBlock(_ components: any Command...) -> [any Command] {
        components
    }

    public static func buildArray(_ components: [any Command]) -> [any Command] {
        components
    }

    public static func buildOptional(_ component: (any Command)?) -> [any Command] {
        component.flatMap { [$0] } ?? []
    }

    public static func buildEither(first component: any Command) -> [any Command] {
        [component]
    }

    public static func buildEither(second component: any Command) -> [any Command] {
        [component]
    }
}

Implementing all of the above might look like busy work but it’s the secret sauce that allows us to use if/when statements and loops so it’s worth the effort for a natural call site.

Next up we need to build the actual yaml document that maestro will read. For this we can import a YAML library like Yams and dump the result of our builder and prepend some required front matter - in this case Maestro wants the key value pair to identify the appId of the app it’s targeting.

public func maestroCompose(_ bundleID: String, @FlowBuilder composition: () -> Flow) throws -> String {
    try "appId: \(bundleID)\n---\n" + Yams.dump(object: composition().map(\.data))
}

With this in place (and all the commands) we can now use a builder to generate a list of commands that will be rendered to the correct format.

If the maestro cli is installed in the standard location (~/.maestro/bin/maestro) we can create a helper function to shell out to the executable

public func maestroRun(_ bundleID: String, @FlowBuilder composition: () -> BasicFlow) throws {
    let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent("commands.yaml")

    try maestroCompose(bundleID, composition: composition).write(
        to: fileURL,
        atomically: true,
        encoding: .utf8
    )

    let process = try Process.run(
        FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".maestro/bin/maestro"),
        arguments: ["test", fileURL.path]
    )
    process.waitUntilExit()
}

Page Objects

A common pattern in UI testing is to create Page Objects that represent parts of the UI and hide all the details about finding items on the screen and encapsulate knowledge of how to then interact with these components.

In order to introduce the concept of Page Objects I wanted it so that you couldn’t just arbitrarily mix primitive commands and flows e.g.

try! maestroRun("com.apple.MobileAddressBook") {
    HomePage {
        $0.tapAdd() // Some scope type that can work with `HomePage` ✅
        LaunchApp("com.apple.MobileAddressBook") // ❌
    }
}

To achieve this we need to introduce a stronger type than [any Command]. I’ve got nothing against [any Command] but I wanted to use phantom types to give some additional compile time identity to the collection of commands. For example commands produced by one page object should be incompatible with other page objects because it makes no sense to try and interact with one page’s elements when it’s not the current page.

For this I created a new type called Flow that wraps the bare [any Command] we currently work with. The full Flow type is defined like this

public struct Flow<T> {
    public let commands: [any Command]

    init(_ command: Command) {
        commands = [command]
    }

    init(_ commands: [Command]) {
        self.commands = commands
    }

    init(_ flow: Flow?) {
        commands = flow?.commands ?? []
    }

    init(_ flows: [Flow]) {
        commands = flows.flatMap(\.commands)
    }
}

The type is essentially a dumb container that can be instantiated in loads of ways just to hold on to the underlying commands, it takes on the role of mostly erasing one type and introducing a new more specific one. The phantom type T makes it so that Flow<Void> is a distinct type from Flow<Other>, this allows us to use the same result builder but only allow the results to be compatible when the types line up

@resultBuilder
public enum PageBuilder<T: Page> {
    @available(*, deprecated, message: "Use the methods defined defined on the page")
    public static func buildExpression(_: Command) -> Flow<T> {
        fatalError()
    }

    public static func buildExpression(_ expression: Flow<T>) -> Flow<T> {
        expression
    }

    public static func buildExpression(_ expression: Page) -> Flow<T> {
        .init(expression)
    }

    public static func buildBlock(_ components: Flow<T>...) -> Flow<T> {
        buildArray(components)
    }

    public static func buildArray(_ components: [Flow<T>]) -> Flow<T> {
        .init(components)
    }

    public static func buildOptional(_ component: Flow<T>?) -> Flow<T> {
        Flow(component)
    }

    public static func buildEither(first component: Flow<T>) -> Flow<T> {
        component
    }

    public static func buildEither(second component: Flow<T>) -> Flow<T> {
        component
    }
}

To make use of the above builder we’d need to define a Page like this (bear with me this will be simplified):

struct HomePage: Page {
    var commands: [any Command] = []

    init(@PageBuilder<HomePage> content: @escaping (HomePage) -> Flow<HomePage>) {
        self.commands = content(self).commands
    }

    @FlowBuilder<HomePage>
    func tapAdd() -> Flow<HomePage> {
        TapOn(.text("Add"))
    }
}

The above looks annoying having to add the boilerplate of the init in the right format with funky page builder syntax and storage for commands. Luckily the kind Swift developers have given us a lovely tool for getting rid of boiler plate in the way of macros. The end result will look like this

@Page
struct HomePage {
    @FlowBuilder<HomePage>
    func tapAdd() -> Flow<HomePage> {
        TapOn(.text("Add"))
    }
}

I’m not going to get into the details of the macro too much as I’m pretty sure I’ve butchered the implementation but it kinda works so I was happy. The general idea was to have an attached extension macro to add conformance to the Page protocol and generate the required initialiser overloads. There is another attached member conformance that allows us to add the storage for var commands: [any Command]. For all the gruesome details check out the macro implementation.

With this in place we can now define page objects that encapsulate knowledge of how to find and interact with components and share this with our team e.g. here’s entering names into a form of the contacts app

@Page
struct EditFormPage {
    @FlowBuilder<EditFormPage>
    func setFirstName(_ name: String) -> Flow<EditFormPage> {
        TapOn(.id("First name"))
        Input(name)
    }

    @FlowBuilder<EditFormPage>
    func setLastName(_ name: String) -> Flow<EditFormPage> {
        TapOn(.id("Last name"))
        Input(name)
    }

    @FlowBuilder<EditFormPage>
    func tapDone() -> Flow<EditFormPage> {
        TapOn(.text("Done"))
    }
}

Sharing the above means that members of the team don’t need to look up the different selector strategies and ways of interacting with controls as they can simply write the following:

try! maestroRun("com.apple.MobileAddressBook") {
    LaunchApp("com.apple.MobileAddressBook")

    HomePage {
        $0.tapAdd()

        EditFormPage {
            $0.setFirstName("John")

            if shouldEnterLastName {
                $0.setLastName("Appleseed")
            }

            $0.tapDone()
        }
    }
}

Wrap up

This post (and accompanying repo) show combining a few Swift language features @resultBuilders, macros and phantom types to build a DSL that prevents consumers from creating wonky input as things are checked at compile time. Maestro isn’t just good for UI tests I’ve found it really helpful just automating flows I need to reproduce whilst doing my day to day dev work with minimum fuss so I’d highly recommended checking it out.