Apps vs Swift 3
Migrating a big app to Swift 3
Swift, the new Apple multipurpose programming language made its first appearance 2 years ago. Thanks to the ongoing support from the Apple community it has been constantly updated ever since its first release.
There have been 3 major releases of Swift since it first launched, the third was this past September. So what does this mean? For devs working with the language, the improvements of the release have come with its fair share of both pros and cons.
To get an idea, Swift was made to interact smoothly with the family of objective-C and to use some of the functions provided by the C standard library. With the release of Swift 3, many of the usages and connections with those external languages were rebuilt in a swifty way. To see all the official new features, check out the Swift website here.
For example, for those who use Swift’s async framework to execute tasks in other threads are used to dealing with something like this:
And because this has so many global functions and global variables, Apple has rewritten it as follows:
As you can see they are implementing a more swifty approach.
The same change goes for various other cases as well. For example the migration from the old objective-C NS classes to the new Swift Classes. (NSTimer -> Timer).
Note: This last example is not a refactor of the name but an entire rebuild of the solution of the class.
With the many improvements of Swift 3 also came the inconveniences (or processes we may not be used to). Now, one must migrate the apps they are working on in order to support the new versions of the OS and the IDE, XCode. Additionally, this new release of Swift has an uncountable number of breaking changes compared to previous versions. Therefore, one must port the parts of the code available and find some workarounds to the parts that no longer exist. This also means the libraries you are using have to make the port too, so those that are no longer maintained may be deprecated.
This may sound overwhelming, however, Swift has added a built- in plugin to port the code automatically. Yes, it has its downside, so keep in mind for those apps that use many frameworks and complex code the plugin may not be enough.
This leads me to the idea of this post…
To understand how to port an app with many frameworks and complex structures leaving you with the tools and resources to port your own apps!
The app we are migrating targets iOS 10 or higher and uses Carthage to manage the dependencies of the project. Currently, we are using Fastlane to handle deployment, testing, certificates and provisioning profiles, Parse to store data and AWS to contain the Parse server with Lambda functions. This way some of the apps backend logic is in a serverless architecture.
The app also uses a bridge with a lower layer made in Objective-C to connect via Bluetooth with an external hardware-based module. As well as using S3 to store various files.
Useful resource: https://martinfowler.com/articles/serverless.html
Estimation of effort
To estimate effort we suggest using the function points technique. This technique is useful to objectively measure the task based on its historical effort, its size, and its uncertainty.
Unfortunately, with this method, we found no way to split the migration to better measure the task involved. This is due to the fact that once you start migrating the code the app will no longer compile or work properly until it is complete. And this process can take up to a few hours or even weeks, trust me.
Before porting, it is essential that you check the releases of your dependencies. In the best case scenario, all your dependencies will have finished porting, however, the reality is many of your dependencies may not have ported their projects yet. Therefore, in this case, you have two options: to estimate the cost of removing the dependency and implement an ad-hoc solution against the cost of migrating the dependency.
Inevitably, you’ll have to make a decision between these two options. The worse that can happen is the dependencies are not maintained and are unable to receive any port from the maintainers. If this is the case, you can prescind from the library or you can make the port and submit a Pull Request to the project. Checking the state of dependencies must be done before starting the migration.
IDE and Devices
If you’re targeting the new versions of the OS, you must have at least one device that runs on iOS 10 or higher for mobile and the equivalent for the other platforms. Additionally, you must download Xcode 8 or higher to make the port.
The migration process
Due to size, I suggest splitting the project into 5 parts:
- Dependencies: Update the dependencies, from Cartfile or CocoaPods configuration to porting them.
- Project compiles: Port the code using the embedded plugin, then refactor it so the project can compile
- Test compiles: Port the tests for the entire app to compile.
- Business logic: Refactor the business logic for the app to work properly, according to Swift 3 standards.
- Test logic: Refactor the tests in conjunction with the business logic, to remove all inconsistencies within the app
- QA test: Full QA test of all functionalities to avoid any regressions.
It is important to stay organized in order to complete the migration properly.
Now, let’s put this process into play.
In my case, we removed 2 unported dependencies and ported 1 individually, making a PR to the project which was eventually merged to master. Note: if you are using ReactiveCocoa for your application, I recommend visiting the Github page to see what has changed.
Once you update the Cartfile with the new versions of the dependencies, you must update them. Pay close attention here because you may run into situations where Carthage doesn’t finish the update. If this is the case here are some reminders:
- Be sure all of your dependencies have been ported to Swift 3.
- For any that you had to port, remember to update the entrance for your dependency in the Cartfile. It should look like this: Github “ReactiveCocoa/ReactiveObjCBridge” “master”
- You can run the update adding the — no-use-binaries flag to the update command of Carthage. This way you build your dependencies locally instead of using the pre-built frameworks which may still be using older versions of the compiler.
Project compiling is definitely the most tedious task in the migration process. Here is why.
As I mentioned (briefly) above, the Swift Migrator Tool is the embedded plugin that helps you with the initial code refactoring.
The problem with the migrator tool is it takes a long time to process all the source files in order to make the changes. My recommendation would be to run the tool several times as it will most likely continue finding replacements to be made in the code. Once all the changes are found with this tool, you can begin making your own changes.
So the battle begins: As we all know sometimes the compiler fails to generate SIL and, therefore, crashes while you are working. The good news is, this is easily detectable! The best indicators are when the text in the file is no longer highlighted and there is no information about typos (For more information about the compiler architecture click here).
Normally, this happens when you are working with nested blocks or for those who work with Reactivecocoa when you are working with FlatMaps. It is probable that a majority of your code is now in a state where the compiler cannot work properly and, therefore, crashes, showing you only a few errors to fix. Although it may be tempting to attack the migration one folder (or logic) at a time, this will end up only showing you the errors the compiler wants and not the errors you are looking for. If that is the case, here are some tips to fix errors without the compiler help:
- Given the new Enum guidelines, you can now search the implementations to refactor them. (They are no longer uppercase)
- There are many classes of Objective-C SDK that are no longer extending NSObject. This means there are new structures with the same name but without the prefix NS (NSURL -> URL)
- NSDate has been re-implemented into Date, but some functionalities have been split into Calendar and DateComponents. To see the usages and refactor the logic check out this post.
- Most of the new implementations of the SDK that return AnyObject/NSError are now returning Any/Error to be more generic.
Please note: For the those who use ReactiveCocoa and Rex:
Rex no longer exists in Swift 3, because ReactiveCocoa has added all the extensions and UI bindings to the main project. The project has split into more contained projects. This means that all the files that use most of the core reactive primitives from the library now have to import the ReactiveSwift library instead of ReactiveCocoa. This is another change you can make without the compiler help using the search functionality of XCode. You can visit the ReactiveCocoa main page here.
Does this mean that ReactiveCocoa no longer exists?
The correct answer is no, ReactiveCocoa still exists, however, it now has some limited functionalities, such as the UI Bindings mentioned above. What does this mean? Everywhere you import Rex you will now be importing ReactiveCocoa. Unfortunately, the change is not that simple, due to the bindings changing their architecture. Originally, the bindings looked like this:
Now they are under a proxy pattern and it looks like this:
ReactiveCoca removed many of the bindings because they were resulting in an anti-pattern. If that’s your case (mine too) then you must move the logic in the view, somewhere else, for example in the ViewModel.
Another thing you can do to fix errors more precisely is by entering one file at a time. This way the compiler checks for errors only in the file you are entering. This process usually takes a few seconds but when it works properly you will end up fixing the errors with the limited help of the compiler.
My recommendation: Make all the replacements you can by using the search text functionality of XCode. This way you remove errors without complaining with the compiler.
For more information check out the Swift Migration page.
While you are fixing the errors in your app, you will notice the compiler able to build all the files pointing at all the remaining errors in the project. Once all the errors in your project have been fixed you can then apply the same procedure to the tests. This step will be a lot simpler than it was for the entire project.
Once your project and tests compile correctly, you can then begin fixing the runtime errors. For this process, you can use the Swift error and Exception breakpoints that come with XCode. Although it will not solve all the problems, it’s definitely helpful.
Hopefully, the tests will have been updated in phase 3, however, sometimes the logic changes drastically leaving an inconsistency with the tests. Another possibility may be that the refactorer plugin changed the code making the tests to work improperly.
The good news is, checking your tests and the logic of your app are working correctly doesn’t take much time.
This step is quite simple to explain but the complexity will vary in each project. Once you’ve migrated the app you must do a full QA test searching bugs in your app. This could take a while so be patient because even if you spend the half of the time here it’s going to be a good investment if you end here with your app working exactly the way it was working before but with the new standards of the language.
Once this is complete, your app is ready to go!
In my case, this process took 4 weeks of time to migrate 25000 LOC. While I was working on this my teammates were working as usual in other features using Swift 2, so we had to rebase with master once in awhile. Additionally, after ending the migration we spend about 2 weeks doing QA to the app, while we were adding some extra features to the new Swift 3 branch, to avoid further conflicts.
In some cases an entire team cannot spend all of their time to migrate the app, so you’ll have to split tasks. Some people will work in the migration while the others will still work as usual in the project. The key is to know when you can stop working with Swift 2 and start with the new version. Each feature you add in Swift 2 have to be migrated, so the migration slows down. But if you completely stop the work in the project you will have to deal with customers who want their release in time. Again, it’s all a trade- off.
After migrating a big app one can see that many of the Swift 3 updates are undoubtedly a step forward for the language. For us devs, this upgrade is a lot to take in, but in the long run is definitely worth it for the apps we are coding.
Any questions, comments, or suggestions? Let me know below!