Technical reconstruction and practice of Swift in hand Amoy commodity evaluation

Posted by snpo123 on Thu, 06 Jan 2022 10:30:10 +0100

Author: Wang Zhejian (Zhejian)

After a month and a half of technical reconstruction, several months of iteration and volume, the new version of the commodity evaluation list of hand Amoy finally completed the whole process with 100% traffic stability on the double 11 in 2021. We have not only made a clear improvement in business, but also precipitated a lot of technical exploration, such as precipitated a light mode R & D framework based on DinamicX + event chain arrangement, promoted the upgrading of native language to Swift/Kotlin, and finally greatly improved the overall R & D efficiency and stability. (Note: DinamicX is an internal self-developed dynamic UI framework)

In this article, I will focus on the part about Swift. If you want to know how Swift improves R & D efficiency / quality, whether existing projects / modules need Swift as a native language, how to select models, what problems we encountered in the process of product evaluation landing Swift, and what benefits and conclusions are there, I hope this article can bring you some help.

First, why did I choose to learn Swift?

Technological change, the future has come

Because I am very firm in my heart. Compared with OC, Swift can carry the future better.

Strong backing

The main reason is that it has a strong backing. As the most important development language of Apple in the future, Swift has exported up to 73 WWDC contents, including but not limited to syntax, design, performance, development tool chain, etc. the specific contents are shown in the figure:

Looking back on the development of Swift in recent years, it has been seven years since it was officially released in 2014. In the whole process, Apple has invested a lot of energy in the construction of Swift, especially the emergence of Swift Only framework, which also means that apple is actively advocating everyone to invest in the development of Swift.

Three advantages

Second, Swift has three clear advantages: faster, safer and more expressive.

Faster means that Swift has made many optimizations in execution efficiency. For example, the swift system library itself adopts many basic types that do not require reference counting, which have been effectively improved in terms of memory allocation size, reference counting loss, static analysis of method distribution and so on. The specific details will not be analyzed here. Those interested can go to Understanding Swift Performance to understand the details.

The so-called security does not mean that Crash does not occur, but that any input has a clear performance definition. The original intention of Swift design is that developers can write code without any Unsafe data structure. Therefore, Swift has a very robust type system. Developers can complete all development work without considering the problem of pointers. At the same time, it also provides a series of types or functions prefixed with Unsafe for relatively Unsafe operations such as high-performance interaction with Unsafe languages (such as C language) and operation of raw memory. On the one hand, Unsafe is used to alert developers to use these API s, and on the other hand, it is zone classification to ensure that most development scenarios use safe types.

Here I can share a data. An App project I participated in was written by Pure Swift (99% +), and our online crash rate continues to be about 8 / 100000 all year round, which is a very impressive result for a small team (4 people at one end). We hardly use Unsafe API s, so that most of our problems can be avoided during compilation. The design of optional types and optional bindings forces developers to think about how to deal with scenarios with empty values, so as to nip developers' errors in the bud before software release.

It is more expressive. In short, it uses less code to express a complete piece of logic. In the Swift Evolution project, there have been 330 proposals to enhance Swift's expressiveness. Thanks to these features, Swift's code volume is about 30% - 50% less than OC. Let's give some practical examples

Builder Pattern

When we define a complex model with many properties, we don't want the properties of the model to be changed after initialization. We need to solve it through builder mode. The code is as follows:

// OCDemoModelBuilder.h
@interface OCDemoModelBuilder : NSObject

@property (nonatomic, copy, nonnull) NSString *a;
@property (nonatomic, copy, nonnull) NSString *b;
@property (nonatomic, copy, nonnull) NSString *c;
@property (nonatomic, copy, nonnull) NSString *d;
@property (nonatomic, copy, nonnull) NSString *e;
@property (nonatomic, copy, nonnull) NSString *f;
@property (nonatomic, copy, nonnull) NSString *g;
@property (nonatomic, copy, nonnull) NSString *h;

@end

// OCDemoModelBuilder.m

@implementation OCDemoModelBuilder

- (instancetype)init {
    if (self = [super init]) {
        _a = @"a";
        _b = @"b";
        _c = @"c";
        _d = @"d";
        _e = @"e";
        _f = @"f";
        _g = @"g";
        _h = @"h";
    }
    return self;
}

