Continue where we left off: Building a complete iOS componentization solution: How to decouple modules for interfaces? (a)

Function extension

Having concluded our approach to module decoupling and dependency management using interfaces, we can extend the Router further. Creating a module using makeDestination is the most basic function above, but there are many useful extensions that can be made using the Router subclass, as demonstrated here.

Automatic registration

When writing router code, you need to register the Router and protocol. It is possible to register in the +load method in OC, but Swift is no longer able to use the +load method, and the registration code scattered in +load is also difficult to manage. BeeHive adds registration information to a custom zone in Mach-O using macro definitions and __attribute((used, section(“__DATA,””BeehiveServices”””)), which is then read at startup and automatically registered. Unfortunately, this doesn’t work with Swift either.

We can write the registration code in the router + registerRoutableDestination method, and then call each router class + registerRoutableDestination method one by one. Still can go a step further, with the runtime technology of traverse the Mach – O __DATA, __objc_classlist area list of classes, all access to the router class, automatic call all + registerRoutableDestination method.

If you do not want to use automatic registration after unified management of registration codes, you can switch to manual registration at any time.

// Router class of the Editor module EditorViewRouter: ZIKViewRouter {override class funcregisterRoutableDestination() {
        registerView(EditorViewController.self)
        register(RoutableView<EditorViewProtocol>())
    }

}

Copy the code

<details><summary>Objective-C Sample</summary>

@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

+ (void)registerRoutableDestination {
    [self registerView:[EditorViewController class]];
    [self registerViewProtocol:ZIKRoutable(EditorViewProtocol)];
}

@end

Copy the code

</details>

Packaging interface jump

One of the reasons for the coupling between modules in iOS is that the interface jump logic is carried out through UIViewController, and the jump function is limited to view Controller, resulting in the data flow is often unable to bypass the View layer. To better manage jump logic, encapsulation is required.

Encapsulating jumps can mask UIKit details so that jump code can reside in non-View layers (such as Presenter, View Model, Interactor, service) and can be cross-platform and easily configured to switch jumps.

For normal modules, use ZIKServiceRouter, and for interface modules, such as UIViewController and UIView, use ZIKViewRouter, which encapsulates the interface jump function.

After the encapsulation interface jumps, the usage mode is as follows:

Class TestViewController: UIViewController {// Jump directly to editor interface funcshowEditor() { Router.perform(to: RoutableView<EditorViewProtocol>(), path: .push(from: Self))} // Go to the editor interface and use protocol to configure interface funcprepareAndShowEditor() {
        Router.perform(
            to: RoutableView<EditorViewProtocol>(),
            path: .push(from: self),
            preparation: { destination in// Destination is automatically inferred as EditorViewProtocol})}}Copy the code

<details><summary>Objective-C Sample</summary>

@implementation TestViewController - (void)showEditor { performPath:ZIKViewRoutePath.pushFrom(self)]; } - (void)prepareAndShowEditor {// Jump to the editor interface, Before the jump with protocol configuration interface [ZIKRouterToView (EditorViewProtocol) performPath: ZIKViewRoutePath. PushFrom (self) Preparation :^(id<EditorViewProtocol> destination) {EditorViewProtocol}]; } @endCopy the code

</details>

You can use ViewRoutePath to toggle different jumps in one click:

enum ViewRoutePath {
    case push(from: UIViewController)
    case presentModally(from: UIViewController)
    case presentAsPopover(from: UIViewController, configure: ZIKViewRoutePopoverConfigure)
    case performSegue(from: UIViewController, identifier: String, sender: Any?)
    case show(from: UIViewController)
    case showDetail(from: UIViewController)
    case addAsChildViewController(from: UIViewController, addingChildViewHandler: (UIViewController, @escaping () -> Void) -> Void)
    case addAsSubview(from: UIView)
    case custom(from: ZIKViewRouteSource?)
    case makeDestination
    case extensible(path: ZIKViewRoutePath)
}

Copy the code

In addition, after the interface hops, the interface can be backstrokeback according to the jump mode, without manual separation of dismiss, POP and other situations:

class TestViewController: UIViewController {
    var router: DestinationViewRouter<EditorViewProtocol>?

    func showEditorPerform (to: RoutableView<EditorViewProtocol>(), path:.push(from: RoutableView<EditorViewProtocol>) Self))} // The Router pops the editor View Controller to remove interface funcremoveEditor() {
        guard let router = router, router.canRemove else {
            return
        }
        router.removeRoute()
        router = nil
    }
}

