preface
Hi Coder, I’m CoderStar!
This week, I’m going to focus on three design patterns (command, mediator, and composite) and their application to the AppDelegate decoupling scenario, and in particular, composite patterns, which spawn their wheels for you to share.
I also want to tell you about the design pattern series of articles in the future, because the design pattern related articles will be organized according to the scenarios we will actually encounter in the development, so the posts may be not continuous, I hope you understand, I will organize most of the design pattern code examples into the DesignPatternsDemo repository. The form is Playground, so there may be some manual calls to system functions in the code examples.
I also recommend a good website for learning design patterns – In-depth Design Patterns, from which some of the UML diagrams covered in this article also come.
scenario
The AppDelegate is the root object, or unique proxy, of the application and can be considered the heart of every iOS project.
- It provides exposure to application life cycle events;
- It ensures that applications interact correctly with the system and other applications;
- They often take on a lot of responsibility, which makes it difficult to change, extend, and test.
As the business iterates, adding new features and businesses, the amount of code in the AppDelegate grows, resulting in Massive. Common businesses in an AppDelegate include:
- Event handling and propagation in the lifecycle;
- Manage the UI stack configuration: Select the initial view controller and perform the root view controller transformation;
- Manage background tasks;
- Management notice;
- Tripartite library initialization;
- Manage equipment direction;
- UIAppearance Settings;
- .
And because appDelegates affect the APP as a whole, we have to be careful with complex Appdelegates, lest our own changes affect other features. Simplicity and clarity of appdelegates are essential to a healthy iOS architecture.
Let’s use the above three design patterns to decouple the AppDelegate and make it elegant.
Command mode
Command is a behavior design pattern that transforms a request into a single object that contains all the information associated with the request. This transformation allows you to parameterize methods, delay request execution, or queue them based on different requests, and implement undoable operations.
UML
implementation
- Declare a single command interface that executes a method.
- Extract the request and make it into a concrete command class that implements the command interface. Each class must have a set of member variables that hold the request parameters and references to the actual recipient object. The values of all these variables must be initialized by the command constructor.
- Find the class that acts as the sender’s responsibility. Add member variables to these classes that hold the command. Senders can only interact with their commands through the command interface. Senders typically do not create command objects themselves, but rather get them through client code.
- Modify the sender to execute the command instead of sending the request directly to the receiver.
- Clients must initialize objects in the following order:
- Create the receiver.
- Create a command that can be associated to the recipient if needed.
- Create a sender and associate it with a specific command.
Code sample
import UIKit
// MARK: - Command interface
protocol AppDelegateDidFinishLaunchingCommand {
func execute(a)
}
// MARK: - Initializes the tripartite command
struct InitializeThirdPartiesCommand: AppDelegateDidFinishLaunchingCommand {
func execute(a) {
print("InitializeThirdPartiesCommand trigger")}}// MARK: - Initialize rootViewController
struct InitialViewControllerCommand: AppDelegateDidFinishLaunchingCommand {
let keyWindow: UIWindow
func execute(a) {
print("InitialViewControllerCommand trigger")
keyWindow.rootViewController = UIViewController()}}// MARK: - command constructor
final class AppDelegateCommandsBuilder {
private var window: UIWindow!
func setKeyWindow(_ window: UIWindow) -> AppDelegateCommandsBuilder {
self.window = window
return self
}
func build(a)- > [AppDelegateDidFinishLaunchingCommand] {
return [
InitializeThirdPartiesCommand(),
InitialViewControllerCommand(keyWindow: window),
]
}
}
// MARK: - AppDelegate
/// act as sender and client
class AppDelegate: UIResponder.UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication.didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(a)AppDelegateCommandsBuilder()
.setKeyWindow(window!)
.build()
.forEach { $0.execute() }
return true}}// MARK: - Manual call
AppDelegate().application(UIApplication.shared, didFinishLaunchingWithOptions: nil)
Copy the code
Actually the transform is not strictly follow the command mode, no receiver role, for example, the sender and the client did not actually completely separate, AppDelegateCommandsBuilder is actually a builder pattern at the same time, this model is also more commonly used, subsequent to this pattern separately. If you want to see a complete command pattern code example for a role, see the Command code example.
Using the Command pattern transformation AppDelegate later, when we need to add the processing logic in the callback, we don’t need to modify the AppDelegate, but directly increase the corresponding Command classes, and in AppDelegateCommandsBuilder added.
The disadvantages of this approach can be clearly seen. The code examples above only decouple the didFinishLaunch method and do not modify the other methods. If the other methods are modified, they also need to implement the above set, which is a bit redundant.
The mediator pattern
Mediator is a behavior design pattern that allows you to reduce messy dependencies between objects. This pattern restricts direct interaction between objects, forcing them to collaborate through a mediator object.
In fact, developers should be familiar with the mediator pattern, because in the MVC pattern, C is a typical mediator that limits the direct interaction between M and V.
UML
Code sample
import UIKit
// MARK: - Lifecycle event interface
protocol AppLifecycleListener {
func onAppWillEnterForeground(a)
func onAppDidEnterBackground(a)
func onAppDidFinishLaunching(a)
}
// MARK: - Interface default implementation, so that the implementation class can optionally implement methods
extension AppLifecycleListener {
func onAppWillEnterForeground(a) {}
func onAppDidEnterBackground(a) {}
func onAppDidFinishLaunching(a){}}// MARK: - Implementation class
class AppLifecycleListenerImp1: AppLifecycleListener {
func onAppDidEnterBackground(a){}}class AppLifecycleListenerImp2: AppLifecycleListener {
func onAppDidEnterBackground(a){}}// MARK: - Broker
class AppLifecycleMediator: NSObject {
private let listeners: [AppLifecycleListener]
init(listeners: [AppLifecycleListener]) {
self.listeners = listeners
super.init()
subscribe()
}
deinit {
NotificationCenter.default.removeObserver(self)}/// Subscribe to lifecycle events
private func subscribe(a) {
NotificationCenter.default.addObserver(self, selector: #selector(onAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onAppDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onAppDidFinishLaunching), name: UIApplication.didFinishLaunchingNotification, object: nil)}@objc private func onAppWillEnterForeground(a) {
listeners.forEach { $0.onAppWillEnterForeground() }
}
@objc private func onAppDidEnterBackground(a) {
listeners.forEach { $0.onAppDidEnterBackground() }
}
@objc private func onAppDidFinishLaunching(a) {
listeners.forEach { $0.onAppDidFinishLaunching() }
}
// MARK: - Add a new Listener
public static func makeDefaultMediator(a) -> AppLifecycleMediator {
let listener1 = AppLifecycleListenerImp1(a)let listener2 = AppLifecycleListenerImp2(a)return AppLifecycleMediator(listeners: [listener1, listener2])
}
}
class AppDelegate: UIResponder.UIApplicationDelegate {
var window: UIWindow?
/// Build listeners that automatically subscribe internally to lifecycle notifications
let mediator = AppLifecycleMediator.makeDefaultMediator()
func application(_ application: UIApplication.didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true}}Copy the code
As you can see above, AppLifecycleMediator is clearly an intermediary through which life cycle events can be propagated to specific users.
In fact, the mediator mode is also commonly used in componentized communication schemes. I will introduce it to you later. If you are interested, you can also learn about it yourself, which is also called CTMediator scheme.
Portfolio model
The composite pattern is a structural design pattern that allows you to combine objects into a tree structure and use them as if they were individual objects.
UML
In the AppDelegate scenario, AppDelegate is a root Composite role, and each business is Leaf role. If applied to componentization, each component is Leaf role or Composite role (component can be distributed to each business Leaf).
Code sample
/ / MARK: - interface, direct inheritance UIApplicationDelegate, UNUserNotificationCenterDelegate two protocols.
/// empty protocol, component modules to implement the protocol
public protocol ApplicationService: UIApplicationDelegate.UNUserNotificationCenterDelegate {}
/// get the window from the component
extension ApplicationService {
/// window
public var window: UIWindow? {
// swiftlint:disable:next redundant_nil_coalescing
return UIApplication.shared.delegate?.window ?? nil}}// MARK: - AppDelegate inheritance
open class ApplicationServiceManagerDelegate: UIResponder.UIApplicationDelegate {
/// subclasses need to be assigned in the constructor
public var window: UIWindow?
/// return the plist file address containing the name of the class that implements ApplicationService for each module
/// the plist file must be of type NSArray
open var plistPath: String? { return nil }
/// subclasses are overridden to return the classes that each module implements ApplicationService
open var services: [ApplicationService] {
guard let path = plistPath else {
return[]}guard let applicationServiceNameArr = NSArray(contentsOfFile: path) else {
return[]}var applicationServiceArr = [ApplicationService] ("ApplicationService")
for applicationServiceName in applicationServiceNameArr {
if let applicationServiceNameStr = applicationServiceName as? String.let applicationService = NSClassFromString(applicationServiceNameStr), let module = applicationService as? NSObject.Type {
let service = module.init(a)if let result = service as? ApplicationService {
applicationServiceArr.append(result)
}
}
}
return applicationServiceArr
}
public func getService(by type: ApplicationService.Type) -> ApplicationService? {
for service in applicationServices where service.isMember(of: type) {
return service
}
return nil
}
/// lazy load gets the compute property services, so that it is computed only once
private lazy var applicationServices: [ApplicationService] = {
self.services
}()
}
// MARK: - Protocol default implementation, to distribute events to each Leaf
extension ApplicationServiceManagerDelegate {
@available(iOS 3.0.*)
open func application(_ application: UIApplication.didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
var result = false
for service in applicationServices {
if service.application?(application, didFinishLaunchingWithOptions: launchOptions) ?? false {
result = true}}return result
}
/** Implements protocol methods one by one and distributes events to each Leaf */
}
// MARK: - Usage
final class AppThemeApplicationService: NSObject.ApplicationService {
func application(_ application: UIApplication.didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
/// setup AppTheme
return true}}final class AppConfigApplicationService: NSObject.ApplicationService {
func application(_ application: UIApplication.didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
/// setup AppConfig
return true}}@UIApplicationMain
class AppDelegate: ApplicationServiceManagerDelegate {
override var services: [ApplicationService] {
return [
AppConfigApplicationService(),
AppThemeApplicationService(),]}override init(a) {
super.init(a)if window = = nil {
window = UIWindow()}}}Copy the code
As you can see from the code examples above, each Leaf implements the ApplicationService protocol and gets all the callbacks that an AppDelegate would otherwise get.
As for the AppDelegate, there will be no internal business logic, and because of the default implementation of the protocol, tasks have been distributed to each Leaf by default, and the remaining tasks are only to provide the list of Leaves, and considering the use in the componentized environment, do not directly reference each Leaf. Provides the form of plist configuration files.
The set of decoupling scheme is perfect, the wheels of the deposited address for ApplicationServiceManager. The function is relatively lightweight, welcome to use.
There’s also Ali’s BeeHive for decoupling appDelegates, but it’s a comprehensive componentalization solution, and the distribution of AppDelegate events is only a part of it.
The last
The above three design modes can be selected or combined according to the actual situation of their respective projects. For example, shell engineering can choose the combined mode to distribute events inside each component, and the command or intermediary mode can be used to distribute events inside the component.
Try harder!
Let’s be CoderStar!
The resources
- Refactoring Massive App Delegate