@end

// OCDemoModel.h

@interface OCDemoModel : NSObject

@property (nonatomic, readonly, nonnull) NSString *a;
@property (nonatomic, readonly, nonnull) NSString *b;
@property (nonatomic, readonly, nonnull) NSString *c;
@property (nonatomic, readonly, nonnull) NSString *d;
@property (nonatomic, readonly, nonnull) NSString *e;
@property (nonatomic, readonly, nonnull) NSString *f;
@property (nonatomic, readonly, nonnull) NSString *g;
@property (nonatomic, readonly, nonnull) NSString *h;

- (instancetype)initWithBuilder:(void(^)(OCDemoModelBuilder *builder))builderBlock;

@end

// OCDemoModel.m

@implementation OCDemoModel

- (instancetype)initWithBuilder:(void(^)(OCDemoModelBuilder *builder))builderBlock {
    if (self = [super init]) {
        OCDemoModelBuilder * builder = [[OCDemoModelBuilder alloc] init];
        if (builderBlock) {
            builderBlock(builder);
        }
        _a = builder.a;
        _b = builder.b;
        _c = builder.c;
        _d = builder.d;
        _e = builder.e;
        _f = builder.f;
        _g = builder.g;
        _h = builder.h;
    }
    return self;
}

@end

// Usage

OCDemoModel *ret = [[OCDemoModel alloc] initWithBuilder:^(OCDemoModelBuilder * _Nonnull builder) {
    builder.b = @"b1";
}];

// ret = a,b1,c,d,e,f,g

However, Swift's Struct supports attribute default values and initializing constructors, which makes the builder pattern not very meaningful. The code is as follows:

struct SwiftDemoModel {
    var a = "a"
    var b = "b"
    var c = "c"
    var d = "d"
    var e = "e"
    var f = "f"
    var g = "g"
    var h = "h"
}

// Usage

let ret = SwiftDemoModel(b: "b1")

// ret = a,b1,c,d,e,f,g

State Pattern

When the execution result of a function may have many different states, we usually use the state pattern to solve the problem.

For example, when we define a function, the execution result may have three states: finish\failure\none. Because there are some associated values, we can't use enumeration to solve them. Three specific types need to be defined. The specific codes are as follows:

///  Executable.h
@protocol Executable <NSObject>

- (nullable NSDictionary *)toFormattedData;

@end

///  OCDemoExecutedResult.h
@interface OCDemoExecutedResult: NSObject<Executable>

///Construct a null return value
+ (OCDemoNoneResult *)none;

///Return value of successful construction
+ (OCDemoFinishedResult *)finishedWithData:(nullable NSDictionary *)data
                                      type:(nullable NSString *)type;
///Construct an error return value
+ (OCDemoFailureResult *)failureWithErrorCode:(nonnull NSString *)errorCode
                                     errorMsg:(nonnull NSString *)errorMsg
                                     userInfo:(nullable NSDictionary *)userInfo;

@end

///  OCDemoExecutedResult.m
@implementation OCDemoExecutedResult

///Construct a null return value
+ (OCDemoNoneResult *)none {
    return [OCDemoNoneResult new];
}

+ (OCDemoFinishedResult *)finishedWithData:(nullable NSDictionary *)data type:(nullable NSString *)type {
    return [[OCDemoFinishedResult alloc] initWithData:data type:type];
}