Copy the code

<details><summary>Objective-C Sample</summary>

@interface TestViewController() @property (nonatomic, strong) ZIKDestinationViewRouter(id<EditorViewProtocol>) *router; @end@implementation TestViewController - (void)showEditor {// Hold router self.router = [ZIKRouterToView(EditorViewProtocol) performPath:ZIKViewRoutePath.pushFrom(self)]; } // The Router performs a pop operation on the Editor View controller to remove the interface - (void)removeEditor {if(! [self.router canRemove]) {return;
    }
    [self.router removeRoute];
    self.router = nil;
}

@end

Copy the code

</details>

Custom jump

Some interfaces have special ways of jumping, such as the tabbar interface, which requires toggling tabbar Items. In some interfaces, there is a custom jump animation. In this case, you can rewrite the corresponding method in the Router subclass to customize the jump.

class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {

    override func destination(with configuration: ViewRouteConfig) -> Any? {
        return EditorViewController()
    }

    override func canPerformCustomRoute() -> Bool {
        return true
    }
    
    override func performCustomRoute(onDestination destination: EditorViewController, fromSource source: Any? , the configuration: ViewRouteConfig) {beginPerformRoute () / / custom jump CustomAnimator transition (from:source, to: destination) {
            self.endPerformRouteWithSuccess()
        }
    }
    
    override func canRemoveCustomRoute() -> Bool {
        return true
    }
    
    override func removeCustomRoute(onDestination destination: EditorViewController, fromSource source: Any? , removeConfiguration: ViewRemoveConfig, configuration: ViewRouteConfig) { beginRemoveRoute(fromSource:source) / / remove custom jump CustomAnimator. Dismiss (destination) {self. EndRemoveRouteWithSuccess (onDestination: destination, fromSource:source)
        }
    }
    
    override class func supportedRouteTypes() -> ZIKViewRouteTypeMask {
        return [.custom, .viewControllerDefault]
    }
}

Copy the code

<details><summary>Objective-C Sample</summary>

@interface EditorViewRouter : ZIKViewRouter
@end

@implementation EditorViewRouter

- (EditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
    return [[EditorViewController alloc] init];
}

- (BOOL)canPerformCustomRoute {
    return YES;
}

- (void)performCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)sourceconfiguration:(ZIKViewRouteConfiguration *)configuration { [self beginPerformRoute]; // Custom jump [CustomAnimator transitionFrom:source to:destination completion:^{
        [self endPerformRouteWithSuccess];
    }];
}

- (BOOL)canRemoveCustomRoute {
    return YES;
}

- (void)removeCustomRouteOnDestination:(id)destination fromSource:(UIViewController *)source removeConfiguration:(ZIKViewRemoveConfiguration *)removeConfiguration configuration:(__kindof ZIKViewRouteConfiguration *)configuration {
    [self beginRemoveRouteFromSource:source]; / / remove custom jump [CustomAnimator dismiss: destination completion: ^ {[self endRemoveRouteWithSuccessOnDestination: destination fromSource:source];
    }];
}

+ (ZIKViewRouteTypeMask)supportedRouteTypes {
    return ZIKViewRouteTypeMaskCustom|ZIKViewRouteTypeMaskViewControllerDefault;
}

@end

Copy the code

</details>

Support the storyboard

Many projects use storyboards, and when modularizing, you certainly can’t require all modules that use storyboards to use code instead. So we can hook a few storyboard related methods, for example – prepareSegue: sender: to invoke prepareDestination: : you can.

URL routing

Although many disadvantages of URL routing have been listed before, URL routing is the best solution if your module needs to be called from an H5 interface, such as an e-commerce app that needs to implement cross-platform dynamic routing rules.

But we don’t want to repackage the module using another framework to implement URL routing. You only need to extend the URL routing function on the router to use the interface and URL management module at the same time.

You can register the URL to the router:

class EditorViewRouter: ZIKViewRouter<EditorViewProtocol, ViewRouteConfig> {
    override class func registerRoutableDestination() {// register url registerURLPattern()"app://editor/:title")}}Copy the code

<details><summary>Objective-C Sample</summary>

@ implementation EditorViewRouter + (void) registerRoutableDestination {/ / registered url [self registerURLPattern: @"app://editor/:title"];
}

@end

Copy the code

</details>

The router can then be obtained with the corresponding URL:

ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))

