09 May 2024
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 T
s 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.
10 Mar 2024
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>{"name":"Paul","token":"some-token","isPaidMember":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.
13 Dec 2023
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 Command
s, 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.