+ (OCDemoFailureResult *)failureWithErrorCode:(nonnull NSString *)errorCode errorMsg:(nonnull NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo {
    return [[OCDemoFailureResult alloc] initWithErrorCode:errorCode errorMsg:errorMsg userInfo:userInfo];
}

- (nullable NSDictionary *)toFormattedData {
    return nil;
}

@end

///  OCDemoNoneResult.h
@interface OCDemoNoneResult : OCDemoExecutedResult

@end

///  OCDemoNoneResult.m
@implementation OCDemoNoneResult

@end

///  OCDemoFinishedResult.h
@interface OCDemoFinishedResult: OCDemoExecutedResult

///Type
@property (nonatomic, copy, nonnull) NSString *type;
///Association value
@property (nonatomic, copy, nullable) NSDictionary *data;

///Initialization method
- (instancetype)initWithData:(nullable NSDictionary *)data type:(nullable NSString *)type;

@end

///  OCDemoFinishedResult.h
@implementation OCDemoFinishedResult

- (instancetype)initWithData:(nullable NSDictionary *)data type:(nullable NSString *)type {
    if (self = [super init]) {
        _data = [data copy];
        _type = [(type ?:@"result") copy];
    }
    return self;
}

- (NSDictionary *)toFormattedData {
    return @{
        @"type": self.type,
        @"data": self.data ?: [NSNull null]
    };
}

@end

///  OCDemoFailureResult.h
@interface OCDemoFailureResult: OCDemoExecutedResult

///Error code
@property (nonatomic, copy, readonly, nonnull) NSString *errorCode;
///Error message
@property (nonatomic, copy, readonly, nonnull) NSString *errorMsg;
///Association value
@property (nonatomic, copy, readonly, nullable) NSDictionary *userInfo;

///Initialization method
- (instancetype)initWithErrorCode:(NSString *)errorCode errorMsg:(NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo;

@end

///  OCDemoFailureResult.m
@implementation OCDemoFailureResult

- (OCDemoFailureResult *)initWithErrorCode:(NSString *)errorCode errorMsg:(NSString *)errorMsg userInfo:(nullable NSDictionary *)userInfo {
    if (self = [super init]) {
        _errorCode = [errorCode copy];
        _errorMsg = [errorMsg copy];
        _userInfo = [userInfo copy];
    }
    return self;
}

- (NSDictionary *)toFormattedData {
    return @{
        @"code": self.errorCode,
        @"msg": self.errorMsg,
        @"data": self.userInfo ?: [NSNull null]
    };
}

@end

However, if we use the enum feature of Swift, the code will be much simpler:

public enum SwiftDemoExecutedResult {
    ///Correct return value
    case finished(type: String, result: [String: Any]?)
    ///Error return value
    case failure(errorCode: String, errorMsg: String, userInfo: [String: Any]?)
    ///Null return value
    case none
    ///Format
    func toFormattedData() -> [String: Any]? {
        switch self {
        case .finished(type: let type, result: let result):
            var ret: [String: Any] = [:]
            ret["type"] = type
            ret["data"] = result
            return ret
        case .failure(errorCode: let errorCode, errorMsg: let errorMsg, userInfo: let userInfo):
            var ret: [String: Any] = [:]
            ret["code"] = errorCode
            ret["msg"] = errorMsg
            ret["data"] = userInfo
            return ret
        case .none:
            return nil
        }
    }
}

Facade Pattern

When we define that an input parameter needs to conform to multiple protocol types, we usually use Facade Pattern to solve the problem.

For example, we have four protocols JSONDecodable, jsonecodable, xmlcodable, xmlcodable, and a method with two input parameters. Input parameter 1 is json, which requires to meet both JSONDecodable and jsonecodable protocols, and input parameter 2 is xml, which meets both xmlcodable and xmlcodable. When we use OC to solve problems, we usually write this:

@protocol JSONDecodable <NSObject>
@end

@protocol JSONEncodable <NSObject>
@end

@protocol XMLDecodable <NSObject>
@end

@protocol XMLEncodable <NSObject>
@end

@protocol JSONCodable <JSONDecodable, JSONEncodable>
@end

@protocol XMLCodable <XMLDecodable, XMLEncodable>
@end

- (void)decodeJSON:(id<JSONCodable>)json xml:(id<XMLCodable>)xml {

}

Two additional protocols JSONCodable and XMLCodable are defined to solve this problem. However, in Swift, we can use & to solve this problem without defining additional types. The code is as follows:

protocol JSONDecodable {}
protocol JSONEncodable {}
protocol XMLDecodable {}
protocol XMLEncodable {}

func decode(json: JSONDecodable & JSONEncodable, xml: XMLDecodable & XMLEncodable) {
}

The above are some of Swift's more expressive contents. Of course, the advantages are far more than these, but the space is limited and will not be expanded here.

In a word, thanks to Swift's high expressiveness, developers can express a complete logic through less code, which reduces the development cost and problems to a certain extent.

be a trend which cannot be halted

Swift not only has a strong backing and three advantages, but also has a good development trend in recent years.

Firstly, according to Githut, the pull request of Swift language in Github has surpassed OC, as shown in the following figure: (the data is up to October 25, 2021)

At the same time, the Swift hybrid application of Top 100 in China has also increased significantly, from 22% in 19 to 59% (the data is as of April 22, 2021)

The improvement here, on the one hand, is that many domestic first-line Internet companies have begun to layout. On the other hand, the emergence of Swift Only frameworks such as WidgetKit is also prompting everyone to start building Swift infrastructure.

Of course, the foreign data is more brilliant, which has reached 91%. It can be said that almost all of them have been used. Why do you say so? Because eight of the top 100 Google applications in the U.S. version don't use Swift.

Here's another data to share with you. When organizing the recruitment of authors of WWDC internal reference in our spare time, we collected the author's technical stack and points of interest. Finally, we found that more than half of the authors have rich Swift development experience, and 2 / 3 are interested in the content of Swift (a total of 180 samples). It can be seen that the community's enthusiasm for Swift is still very high. In the long run, whether to use Swift for development will also become one of the reasons why we choose to work.

Why select the product evaluation list?

Maybe many people will have an impulse to use Swift in our project immediately after seeing the first part. In order to avoid you paying for your "impulse", let me share my journey of choosing Swift from the "hand shopping product evaluation list".

First, let's briefly talk about my own experience in hand Amoy. At first, I joined the hand Amoy infrastructure group. One of my main responsibilities was to build Swift infrastructure. Later, due to organizational needs, I joined a new business architecture group. The focus of my work changed from the original Swift infrastructure upgrade driven business to the business pilot driven basic technology upgrade. In this process, we have mainly experienced three technical decisions:

  • I The first project the team took over: upgrading the manual order agreement to new Austrian innovation
  • II Based on the field understanding of business R & D, the team proposed new event chain arrangement ability and jointly built with DX
  • III Reconstruction of commodity evaluation, including evaluation list, interaction, etc

At each stage, I have thought about whether I want to use Swift, but I finally gave up using Swift, which I am good at, for the following reasons:

You need to have the prerequisites for using Swift

The reason why the order new Austrian innovation project did not use Swift as the main development language was that the basic infrastructure was not complete at that time. Most of the dependent modules almost don't support modules. It's almost impossible to use Swift hard, which will increase a lot of workload. It's not a wise move for a project with a short construction period. Under the balance, I gave up the idea of using Swift for the time being.

What kind of business is more suitable for Swift refactoring

When the basic conditions are complete, Swift will be a better choice for a business reconstruction project. No matter the trend of the environment or the unique advantages of Swift, it is not suitable to continue to use OC to reconstruct a business module.

For large-scale projects that want to try Swift, it is suggested that priority should be given to pilot businesses with small burden and involvement. At that time, another important reason why we gave up using Swift in the order of new Austrian innovation project was that the overall architecture of Austrian innovation was complex, the mixing of construction and data, and the high cost of local changes would lead to the problem of pulling one hair and affecting the whole body, and the openness and inclusiveness of the overall end-to-end new technology interaction was limited. However, there is no such problem in the evaluation of hand Amoy products, and there is more room to choose. Therefore, we firmly chose Swift as the main development language on the end side.

We should not only adjust measures to local conditions, but also obtain support

When the project has the conditions to use Swift, it must be comprehensively considered in combination with the current situation of its own team.

First, the team needs to train or equip a person with Swift development experience in advance to ensure the tackling of complex problems and the control of code quality. In particular, code quality. Most people who first contact Swift from OC will experience an "uncomfortable" period. During this period, it is easy to write Swift code with "OC flavor", so a person with enthusiasm, relevant experience and technical ability is particularly needed to practice and set an example.

At the same time, we also need the support of the supervisor, which is very key. It is difficult to continue an event only with the love of technology. It is also a very interesting process to continuously communicate with the supervisor in combination with the project situation, and constantly upgrade their thinking on a technology in the communication process, so as to make the supervisor from the initial query to the final support.

A certain technical foundation support is required

First, in terms of infrastructure completeness, we did a large-scale Module adaptation work to solve the core problem of mixed programming. At the same time, we upgraded DevOps, upgraded the package management tool tpod to 1.9.1, supported the static library version framework project at the source level, provided tpodedit mode to solve the dependency of header files, and added some core bayonet checks in the release link to prevent project deterioration.

Secondly, based on the existing technical solutions, after weighing the problems such as performance and efficiency, we finally carried out the exploration of R & D mode upgrading based on event chain arrangement in combination with our understanding of the pain points of business R & D. considering the cost, we built it in DX and exported it to Enn at the initial stage. The overall architecture is as follows:

In the UI layer, we use XML as DSL to ensure the consistency of both ends and reduce the development cost of both ends.

In terms of logical arrangement, we designed the event chain technology scheme to atomize each end-side basic capability as much as possible, so as to ensure that end-side capability developers can focus on capability development.

Based on the support of the above framework, developers can decide the development language used for a single basic ability. For novices, the starting cost of using Swift can be reduced by one level, and there is no need to struggle with the complex environment.

What problems have you encountered?

Frankly speaking, although we have made in-depth thinking in technical decision-making, we still encounter many problems when we really implement it.

The underlying library API is not adapted to Swift

Although Xcode provides the ability to "automatically" generate bridging files, most of the automatically generated Swift API s do not follow the "API Design Guidelines" due to the large differences between OC and Swift syntax, which will lead to many poorly readable and poorly maintained codes written in the currently accessed Swift service library.

At the same time, due to Swift's optional value design, it is necessary to sort out the optional settings of each external API input and output parameters when the OC SDK is provided to swift. Commodity evaluation relies heavily on a basic SDK, which does not do this well, so that we have encountered many problems.

Unnecessary compatibility due to incorrect derivation

Let's take a look at the following code:

// DemoConfig.h

@interface DemoConfig : NSObject

/* Useless code has been omitted here */

- (instancetype)initWithBizType:(NSString *)bizType;

@end

// DemoConfig.m

@implementation DemoConfig

- (instancetype)initWithBizType:(NSString *)bizType {
    if (self = [super init]) {
        _bizType = bizType;
    }
    return self;
}

/* Useless code has been omitted here */

@end

Since the DemoConfig class does not indicate whether the return value of the initialization method is optional, the API deduced by Xcode by default becomes.

// Automatically generated Swift API
open class DemoConfig : NSObject {
    /* Useless code has been omitted here */
    public init!(bizType: String!)
}

Developers have to think about how to solve the scenario with null initialization, which is obviously redundant.

In addition to optional semantic adaptation in the SDK, we can also add a new category to provide an OC method with a non empty return value. The code is as follows:

/// DemoConfig+SwiftyRateKit.h

NS_ASSUME_NONNULL_BEGIN

@interface DemoConfig (SwiftyRateKit)

- (instancetype)initWithType:(NSString *)bizType;

@end

NS_ASSUME_NONNULL_END

/// DemoConfig+SwiftyRateKit.m
#import <SwiftyRateKit/DemoConfig+SwiftyRateKit.h>

@implementation DemoConfig (SwiftyRateKit)

- (instancetype)initWithType:(NSString *)bizType {
    return [self initWithBizType:bizType];
}

@end

Unsafe API

It is inherently unsafe to bridge the OC API that does not clearly specify the optional settings to Swift. Why do you say that?

Let's take a real case of online Crash as an example. The stack is as follows:

Thread 0 Crashed:
0x0000000000000012 Swift runtime failure: Unexpectedly found nil while implicitly unwrapping an Optional value DemoEventHandler.swift
0x0000000000000011 handle DemoEventHandler.swift
0x0000000000000010 handle <compiler-generated>
0x0000000000000009 -[XXXXXXXXXX XXXXXXXXXX:XXXXXXXXXX:XXXXXXXXXX:] XXXXXXXXXX.m
0x0000000000000008 -[XXXXXXXX XXXXXXXX:XXXXXXXX:XXXXXXXX:] XXXXXXXX.m
0x0000000000000007 +[XXXXXXX XXXXXXX:XXXXXXX:XXXXXXX:] XXXXXXX.m
0x0000000000000006 -[XXXXXX XXXXXX:] XXXXXX.m
0x0000000000000005 -[XXXXX XXXXX:] XXXXX.m
0x0000000000000004 -[XXXX XXXX:] XXXX.m
0x0000000000000003 -[XXX XXX:XXX:] XXX.m
0x0000000000000002 -[XX XX:]
0x0000000000000001 -[X X:]

The implementation code of the client is as follows:

class DemoEventHandler: SwiftyEventHandler {

    override func handle(event: DemoEvent?, args: [Any], context: DemoContext?) {

        guard let ret = context?.demoCtx.engine.value else {
            return
        }
        ///Useless code is omitted here
    }
}

The cause of Crash is context demoCtx. engine. Value this code.

The essential reason is that demoCtx does not indicate optional semantics, resulting in the default use of implicit unpacking when OC bridges to Swift. During the reading process, if the value has no value, the Crash of Unexpectedly found nil while implicitly unwrapping an Optional value will be directly generated due to forced unpacking.

To solve this problem, in addition to the optional semantic adaptation of the SDK, we can also change the calling code into optional calls to avoid the problem of forced unpacking:

Destructive inheritance

The biggest problem encountered in using the above basic SDK is the destructive inheritance of DemoArray.

DemoArray inherits from NSArray and rewrites many methods, including objectAtIndex: this method.

It is clearly defined in the NSArray header file

objectAtIndex: the return value of this method must not be empty, but the SDK returns nil when implementing objectAtIndex: this method in the subclass DemoArray. The code is as follows:

This makes it impossible to customize EventHandler using Swift development SDK.

The core reason is that the implementation of an SDK custom EventHandler must first comply with the DemoEventHandler protocol, which must implement - (void)handleEvent:(DemoEvent *)event args:(NSArray *)args context:(DemoContext *)context; Since the NSArray type is agreed on the protocol, this method is converted to Swift API args and becomes the [Any] type, as shown in the following figure:

However, the type passed to DemoEventHandler by SDK is essentially a DemoArray type:

If there is a [null] object in DemoArray, it will cause a Crash of attempt to insert nil object from objects[0], as shown in the following figure:

The specific reason is that when calling handleevent (: args: Context:), Swift will call static Array Unconditionalybridgefromobjectivec (:) converts the args input parameter from NSArray to Swift Array. When calling the bridge function, the original Array will be copied first, and when NSArray Copy, NSPlaceholderArray initWithObjects:count:] will be called -[#u. Because the NSNull of DemoArray is converted to nil, the initialization will fail, Directly Crash.

To avoid this problem, it is obviously unrealistic for the SDK to modify DemoArray. Because there are too many callers, neither the impact surface nor the regression test cost can be evaluated in the short term. Therefore, we can only add an intermediate layer to solve this problem. We first designed an OC class called DemoEventHandlerBox for packaging and bridging. The code is as follows:

/// DemoEventHandlerBox.h

@class SwiftyEventHandler;

NS_ASSUME_NONNULL_BEGIN

@interface DemoEventHandlerBox : NSObject<DemoEventHandler>

-(instancetype)initWithHandler:(SwiftyEventHandler *)eventHandler;

@end

NS_ASSUME_NONNULL_END

/// DemoEventHandlerBox.m
#import <SwiftyRateKit/DemoEventHandlerBox.h>
#import <SwiftyRateKit/SwiftyRateKit-Swift.h>

@interface DXEventHandlerBox ()

///Handling event objects
@property (nonatomic, strong) SwiftyEventHandler *eventHandler;

@end

@implementation DemoEventHandlerBox


-(instancetype)initWithHandler:(SwiftyEventHandler *)eventHandler {

    self = [super init];

    if (self) {
        _eventHandler = eventHandler;
    }

    return self;
}

- (void)handleEvent:(DemoEvent *)event args:(NSArray *)args context:(DemoContext *)context {
    [self.eventHandler handle:event args:args context:context];
    return;
}

@end

There is a SwiftyEventHandler class of type in DemoEventHandlerBox for logical processing, and the code is as follows:

@objcMembers
public class SwiftyEventHandler: NSObject {

    @objc
    public final func handle(_ event: DemoEvent?, args: NSArray?, context: DemoContext?) {
        var ret: [Any] = []
        if let value = args as? DemoArray {
            ret = value.origin
        } else {
            ret = args as? [Any] ?? []
        }
        return handle(event: event, args: ret, context: context)

    }

    func handle(event: DemoEvent?, args: [Any], context: DemoContext?) {
        return
    }

}

The method exposed to OC by SwiftyEventHandler is set to final, and the logic of transferring DemoArray back to NSArray is compatible. Finally, all EventHandler implementation classes on the Swift side inherit from SwiftyEventHandler and override the handle(event:args:context) method. In this way, the problems caused by destructive inheritance can be perfectly avoided.

Clang Module build error

The second category of problems is mainly related to dependence. Although it was mentioned earlier that the current basic infrastructure is complete, there are still some problems.

Dependency update is not timely

When many people start writing Swift, they often encounter a problem, course not build Objective-C Module. Generally, the reason is that the Module you rely on is not adapted to the Module, but because the manual Amoy infrastructure is basically complete, most libraries have completed modular adaptation, So you may only need to update the Module dependency to solve this kind of problem.

For example, STD library, Amoy currently relies on version 1.6.3.2, but when your Swift module needs to rely on STD, using 1.6.3.2 will result in failure to compile. At this time, your Swift module may need to be upgraded to 1.6.3.3 to solve this problem. In essence, the difference between 1.6.3.3 and 1.6.3.2 is modular adaptation, so you don't have to worry about side effects.

Dependency problems caused by mixing

Although the Module adaptation mentioned above solves most of the problems, there are still some abnormal case s. Let's expand here.

In the process of commodity evaluation reconstruction, in order to ensure that the project can be scaled up step by step, we have made physical isolation of the code and created a new module called SwiftyRateKit, which is a Swift module. However, the entry class of the evaluation list is in an OC module called TBRatedisplay. Therefore, in order to cut flow, TBRatedisplay needs to rely on some implementations of SwiftyRateKit. However, when TBRatedisplay relies on SwiftyRateKit to start compiling, we encounter the following problem:

Xcode writes the header file declaration of the Swift class ExpandableFastTextViewWidgetNode exposed to OC to swiftyratekit Swift H, ExpandableFastTextViewWidgetNode inherits from TBDinamic class DXFastTextWidgetNode.

Because TBRatedisplay did not turn on the clang_enable_modules switch at that time, swiftyratekit swift The following macro definition of H does not take effect, so I don't know where the ExpandableFastTextViewWidgetNode class is defined:

But when we turned on the Clang Module switch of TBRatedisplay, something more terrible happened. TBDinamic failed to turn on the Clang Module switch, resulting in @import TBDinamic failed to compile and entered an "dead loop". Finally, it had to temporarily remove all OC module exports that did not support the Clang Module switch.

The concept here is abstract. I use a diagram to represent the dependency relationship:

First of all, for a Swift module, as long as the module turns on definitions_ If module = yes and Umbrella Header is provided, dependencies can be imported by importing TBDinamic. Therefore, SwiftyRateKit can display dependencies when TBDinamic does not turn on the Clang Module switch, and can be compiled.

However, for an OC module, there are two cases of importing another module.

  • The first is to enable definitions_ For modules with module = yes, we can use #import < tbdinamic / tbdinamic_ Umbrella. h> Import.
  • The second is when the Clang Module switch is turned on @import TBDinamic import

Because TBRatedisplay relies on SwiftyRateKit, Xcode automatically generates SwiftyRateKit swift H header file adopts @import TBDinamic is used to import modules, which causes the above problems.

Therefore, I personally suggest that at this stage, we should try to avoid or reduce providing the API of a swift module to OC, otherwise the OC modules that the swift external API needs to rely on need to open the Clang Module, and the OC modules that depend on the swift module also need to open the Clang Module. Moreover, due to the unequal syntax between swift and OC, the ability of the interface layer developed by swift will be very limited, resulting in the incompatibility of Swift's external APIs.

The class name has the same name as the Module

In theory, there is no problem for Swift modules to call each other. However, due to the large number of manual Amoy modules and the heavy historical burden, we encountered a hard problem of "the same name of the class name and the Module" when doing the transformation of commodity evaluation.

We have an SDK called STDPop. The Module name of this SDK is also called STDPop. At the same time, there is a tool class called STDPop. What problems does this lead to? If all Swift modules that depend on STDPop cannot be used by another Swift Module, a magical error will be reported: 'xxx' is not a member type of class' STDPop Stdpop 'is mainly generated by Swift Module that relies on STDPop When the swiftinterface file is created, each STDPop class will be prefixed with an STDPop. For example, PopManager becomes STDPop PopManager, however, since STDPop itself is a class called STDPop, the compiler cannot understand whether STDPop is a Module name or a class name.

The only way to solve this problem is to remove or modify the class name of STDPop.

What are the specific benefits?

After careful consideration, we embarked on the road of Swift landing. Although we encountered many unprecedented challenges in the whole process, now looking back, our original technology selection was relatively correct. It is mainly reflected in the following aspects:

Reduce the amount of code and improve the efficiency of Coding

Thanks to Swift's strong expressiveness, we can use less code to implement the logic originally implemented with OC, as shown in the figure below. We no longer need to write too much defensive programming code, so we can clearly express the logic we want to implement.

At the same time, we rewrite the original 13 expressions implemented with OC with Swift. The changes in the overall amount of code are as follows:

Less code means less time to develop and fewer opportunities for bug s.

Significantly reduce the cost of cross Review

OC's peculiar syntax makes most other developers unable to understand the specific logic at all, resulting in the high cost of iOS and Android dual terminal cross Review, which also makes many libraries often have dual terminal logic inconsistencies.

At the beginning of order migration, ENN was faced with many inconsistencies in the two-end API s, and the taste of some code logic was complex. There were too many temporary problem troubleshooting affecting the rhythm of the project.

Therefore, we find another way to develop with Swift & Kotlin's model. Due to the extremely similar syntax between Swift and Kotlin, we have no pressure on cross Review.

At the same time, thanks to the scaffolding used in commodity evaluation, the subsequent demand iterations also decreased significantly. Let's take "new sharing button for evaluation Item" as an example:

If you use OC & Java mode, you can't understand the double ended code. Therefore, one person needs to be sent from each side of the requirements review, and it takes about 0.5 person days to discuss and other matters. Then, after discussing the scheme at both ends, one person develops the template, which takes about 1 person day. Finally, it takes about 2 person days for the two terminals to realize the native sharing atomic capability respectively (one person day needs to investigate how to access the sharing SDK), with a total of 2 * 0.5 + 1 + 2 * 2 = 6 person days.

However, if Swift & kotlin mode is adopted, we only need one person to participate in the requirements Review, 0.5 person days. About 3 person days for technical research and template development. Finally, show the written code to the other end. The other end can directly copy the code and adapt it according to the characteristics of its own end for about 1 person day. Total 0.5 + 3 + 1 = about 4.5 person days. Save about 25% of your time.

The stability of the project has been improved

Because there is no better quantitative index, I can only talk about my feelings.

Firstly, the detection problems caused by coding problems are significantly reduced. Basically, the abnormal branch flows benefit from Swift's optional value design, which have been clearly considered in the development stage. The overall detection problems are significantly less than when using OC.

Secondly, online problems have also decreased significantly, except for the Crash problem mentioned above. There are basically no online problems in the commodity evaluation reconstruction project.

Give priority to technical dividends

Whether it's WidgetKit or DocC, it's obvious that Apple's internal upgrade of new features and development tool chain must be that Swift takes precedence over OC, so all students who use Swift can quickly use all Apple's newly developed features and tools.

At the same time, thanks to Swift's open source, we can not only learn some good design patterns through the source code, but also locate some difficult and miscellaneous diseases, so we no longer need to fight against the astringent assembly code.

Summary and Prospect

The above is a summary of our exploration of swift business landing in hand Amoy. I hope we can give you some help in technology selection or exploring pit avoidance. Of course, this is only the beginning, and there are still many things worth doing. First of all, we need to work together to improve and standardize Swift's coding specifications, and even precipitate a series of best practices to guide us to transform from OC to swift at a lower cost; Secondly, we also need to promote the construction of the Swift Layer of the basic SDK and continue to optimize the existing swift tool chain for the above-mentioned mixing problem; Finally, we also need to introduce some excellent open source libraries to avoid repeated wheel building, make good use of the capabilities provided by Apple (such as DocC), and finally find a best practice that Swift is scouring in hand.

Finally, if you are interested in what we do, welcome to join us to build Swift/Kotlin Ecology, my contact information is: Zhejian wzj@alibaba -Inc.com, looking forward to your joining.

[References]

Focus on Alibaba mobile technology WeChat official account, 3 mobile technology practices dry cargo per week to give you thought!

Topics: github xcode kotlin CLang