Copy the code

<details><summary>Objective-C Sample</summary>

[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];

Copy the code

</details>

And handle URL Scheme:

public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
    let urlString = url.absoluteString
    if let _ = ZIKAnyViewRouter.performURL(urlString, fromSource: self.rootViewController) {
        return true
    } else if let _ = ZIKAnyServiceRouter.performURL(urlString) {
        return true
    }
    return false
}

Copy the code

<details><summary>Objective-C Sample</summary>

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
        return YES;
    } else if ([ZIKAnyServiceRouter performURL:urlString]) {
        return YES;
    }
    return NO;
}

Copy the code

</details>

Each Router subclass can also perform further processing on the URL, such as processing parameters in the URL, executing corresponding methods through the URL, and sending return values to the caller after executing the route.

Each project has different requirements for URL routing. Based on ZIKRouter’s powerful scalability, you can also implement your own URL routing rules according to project requirements.

Replace router subclasses with router objects

In addition to creating router subclasses, you can also use generic Router instance objects that provide the same functionality as router subclasses in the block attribute of each object, so you don’t have to worry about having too many classes. It works the same way as replacing a subclass of Configuration with a generic Configuration.

The ZIKViewRoute object subclass overrides the block property, and the code can be chained:

ZIKViewRoute<EditorViewController, ViewRouteConfig>
.make(withDestination: EditorViewController.self, makeDestination: ({ (config, router) -> EditorViewController? in
    return EditorViewController()
}))
.prepareDestination({ (destination, config, router) in

}).didFinishPrepareDestination({ (destination, config, router) in

})
.register(RoutableView<EditorViewProtocol>())

Copy the code

<details><summary>Objective-C Sample</summary>

[ZIKDestinationViewRoute(id<EditorViewProtocol>) 
 makeRouteWithDestination:[ZIKInfoViewController class] 
 makeDestination:^id<EditorViewProtocol> _Nullable(ZIKViewRouteConfig *config, ZIKRouter *router) {
    return [[EditorViewController alloc] init];
}]
.prepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {

})
.didFinishPrepareDestination(^(id<EditorViewProtocol> destination, ZIKViewRouteConfig *config, ZIKViewRouter *router) {

})
.registerDestinationProtocol(ZIKRoutable(EditorViewProtocol));

Copy the code

</details>

Simplified Router Implementation

Router implementation based on ZIKViewRoute object can further simplify the router implementation code.

If your class is simple and does not need the Router subclass, just register the class with one line of code:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), forMakingView: EditorViewController.self)

Copy the code

<details><summary>Objective-C Sample</summary>

[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewProtocol) forMakingView:[EditorViewController class]];

Copy the code

</details>

Or use blocks to customize the way objects are created:

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), 
                 forMakingView: EditorViewController.self) { (config, router) -> EditorViewProtocol? in
                     return EditorViewController()
        }


Copy the code

<details><summary>Objective-C Sample</summary>

[ZIKViewRouter
    registerViewProtocol:ZIKRoutable(EditorViewProtocol)
    forMakingView:[EditorViewController class]
    making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
        return [[EditorViewController alloc] init];
 }];

Copy the code

</details>

Or specify a C function to create an object:

function makeEditorViewController(config: ViewRouteConfig) -> EditorViewController? {
    return EditorViewController()
}

ZIKAnyViewRouter.register(RoutableView<EditorViewProtocol>(), 
                 forMakingView: EditorViewController.self, making: makeEditorViewController)

Copy the code

<details><summary>Objective-C Sample</summary>

id<EditorViewController> makeEditorViewController(ZIKViewRouteConfiguration *config) {
    return [[EditorViewController alloc] init];
}

[ZIKViewRouter
    registerViewProtocol:ZIKRoutable(EditorViewProtocol)
    forMakingView:[EditorViewController class]
    factory:makeEditorViewController];

Copy the code

</details>

The event processing

Sometimes the module needs to handle system events or app custom events. In this case, the router subclass can implement it and then iterate the distribution.

class SomeServiceRouter: ZIKServiceRouter {
    @objc class func applicationDidEnterBackground(_ application: UIApplication) {
        // handle applicationDidEnterBackground event
    }
}

Copy the code
class AppDelegate: NSObject, NSApplicationDelegate {

