It's time to throw! Migration Experience from Objective-C to Swift
Oleg Alekseenko, head of iOS Superjob development, talks about the company's experience in switching from Objective-C to Swift.
The article is based on the speech at RIT2017.
')
Superjob has 3 mobile apps for iOS:
Job search (resume creation, job search, etc.)
Employee search (job creation, resume search)
Production calendar (planning of working hours and vacations)
Per day applications are downloaded more than 3 thousand times, the number of active users per day is more than 250 thousand.
The first versions of applications developed in 2012. All applications were originally written in Objective-C. After 5 years, a new language, Swift, was released, we decided to switch to it. What did we want to achieve?
Increase code predictability. The application has a filter model with 20 parameters, and this model must implement a method (to be comparable). Many places where changes are possible. This all brings a lot of pain to the business logic, since when adding new properties everything should be taken into account in all sections of the application. In Objective-C, you need to follow it with your hands, check all 100,500 places - the probability of an error increases. Swift makes such a situation impossible in principle.
Migrate to objc libraries in advance. New libraries and UI components are written in Swift. Older ones are not supported. For example: the situation with Reactive Cocoa. If you do not go today, then in five years we will have a dead piece in the application.
Increase the efficiency of the team and the stability of the project: we have become more attractive to new employees, have adopted internal standards of work quality. 70% of new candidates for the Superjob development team wanted to write specifically on Swift.
What specific steps in our case had to be done to go
Increase Nullability
At the start - less than 5%. Inability to start the transition here and now. At the first attempt to implement Swift, there were many places in the application where Objective-C told us about the existence of an object, and in fact there was no object at run-time. When we tried to transfer such an object to Swift, the application crashed.
How to solve:
3 months for each modified file added Nullability.
We wrote a script that prevents Pull Request to pass without tags.
What was achieved: in three months Nullability reached 60%. This is the threshold from which to start the transition.
Result: in 3 months we improved the quality of the code, the application stopped writing when writing code on Swift. Laid the system for further development of applications on Swift.
We migrate in advance from objc libraries
We use CocoaPods as a dependency manager. We had libraries connected through it. They can be divided into two categories:
Those that are easily replaced by Swift-analog.
Those who simply did not change, but demanded the writing of a migration or the selection of an analog (with the subsequent writing of a migration) on this fucking analog.
In our case, the project was a library of ReactiveCocoa.
There is no understanding of what you get. For example, this is how the call to the ReactiveCocoa method in Swift looks like
And therefore it is necessary every time to cast to the desired type and constantly remember why we cast
profileFacade .authorize(withLogin: login, password: pass) .subscribeNext { (response) inif let user = response as? SJAProfileModel { print("\(String(describing: user.name))") } }
We define the decision criteria:
• To make it easy to use. • To have strict typing. • Swift like API.
In the end, chose RxSwift.
How we made ReactiveCocoa friends with RxSwfit
As a solution, we wrote a category on RACSignal , which turns untyped ReactiveCocoa signals into typed Observable RxSwfit. First steps: create an Observable and subscribe to new RACSignal values. When retrieving new data in RACSignal, we try to convert it to the type we specified in generic using convertBlock (we will consider it a little later). If it does, then we forward the new typed value further to Observable subscribers. And if not, then we report an error of casting to the required type.
Then we have a public method, which internally calls the Observable creation function and closes the RACSignal values, which are converted to the required type specified in generic.
This solution is well suited for standard types and collections, for example NSArray is easily cast into swift Array, NSNumber is swift, but in ReactiveCocoa there is such a data structure as RACTuple. There was a problem with it, because it just doesn’t work on the death of it, so, especially for RACTuple, I had to write a separate method that unpacks each value from the RACTuple and collects the carthage from them.
And, like the kernel itself, the function of casting an untyped value to a typed one is made.
internalfuncrx_cast<T>(_ value: Any?) -> T? { iflet v = value as? T { return v } elseifletE = T.selfas? ExpressibleByNilLiteral.Type { returnE.init(nilLiteral: ()) as? T } returnnil }
At the input of the function we pass any value that came to us from RACSignal, and the necessary type to which we need to lead. If it turns out to bring the value to the type immediately, then the value itself is returned; if not, the second step is to check whether the type to which we are trying to cast is optional. If so, then create a variable of this optional type with an empty value. The last manipulations are needed, because if you do not create an optional variable, but simply return nil, the compiler will say that it cannot bring nil to the type T we need.
Now you can call the rxMap function of RACSignal and transfer the required type that we expect in the subscribe block, and from this moment on onNext we will always get a user model
We immediately show in it that we are returning Observable, and inside we simply call rxMap (), and in this case it is not necessary to specify which type should be reduced to. The type itself is pulled from the return value. As a result, we get rid of the need to type types each time, and we only do it once.
A large amount of existing application code cannot be replaced immediately. This leads to a problem: not all Swift features are available in Objective-C.
What exactly is not available from what we need to use:
Struct
Enum
Moki
The solution is Sourcery.
This solution is able to auto-generate code.
It's easier to understand this with an example: we have a Resume structure that must satisfy the protocols Hashable, Equatable. But if you implement them yourself, you always have to remember that you can not forget to take into account a new property. You can trust all this to do Sourcery. To do this, we point out that our struct Resume satisfies the two protocols, AutoHashable and AutoEquatable.
These protocols themselves are nothing of this kind.
protocolAuthoHashable{} protocolAutoEqutable{ }
They are simply needed in order for Sourcery to understand which template to use for a particular structure.
Now you can run Sourcery. We get a file in which the implementation of the Hashable and Equatable protocols for Resume is automatically generated. If we embed sourcery in the build phase, then we don’t have to worry that, adding new properties to our resume, we will forget to take them into account.
Hashbale and Equtable autogeneration templates are out of the box, but this does not limit us, as we can write a template for our needs. For example, we have such an enum.
In this template, we scan all the types found. If it is enum, then we create an extension for it, in which we declare a static variable with a quantity.
extensionConf{ staticvar numberOfCases:Int = 4 }
Therefore, we took the opportunity to write our templates to port the struct in Objective-c. We had to use this trick so that in those places that have not yet been rewritten to Swift, we could work with a resume. As a result, we automatically generate a class ResumeObjc from our structure, which we can use in the old Objective-c code.
On the example of mocks for tests
When writing tests in Objective-c, we often used swizzling for mocks. But in Swift this is impossible, so I had to create some “FakeProtocolClass” and there implement all the necessary methods, add specially additional variables that show whether the method was called or not. Sourcery can help again, which automatically generates such mocks.
Pump team
Over the past six months, three out of four candidates for interviews at Superjob talked about wanting to work on Swift.
When switching to Swift, it was important to take into account organizational issues in the team, such as code style and work with resources. The team has been working on Objective-C for many years, and everyone had their own vision about Swift. Therefore, we needed a tool that would help direct the team in the right direction. One of the most famous tools for the Swift codestyle is SwiftLint.
SwiftLint allows you to enter your own rules. This helped us to register the errors peculiar to our team, and quickly get rid of them. For example, we wrote a rule that forbids the use of ReactiveCocoa in Swfit.
We also wanted to unify the work with graphics, as the project went through several redesigns. SwiftGen helped with this: when deleting an icon, it tells you where it was used.