📜 ⬆️ ⬇️

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:





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?

  1. 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.

  2. 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.

  3. 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:


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:


In our case, the project was a library of ReactiveCocoa.


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.

 extension RACSignal { private func rxMapBody<T>(convertBlock: @escaping (Any?) -> T?) -> Observable<T> { return Observable.create() { observer in self.subscribeNext( { anyValue in if let converted = convertBlock(anyValue) { observer.onNext(converted) } else { observer.onError(RxCastError.cannotConvertTypes) } }, ... ... ... return Disposables.create() { } } } 

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.
 extension RACSignal { public func rxMap<T>(_ type: T.Type = T.self) -> Observable<T> { return rxMapBody() { anyValue in if let value: T = rx_cast(anyValue) { return value } else { return nil } } } } 

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.

 public func rxMapTuple<Q, W>(_ type: (Q, W).Type = (Q, W).self) -> Observable<(Q, W)> { return rxMapBody() { anyValue in if let convertible = anyValue as? RACTuple, let value: (Q, W) = convertible.rx_convertTuple() { return value } else { return nil } } } 

And, like the kernel itself, the function of casting an untyped value to a typed one is made.

 internal func rx_cast<T>(_ value: Any?) -> T? { if let v = value as? T { return v } else if let E = T.self as? ExpressibleByNilLiteral.Type { return E.init(nilLiteral: ()) as? T } return nil } 

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

 profileFacade .authorize(withLogin: login, password: pass) .rxMap(SJAProfileModel.self) .subscribe(onNext: { (user) in }) .addDisposableTo(disposeBag) 

It is necessary to make it more convenient and write extensions to the facade itself.

 extension SJAProfileFacade { func authorize(login: String, passwrod: String) -> Observable<SJAProfileModel> { return self.authorize(withLogin: login, password: passwrod).rxMap() } } 

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.

 profileFacade .authorize(login: login, password: pass) .subscribe(onNext: { (user) in }) .addDisposableTo(disposeBag) 

Objective doesn't just let go

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:


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.

 struct Resume: AutoHashable, AutoEquatable { var key: Int? let name: String? var firstName: String? var lastName: String? var middleName: String? var birthDate: Date? } 

These protocols themselves are nothing of this kind.

 protocol AuthoHashable {} protocol AutoEqutable { } 

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.

 extension Resume: Hashable { internal var hashValue: Int { return combineHashes([key?.hashValue ?? 0, name?.hashValue ?? 0, firstName?.hashValue ?? 0, lastName?.hashValue ?? 0, middleName?.hashValue ?? 0, birthDate?.hashValue ?? 0, 0]) } } extension Resume: Equatable {} internal func == (lhs: Resume, rhs: Resume) -> Bool { guard compareOptionals(lhs: lhs.key, rhs: rhs.key, compare: ==) else { return false } guard compareOptionals(lhs: lhs.name, rhs: rhs.name, compare: ==) else { return false } guard compareOptionals(lhs: lhs.firstName, rhs: rhs.firstName, compare: ==) else { return false } ... ... return true } 

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.

 enum Conf { case Apps case Backend case WebScale case Awesome } 

We want to build some kind of logic on the number of enums in enum, for this you can write a template and pass it to Sourcery.

 {% for enum in types.enums %} extension {{ enum.name }} { static var numberOfCases:Int = {{ enum.cases.count}} } {% endfor %} 

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.

 extension Conf { static var 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.

Source: https://habr.com/ru/post/331780/


All Articles