    func applicationDidEnterBackground(_ application: UIApplication) {
        
        Router.enumerateAllViewRouters { (routerType) in
            if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
                routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)
            }
        }
        Router.enumerateAllServiceRouters { (routerType) in
            if routerType.responds(to: #selector(applicationDidEnterBackground(_:))) {
                routerType.perform(#selector(applicationDidEnterBackground(_:)), with: application)}}}}Copy the code

<details><summary>Objective-C Sample</summary>

@interface SomeServiceRouter : ZIKServiceRouter
@end
@implementation SomeServiceRouter

+ (void)applicationDidEnterBackground:(UIApplication *)application {
    // handle applicationDidEnterBackground event
}

@end

Copy the code
@interface AppDelegate ()
@end
@implementation AppDelegate

- (void)applicationDidEnterBackground:(UIApplication *)application {
    
    [ZIKAnyViewRouter enumerateAllViewRouters:^(Class routerClass) {
        if([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) { [routerClass applicationDidEnterBackground:application]; }}]; [ZIKAnyServiceRouter enumerateAllServiceRouters:^(Class routerClass) {if([routerClass respondsToSelector:@selector(applicationDidEnterBackground:)]) { [routerClass applicationDidEnterBackground:application]; }}]; } @endCopy the code

</details>

Unit testing

With a scheme that uses interfaces to manage dependencies, we can freely configure mock dependencies when unit testing our modules without the need for code inside the hook modules.

For example, a login module that relies on a network module:

Class LoginService {func login(Account: String, password: String, completion: (Result<LoginError>) -> Void) {// Use RequiredNetServiceInput internally for network accesslet netService = Router.makeDestination(to: RoutableService<RequiredNetServiceInput
        >())
        letrequest = makeLoginRequest(account: account, password: password) netService? .post (request: request, completion: completion)}} // Declare dependency on Extension RoutableServicewhere Protocol == RequiredNetServiceInput {
    init() {}}Copy the code

<details><summary>Objective-C Sample</summary>

// @interface LoginService: NSObject @end @implementation LoginService - (void)loginWithAccount:(NSString *)account password:(NSString *)password Completion :(void(^)(Result * Result))completion {// use RequiredNetServiceInput internally to access the network. netService = [ZIKRouterToService(RequiredNetServiceInput) makeDestination]; Request *request = makeLoginRequest(account, password); [netService POSTRequest:request completion: completion]; } @end // declaration depends on @protocol RequiredNetServiceInput <ZIKServiceRoutable> - (void)POSTRequest:(Request *) Request completion:(void(^)(Result *result))completion; @endCopy the code

</details>

Instead of introducing a real network module when writing unit tests, you can provide a custom mock network module:

class MockNetService: RequiredNetServiceInput {
    func POST(request: Request, completion: (Result<NetError>) {
        completion(.success)
    }
}

Copy the code
/ / registered mock rely on ZIKAnyServiceRouter. Register (RoutableService < RequiredNetServiceInput > (),forMakingService: MockNetService.self) { (config, router) -> EditorViewProtocol? in
                     return MockNetService()
        }

Copy the code

<details><summary>Objective-C Sample</summary>

@interface MockNetService : NSObject <RequiredNetServiceInput>
@end
@implementation MockNetService

- (void)POSTRequest:(Request *)request completion:(void(^)(Result *result))completion {
    completion([Result success]);
}
  
@end

Copy the code
/ / registered mock rely on [ZIKServiceRouter registerServiceProtocol: ZIKRoutable (EditorViewInput)forMakingService:[MockNetService class]];

Copy the code

</details>

For external dependencies without interface interaction, such as simply jumping to the corresponding interface, you only need to register a blank proxy.

Unit test code:

class LoginServiceTests: XCTestCase {
    
    func testLoginSuccess() {
        let expectation = expectation(description: "end login")
        
        let loginService = LoginService()
        loginService.login(account: "account", password: "pwd") { result in
            expectation.fulfill()
        }
        
        waitForExpectations(timeout: 5, handler: { if let error = $0 {print(error)}})
    }
    
}

Copy the code

<details><summary>Objective-C Sample</summary>

@interface LoginServiceTests : XCTestCase
@end
@implementation LoginServiceTests

- (void)testLoginSuccess {
    XCTestExpectation *expectation = [self expectationWithDescription:@"end login"];
    
    [[LoginService new] loginWithAccount:@"" password:@"" completion:^(Result *result) {
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:5 handler:^(NSError * _Nullable error) { ! error? : NSLog(@"% @", error);
    }];
}
@end

Copy the code

</details>

Using interfaces to manage dependencies makes it easier to mock out external dependencies and make unit tests more stable.

Interface Version Management

There is another issue to be aware of when using an interface management module. The interface will change with module update. This interface has been used by many external users. How to reduce the impact of interface change?

In this case, you need to distinguish the new interface from the old interface and distinguish the version. When promoting the new interface, keep the old interface and mark the old interface as obsolete. This allows the consumer to temporarily use the old interface and change the code incrementally.

Refer to the version management macros in Swift and OC for this section.

The interface is obsolete and can be used temporarily. You are advised to replace it with a new interface as soon as possible:

// Swift
@available(iOS, deprecated: 8.0, message: "Use new interface instead")

Copy the code
// Objective-C
API_DEPRECATED_WITH_REPLACEMENT("performPath:configuring:"The ios (7.0, 7.0));Copy the code

The interface is invalid:

// Swift
@available(iOS, unavailable)

Copy the code
// Objective-C
NS_UNAVAILABLE

Copy the code

Final shape

Finally, a router looks like this:

// The editor module's router class EditorViewRouter: ZIKViewRouter<EditorViewController, ViewRouteConfig> {Override class funcregisterRoutableDestination() {
        registerView(EditorViewController.self)
        register(RoutableView<EditorViewProtocol>())
        registerURLPattern("app://editor/:title")
    }

    override func processUserInfo(_ userInfo: [AnyHashable : Any] = [:], from url: URL) {
        let title = userInfo["title"Override func destination(with configuration: ViewRouteConfig) -> Any? {let destination = EditorViewController()
        returnFunc prepareDestination(_ Destination: EditorViewController, Configuration: ViewRouteConfig) {/ / injection service depend on the destination. The storageService. = the Router makeDestination (to: RoutableService < EditorStorageServiceInput > ()) from the url / / / / other configuration parametersif let title = configuration.userInfo["title"] as? String {
            destination.title = title
        } else {
            destination.title = "Default title"}} / / event handling @ objc class func applicationDidEnterBackground (_ application: UIApplication) { // handle applicationDidEnterBackground event } }Copy the code

<details><summary>Objective-C Sample</summary>

// Router @Interface EditorViewRouter of the editor module: ZIKViewRouter @end @implementation EditorViewRouter + (void)registerRoutableDestination { [self registerView:[EditorViewController class]]; [self registerViewProtocol:ZIKRoutable(EditorViewProtocol)]; [self registerURLPattern:@"app://editor/:title"];
}

- (void)processUserInfo:(NSDictionary *)userInfo fromURL:(NSURL *)url {
    NSString *title = userInfo[@"title"]; } // subclass override, Create a module - (EditorViewController *) destinationWithConfiguration: (ZIKViewRouteConfiguration *) configuration { EditorViewController *destination = [[EditorViewController alloc] init];returndestination; } // Configure the module, Static dependence - (void) injection prepareDestination: (EditorViewController *) destination configuration: (ZIKViewRouteConfiguration *) configuration {/ / injection service depend on the destination. StorageService = [ZIKRouterToService (EditorStorageServiceInput) makeDestination]; // Other configurations // Handle the parameter NSString *title = configuration.userinfo [@ from url"title"];
    if (title) {
        destination.title = title;
    } else {
        destination.title = @"Default title"; }} / / event handling + (void) applicationDidEnterBackground: (application UIApplication *) {/ / handle applicationDidEnterBackground  event } @endCopy the code

</details>

Advantages of interface-based decoupling

We can see the advantages of an interface based management module:

  • Rely on compile checking for strict type safety
  • Rely on compile checks to reduce the cost of refactoring
  • The dependencies required by the module are explicitly declared through an interface, allowing external dependency injection
  • While maintaining the dynamic feature, check routes to avoid using non-existent routing modules
  • Interfaces are used to distinguish required Protocol from Provided Protocol for explicit module adaptation and complete decoupling

Looking back at the previous eight decoupling metrics, the ZIKRouter meets them perfectly. Router provides multiple module management methods (makeDestination, prepareDestination, dependency injection, page jump, storyboard support) that can override most existing scenarios, enabling progressive modularity and reducing the cost of refactoring existing code.

Book catalog – get the address plus xiaobian wechat to pull you into the iOS development group: 17512010526



Author: Black Super Panda Zuik

Source: www.jianshu.com/p/3aab83626…