18 May 2024
I’ve recently started using mise “a polyglot tool version manager” and have been really impressed with how much simpler it makes configuring projects.
For example if I want to use a tool like swiftlint I have to make sure that all other developers on my team and the CI machine agree on which version to use.
If we have different versions we might end up with contrasting linting rules causing various errors/disagreement.
In the past I’ve dealt with this by bundling the binary into the repository but this consumes repository space that will never be reclaimed.
A better way
Enter mise
a tool which I only investigated because tuist started using it for managing its versions.
With mise
I can create a file in my repository called .mise.toml
and configure the version of swiftlint
I want to use like this
.mise.toml
[tools]
swiftlint = "0.55.1"
With this file in place I can call mise install
and mise
will download the version I specified and make sure it is installed on my $PATH
.
Now when I call swiftlint
from within my projects directory it will ensure that the correct version of swiftlint
is used.
What about in Xcode?
mise
works by modifying $PATH
when you cd
into a directory, so that won’t work in Xcode run scripts.
Fortunately mise
provides a solution; instead of calling swiftlint
directly in my run script I can instead tell mise
to execute swiftlint
like this
$HOME/.local/bin/mise x -- swiftlint
mise
will do the same thing of looking inside the .mise.toml
file and then making sure it invokes the correct version of swiftlint
for me.
What about on CI?
You can use the same trick above on CI or if you are using github actions there is a specific mise
action that will take care of calling install and setting up the path for any following actions.
Can it help with fastlane?
Yes.
fastlane
is a ruby gem and Ruby is not always easy to get configured right especially if you’ve not got much terminal experience.
Now I love Ruby but I hate to think how many hours/days I lost over the years helping colleagues get their environment set up.
I’ve also personally changed my Ruby version management tooling from rvm to chruby to rbenv over the years in search of a solution that is stable.
Thankfully mise
is polyglot and so I can now just use this for making sure that I have a version of Ruby suitable for use with fastlane.
e.g. updating my .mise.toml
to
.mise.toml
[tools]
ruby = "3.3.0"
swiftlint = "0.55.0"
Then again running mise install
- this time it will take longer as it builds me the version of Ruby I specify.
For best results with building Rubies I’ve found it wise to get your build environment set up as recommended here.
Any more?
I work in different languages on various Kotlin/Java code bases.
Nothing is ever simple so of course those different code bases require different JVMs for which I had to use a version manager - I was using jenv.
Now that I have mise
I don’t need jenv
anymore and my entire set up is simplified further.
In fact there is an eye watering list of tooling that mise
supports which you can see here and it doesn’t look difficult to add your own plugin if you want to support different tools.
Wrap up
I’m really liking mise
.
There were a couple of things to figure out like the mise x
command but after that and realising I can bin off my various other tools for managing versions I feel I have a much cleaner and more maintainable way to get build environments set up across the team.
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.