Generics are a powerful language feature that we use daily when using the Swift standard library without really thinking about it.
Things get tricky when people first start to write their own APIs with generics, there is often a confusion about what/why/when/how they should be used.
This post runs through a worked example of writing a generic function and explaining the benefits as we go.
Problem Outline
Let’s imagine that we want a function that will take an array of shapes and remove any where the area is less than 100. The function should be able to handle multiple different shape types - here are two example types:
There are plenty of ways to tackle this problem so let’s just pick one to begin. Without generics we might try writing a function that ignores types by working with Any.
funcfilterSmallShapes(_shapes:[Any])->[Any]
To write the implementation we need to cast to the correct type, call the area function and compare it against 100.
1) It can crash at run time if we use it on any type that is not a Square or Rectangle.
filterSmallShapes([Circle(radius:10)])// This will crash as we have no implementation for `Circle`
2) The size predicate logic is duplicated twice.
This is not great because it means we’ll need to update multiple places in our code base if the core business rules change.
3) The function will keep getting bigger for every type we support. 4) We get an array of Any as the output, which means we’ll probably need to cast this output to a more useful type later.
On line 6 above we have to cast back to a more specific type in order to do anything useful, in this case a simple equality check.
This might not seem too bad but we must remember that this cast happens at runtime, which means that we put more pressure on our testing to ensure that we are exercising all possible scenarios in our code.
Second Attempt
Let’s introduce a protocol so that we don’t need to cast for each shape type. Doing this will resolve issues 1, 2 and 3.
This implementation is a big improvement but we now return [Sizable] as the output, which is just as unhelpful as [Any] from the first attempt, which will still require a runtime cast.
Third Attempt
To resolve all the issues that we have encountered so far we might decide our previous attempts were ok but it might be easier to just duplicate the code and keep everything type safe:
This all works but we have reintroduced a couple of issues from our first attempt:
1) The size predicate logic is duplicated twice.
2) The function will keep being duplicated for every type we support.
The Generic Approach
This approach is a combination of the above attempts. The idea is that we’ll ask Swift to generate the various versions of our function (like in attempt 3) by providing a generic function that it can use as a blueprint.
I’ll show the implementation and then explain the new bits:
The function body is identical to attempt two, the real change is in the function definition. We’ve introduced a “placeholder” type between <> that we have called Shape. This placeholder type has some constraints placed upon it where by we are saying it has to be a type that conforms to Sizable, this is indicated by writing Sizable after the :.
Our test is identical to the one written in attempt three - it’s as if we have just duplicated the function.
To understand how this all works I like to imagine the following mental model:
The compiler sees that I am calling the function with a type of Square.
The compiler will check that Square conforms to Sizable.
If it does not then it will cause a compiler error.
The compiler will generate a specialised copy of the function where mentions of Shape are replaced with Square.
I have no idea about the technical implementation of this from the compiler’s point of view but externally as a language user this model works well for me.
Conclusion
Writing your first functions/types that have generics can seem a little daunting but the steep learning curve is worth it when things start to click and you see the possible use cases as well as understand when it’s not appropriate to use generics.
In the example above we end up in a position where we have no duplication of our core business logic (checking the area is < 100) and we have kept compile time type safety.
I think analysing a few versions of the function can help with understanding the benefits/disadvantages of our decisions and make us more aware of the tradeoffs we are making when designing our APIs.
Are your iOS Swift isolation tests running fast enough? My guess would be “no” unless you have taken deliberate actions to make them run faster. Here’s an approach to reduce the time we spend waiting for our tests to run.
Problem
If I head to Xcode and create a new iOS project with File > Project > Single View App. This gives me a bare bones project which hasn’t yet been sullied with my attempt at coding. I select the option to include unit tests so that everything is wired up and I get a blank test class.
From here I run the tests to get a sense of what my feedback cycle will be:
# Clean and Testtime xcodebuild -scheme SingleViewApp -destination'platform=iOS Simulator,name=iPad Air 2,OS=latest'-quiet clean test#=> 1) 38.470#=> 2) 32.856#=> 3) 34.413# Test with warm cachestime xcodebuild -scheme SingleViewApp -destination'platform=iOS Simulator,name=iPad Air 2,OS=latest'-quiettest#=> 1) 31.669#=> 2) 28.552#=> 3) 28.462
I’m not going to dig into why these run times are soo slow but just know that this is way off the mark. This is a project with no production code and an empty test file but I have to wait ~30 seconds for feedback.
The End Result
Before going through the practical details of the Swift Package Manager based approach to speeding this up let’s look at the end results - this way I can avoid wasting people’s time if they decide the improvements don’t merit reading further.
Here I look at a single module extracted from a production code base. It’s a small module at 4633 lines of Swift, which is made from several smaller modules. It’s executing a modest 110 tests:
Xcode Clean
SPM Clean
Diff
46.873
22.717
2.06x faster
54.753
19.897
2.75x faster
58.447
19.883
2.93x faster
Clean builds are still painfully slow but they are 2-3x faster when using this technique so that’s still a win.
If you simply rerun the tests without changing any code you get a wild speed up of ~39% but that’s not a realistic test case. So the numbers for cached builds here will take into consideration changing some code to force some recompilation:
Xcode Cached
SPM Cached
Diff
31.840
7.389
4.30x faster
29.501
7.166
4.11x faster
25.396
6.306
4.02x faster
This is where the numbers get more exciting - ~7 seconds is still an awfully long time to wait for feedback when you are in a flow but it’s much more manageable than 30-60 seconds.
I would start by creating a new SPM project - I’m going to go with the arbitrary name of UIKitless. The swift package init command could be used but it generates a lot of stuff we don’t need so it’s simpler to build it manually here:
With this we can run swift test and everything should work.
Worked example of a more complicated project
If you have modularised your project in any way then it will mean that you are using statements like import SomeOtherModule in your source files. This is not a problem as long as the module you want to import also follows the prerequisites above.
Here’s the additional steps that are required:
Symlink in the other module’s sources
Update the Package.swift file to tell SPM about this module
The trick is to use the same module name in SPM as is used in your code base e.g. if I import SomeOtherModule in my source files then the target name of my symlink should be SomeOtherModule
With this we can run swift test and everything should work.
Conclusion
Whilst this example is only using a small code base and possibly unrealistic tests it shows some promise. Hopefully I’ve shown that depending on how your codebase is structured this could be quite a cheap experiment to run and if it yields good results then you can keep using it. Having done a fair amount of Ruby where I would expect my tests to run in fractions of a second any time I save a file these tests are still mind numbingly slow - these things are improving all the time so I remain optimistic that testing won’t be this painful forever.
I write a lot of custom command line tools that live alongside my projects. The tools vary in complexity and implementation. From simplest to most involved here’s my high level implementation strategy:
A single file containing a shebang #!/usr/bin/swift.
A Swift Package Manager project of type executable.
A Swift Package Manager project of type executable that builds using sources from the main project (I’ve written about this here).
The same as above but special care has been taken to ensure that the tool can be dockerized and run on Linux.
The hardest part with writing custom tools is knowing how to get started, this post will run through creating a single file tool.
Problem Outline
Let’s imagine that we want to grab our most recent app store reviews, get a high level overview of star distribution of the recent reviews and look at any comments that have a rating of 3 stars or below.
Skeleton
Let’s start by making sure we can get an executable Swift file. In your terminal you can do the following:
The first line is equivalent to just creating a file called reviews with the following contents
#!/usr/bin/swiftprint("It works!!")
It’s not the most exciting file but it’s good enough to get us rolling. The next command chmod u+x reviews makes the file executable and finally we execute it with ./reviews.
Now that we have an executable file lets figure out what our data looks like.
Source data
Before we progress with writing the rest of the script we need to figure out how to get the data, I’m going to do this using curl and jq.
This is a useful step because it helps me figure out what the structure of the data is and allows me to experiment with the transformations that I need to apply in my tool.
First let’s checkout the URL that I grabbed from Stack Overflow (for this example I’m just using the Apple Support app’s id for reviews):
This is a really fast way of experimenting with data and as we’ll see later it’s helpful when we come to write the Swift code.
The result of the jq filter above is that the large feed will be reduced down to an array of objects with just the title, rating and comment.
At this point I’m feeling pretty confident that I know what my data will look like so I can go ahead and write this in Swift.
Network Request in swift
We’ll use URLSession to make our request - a first attempt might look like:
2 we need to import Foundation in order to use URLSession and URL.
6 we’ll use the default session as we don’t need anything custom.
7 to start we’ll just print anything to check this works.
8 let’s not forget to resume the task or nothing will happen.
Taking the above we can return to terminal and run ./reviews…. nothing happened.
The issue here is that dataTask is an asynchronous operation and our script will exit immediately without waiting for the completion to be called.
Modifying the code to call dispatchMain() at the end resolves this:
Heading back to terminal and running ./reviews we should get some output like Optional(41678 bytes) but we’ve also introduced a new problem - the programme didn’t terminate. Let’s fix this and then we can crack on with the rest of our tasks:
Lines 7-10 are covering cases where there is a failure at the task level.
Lines 12-15 are covering errors at the http level.
Lines 20-23 are covering cases where there is no data returned.
The happy path is hidden in lines 17-20.
Side note: Depending on the usage of your scripts you may choose to tailor the level of error reporting and decide if things like force unwraps are acceptable. I tend to find it’s worth putting error handling in as I’ll rarely look at this code, so when it goes wrong it will be a pain to debug without some guidance.
Parsing the data
We can look at the jq filter we created earlier to guide us on what we need to build.
In order to decode an Entry we’ll provide a custom implementation of init(from:) - this will allow us to flatten the data e.g. instead of having entry.title.label we end up with just entry.title. We can do this with the following:
With this done we can wire it all up - we’ll go back to the happy path and add:
do{print(tryJSONDecoder().decode(Response.self,from:data))exit(EXIT_SUCCESS)}catch{print("Failed to decode - \(error.localizedDescription)")exit(EXIT_FAILURE)}
That’s the complicated stuff out of the way - the next part is the data manipulation that makes the tool actually useful.
Processing the data
Let’s start by printing a summary of the different star ratings. The high level approach will be to loop over all the reviews and keep a track of how many times each star rating was used. We’ll then return a string that shows the rating number and then an asterisk to represent the number of ratings.
The other task we wanted to do was print all the comments that had a rating of 3 or less. This is the simpler of the two tasks as we just need to filter the entries and then format for printing:
Creating tools is a lot of fun and isn’t as scary as it might seem at first. We’ve done networking, data parsing and some data munging all in one file with not too much effort, which is very rewarding.
The single file approach is probably best for shorter tasks. In the example above it’s already becoming unwieldy and it would be worth considering moving to a Swift Package Manager tool (maybe that’s a future post).