Home Practical Application Record of Design Patterns—In WKWebView with Builder, Strategy & Chain of Responsibility Pattern
Post
Cancel

Practical Application Record of Design Patterns—In WKWebView with Builder, Strategy & Chain of Responsibility Pattern

Practical Application Record of Design Patterns—In WKWebView with Builder, Strategy & Chain of Responsibility Pattern

Scenarios of using Design Patterns (Strategy, Chain of Responsibility, Builder Pattern) when encapsulating iOS WKWebView.

Photo by [Dean Pugh](https://unsplash.com/@wezlar11?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash){:target="_blank"}

Photo by Dean Pugh

About Design Patterns

Before discussing Design Patterns, it is worth mentioning that the most classic GoF 23 design patterns were published 30 years ago (in 1994). With changes in tools, languages, and software development patterns, many new design patterns have emerged in various fields. Design Patterns are not a universal solution or the only solution. Their existence is more like a “linguistic term” where the appropriate design pattern is applied in suitable scenarios, reducing obstacles in development collaboration. For example, applying the Strategy pattern here allows future maintainers to iterate directly according to the structure of the Strategy pattern, and design patterns mostly decouple well, providing significant assistance in scalability and testability.

Guidelines for Using Design Patterns

  • Not the only solution
  • Not a universal solution
  • Avoid forcing patterns; choose the appropriate design pattern based on the type of problem to be solved (creation? behavior? structure?) and the purpose
  • Avoid arbitrary modifications, as this can lead to misunderstandings by future maintainers. Just like how everyone calls Apple “Apple,” if you define it as “Banana,” it becomes an additional development cost that needs special knowledge
  • Avoid using keywords unnecessarily; for example, if the Factory Pattern is conventionally named XXXFactory, it should not be used unless it is a factory pattern
  • Be cautious about creating new patterns. Although there are only 23 classic patterns, the evolution in various fields over the years has introduced many new patterns. It is advisable to first refer to online resources to find suitable patterns (after all, three mediocre craftsmen surpass one Zhuge Liang). If no suitable pattern is found, propose a new design pattern and publish it for review and adjustment by people in different fields and contexts
  • Ultimately, code is written for human maintenance. As long as it is easy to maintain and extend, design patterns are not always necessary
  • Team consensus on Design Patterns is essential for their effective use
  • Design Patterns can be combined with other Design Patterns
  • Practical experience is crucial for mastering Design Patterns and understanding when to apply them appropriately

Auxiliary Tool ChatGPT

With ChatGPT, learning the practical application of Design Patterns has become easier. Just provide a detailed description of your problem, ask which design patterns are suitable for the scenario, and it can suggest several potentially suitable patterns with explanations. While not every answer may be perfect, it provides viable directions. By delving into these patterns and combining them with your practical scenarios, you can ultimately choose a good solution!

Practical Application Scenarios of Design Patterns in WKWebView

This Design Patterns practical application is to converge the functionality of the WKWebView object in the current Codebase and develop a unified WKWebView component. The experience of applying Design Patterns at appropriate logical abstraction points when developing the WKWebView component is shared.

The complete demo project code will be attached at the end of the document.

Original Unabstracted Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
class WKWebViewController: UIViewController {

    // MARK - Define some variables and switches for injecting features during external initialization...

    // Simulate business logic: Switch to match special paths to open native pages
    let noNeedNativePresent: Bool
    // Simulate business logic: Switch for DeeplinkManager check
    let deeplinkCheck: Bool
    // Simulate business logic: Is it the homepage?
    let isHomePage: Bool
    // Simulate business logic: Scripts to inject into WKWebView as WKUserScript
    let userScripts: [WKUserScript]
    // Simulate business logic: Scripts to inject into WKWebView as WKScriptMessageHandler
    let scriptMessageHandlers: [String: WKScriptMessageHandler]
    // Override ViewController Title with Title obtained from WebView
    let overrideTitleFromWebView: Bool
    
    let url: URL
    
    // ... 
}
// ...
extension OldWKWebViewController: WKNavigationDelegate {
    // MARK - iOS WKWebView's navigationAction Delegate, used to determine how to handle the upcoming link
    // Must call decisionHandler(.allow) or decisionHandler(.cancel) at the end
    // decisionHandler(.cancel) will interrupt loading the upcoming page

    // Different variables and switches have different logic processing here:

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }
        
        // Simulate business logic: WebViewController deeplinkCheck == true (indicating the need to check with DeepLinkManager and open the page)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // Simulate DeepLinkManager logic, open the URL if successful and end the process.
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // Simulate business logic: WebViewController isHomePage == true (indicating the homepage) & WebView is browsing the homepage, switch TabBar Index
        if isHomePage {
            if url.absoluteString == "https://zhgchg.li" {
                print("Switch UITabBarController to Index 0")
                decisionHandler(.cancel)
            }
        }
        
        // Simulate business logic: WebViewController noNeedNativePresent == false (indicating the need to match special paths to open native pages)
        if !noNeedNativePresent {
            if url.pathComponents.count >= 3 {
                if url.pathComponents[1] == "product" {
                    // match http://zhgchg.li/product/1234
                    let id = url.pathComponents[2]
                    print("Present ProductViewController(\(id)")
                    decisionHandler(.cancel)
                } else if url.pathComponents[1] == "shop" {
                    // match http://zhgchg.li/shop/1234
                    let id = url.pathComponents[2]
                    print("Present ShopViewController(\(id)")
                    decisionHandler(.cancel)
                }
                // more...
            }
        }
        
        decisionHandler(.allow)
    }
}
// ...

Issues

  1. Setting variables and switches in the Class makes it unclear which ones are for configuration.
  2. Exposing WKUserScript variables directly to the outside, we want to control the injected JS and only allow injection of specific behaviors.
  3. Unable to control the registration rules of WKScriptMessageHandler.
  4. If you need to initialize a similar WebView, you need to repeatedly write the injection parameter rules, and the parameter rules cannot be reused.
  5. The navigationAction Delegate controls the flow internally based on variables. If you need to delete or modify the flow or sequence, you have to modify the entire code, which may disrupt the originally normal flow.

Builder Pattern

The Builder Pattern is a creational design pattern that separates the construction steps and logic of creating an object. The operator can set parameters step by step and reuse the settings, and finally create the target object. Additionally, the same construction steps can create different object implementations.

Using the example of making a Pizza in the image above, the steps of making a Pizza are broken down into several methods and declared in the PizzaBuilder protocol (Interface). ConcretePizzaBuilder is the actual object that makes the Pizza, which could be VegetarianPizzaBuilder & MeatPizzaBuilder; different builders may have different ingredients, but they all ultimately build() to produce a Pizza object.

WKWebView Scenario

In the WKWebView scenario, our final output object is MyWKWebViewConfiguration. We consolidate all the variables that WKWebView needs to set into this object and use the Builder Pattern MyWKWebViewConfigurator to gradually complete the construction of the Configuration.

1
2
3
4
5
6
7
8
public struct MyWKWebViewConfiguration {
    let headNavigationHandler: NavigationActionHandler?
    let scriptMessageStrategies: [ScriptMessageStrategy]
    let userScripts: [WKUserScript]
    let overrideTitleFromWebView: Bool
    let url: URL
}
// All parameters are only exposed internally within the module

MyWKWebViewConfigurator (Builder Pattern)

Since I only have the need to Build for MyWKWebView here, I did not further break down MyWKWebViewConfigurator into multiple Protocols (Interfaces).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public final class MyWKWebViewConfigurator {
    
    private var headNavigationHandler: NavigationActionHandler? = nil
    private var overrideTitleFromWebView: Bool = true
    private var disableZoom: Bool = false
    private var scriptMessageStrategies: [ScriptMessageStrategy] = []
    
    public init() {
        
    }
    
    // Encapsulate parameters, internal control
    public func set(disableZoom: Bool) -> Self {
        self.disableZoom = disableZoom
        return self
    }
    
    public func set(overrideTitleFromWebView: Bool) -> Self {
        self.overrideTitleFromWebView = overrideTitleFromWebView
        return self
    }
    
    public func set(headNavigationHandler: NavigationActionHandler) -> Self {
        self.headNavigationHandler = headNavigationHandler
        return self
    }
    
    // Can encapsulate additional logic rules inside
    public func add(scriptMessageStrategy: ScriptMessageStrategy) -> Self {
        scriptMessageStrategies.removeAll(where: { type(of: $0).identifier == type(of: scriptMessageStrategy).identifier })
        scriptMessageStrategies.append(scriptMessageStrategy)
        return self
    }
    
    public func build(url: URL) -> MyWKWebViewConfiguration {
        var userScripts:[WKUserScript] = []
        // Attach only when generating
        if disableZoom {
            let script = "var meta = document.createElement('meta'); meta.name='viewport'; meta.content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'; document.getElementsByTagName('head')[0].appendChild(meta);"
            let disableZoomScript = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
            userScripts.append(disableZoomScript)
        }
        
        return MyWKWebViewConfiguration(headNavigationHandler: headNavigationHandler, scriptMessageStrategies: scriptMessageStrategies, userScripts: userScripts, overrideTitleFromWebView: overrideTitleFromWebView, url: url)
    }
}

Adding an extra layer can also better control the usage permissions of Access Control for isolating parameters. In this scenario, we still want to be able to directly inject WKUserScript into MyWKWebView, but we don’t want to leave the door wide open for users to inject at will. Therefore, combining the Builder Pattern with Swift Access Control, after MyWKWebView has been placed in a Module, MyWKWebViewConfigurator encapsulates externally as an operation method func set(disableZoom: Bool), internally generating MyWKWebViewConfiguration with attached WKUserScript. All parameters of MyWKWebViewConfiguration are immutable externally and can only be generated through MyWKWebViewConfigurator.

MyWKWebViewConfigurator + Simple Factory Simple Factory

Once we have the MyWKWebViewConfigurator Builder, we can create a simple factory to encapsulate and reuse the construction steps.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct MyWKWebViewConfiguratorFactory {
    enum ForType {
        case `default`
        case productPage
        case payment
    }
    
    static func make(for type: ForType) -> MyWKWebViewConfigurator {
        switch type {
        case .default:
            return MyWKWebViewConfigurator()
                .add(scriptMessageStrategy: PageScriptMessageStrategy())
                .set(overrideTitleFromWebView: false)
                .set(disableZoom: false)
        case .productPage:
            return Self.make(for: .default).set(disableZoom: true).set(overrideTitleFromWebView: true)
        case .payment:
            return MyWKWebViewConfigurator().set(headNavigationHandler: paymentNavigationActionHandler)
        }
    }
}

Chain of Responsibility Pattern

The Chain of Responsibility Pattern belongs to the behavioral design pattern, encapsulating object handling operations and chaining them together in a linked structure. The request operation will be passed along the chain until it is handled; the chained encapsulated operations can be flexibly combined and the order changed.

The Chain of Responsibility focuses on whether you want to handle something that comes in, if not, then skip it, so it cannot handle halfway or modify the input object and pass it to the next; if this is the requirement, it is another Interceptor Pattern.

The diagram above uses Tech Support (or OnCall…) as an example. When a problem object comes in, it first goes through CustomerService. If it cannot handle it, it is passed down to the next level, Supervisor. If it still cannot handle it, it continues down to TechSupport. Additionally, different responsibility chains can be formed for different issues. For example, if it is a problem from a major client, it will be handled directly from Supervisor. In the Swift UIKit Responder Chain, the Chain of Responsibility pattern is also used to respond to user operations on the UI.

WKWebView Scenario

In our WKWebView scenario, it is mainly applied in the func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) delegate method.

When the system receives a URL request, it will go through this method for us to decide whether to allow the redirection, and call decisionHandler(.allow) or decisionHandler(.cancel) at the end to inform the result.

In the implementation of WKWebView, there will be many judgments or page handling that are different from others and need to be bypassed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Original implementation...
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }
        
        // Simulated business logic: WebViewController deeplinkCheck == true (indicating the need to check and open the page through DeepLinkManager)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // Simulated DeepLinkManager logic, open the URL if successful and end the process.
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // Simulated business logic: WebViewController isHomePage == true (indicating the home page is open) & WebView is browsing the homepage, then switch TabBar Index
        if isHomePage {
            if url.absoluteString == "https://zhgchg.li" {
                print("Switch UITabBarController to Index 0")
                decisionHandler(.cancel)
            }
        }
        
        // Simulated business logic: WebViewController noNeedNativePresent == false (indicating the need to match special paths to open native pages)
        if !noNeedNativePresent {
            if url.pathComponents.count >= 3 {
                if url.pathComponents[1] == "product" {
                    // match http://zhgchg.li/product/1234
                    let id = url.pathComponents[2]
                    print("Present ProductViewController(\(id)")
                    decisionHandler(.cancel)
                } else if url.pathComponents[1] == "shop" {
                    // match http://zhgchg.li/shop/1234
                    let id = url.pathComponents[2]
                    print("Present ShopViewController(\(id)")
                    decisionHandler(.cancel)
                }
                // more...
            }
        }
        
        // more...
        decisionHandler(.allow)
}

As time goes by, the functionality becomes more and more complex, and the logic here will also become more and more. If the processing order is different, it will become a disaster.

Define the Handler Protocol first:

public protocol NavigationActionHandler: AnyObject {
    var nextHandler: NavigationActionHandler? { get set }

    /// Handles navigation actions for the web view. Returns true if the action was handled, otherwise false.
    func handle(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Bool
    /// Executes the navigation action policy decision. If the current handler does not handle it, the next handler in the chain will be executed.
    func exeute(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
}

public extension NavigationActionHandler {
    func exeute(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        if !handle(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) {
            self.nextHandler?.exeute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) ?? decisionHandler(.allow)
        }
    }
}
  • The operation is implemented in func handle(), returning true if there is further processing, otherwise false.
  • func exeute() is the default chain access implementation, which will traverse the entire operation chain from here. The default behavior is that when func handle() returns false (indicating that this node cannot handle it), it automatically calls the execute() of the next nextHandler to continue processing until the end.

Implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Default implementation, usually placed at the end
public final class DefaultNavigationActionHandler: NavigationActionHandler {
    public var nextHandler: NavigationActionHandler?
    
    public init() {
        
    }
    
    public func handle(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Bool {
        decisionHandler(.allow)
        return true
    }
}

//
final class PaymentNavigationActionHandler: NavigationActionHandler {
    var nextHandler: NavigationActionHandler?
    
    func handle(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Bool {
        guard let url = navigationAction.request.url else {
            return false
        }
        
        // Simulate business logic: Payment related, two-step verification WebView...etc
        print("Present Payment Verify View Controller")
        decisionHandler(.cancel)
        return true
    }
}

//
final class DeeplinkManagerNavigationActionHandler: NavigationActionHandler {
    var nextHandler: NavigationActionHandler?
    
    func handle(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Bool {
        guard let url = navigationAction.request.url else {
            return false
        }
        
        
        // Simulate DeepLinkManager logic, open the URL if successful and end the process.
        // if DeepLinkManager.open(url) == true {
            decisionHandler(.cancel)
            return true
        // } else {
            return false
        //
    }
}

// More...
1
2
3
4
5
6
7
8
9
10
11
12
extension MyWKWebViewController: WKNavigationDelegate {
    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
       let headNavigationActionHandler = DeeplinkManagerNavigationActionHandler()
       let defaultNavigationActionHandler = DefaultNavigationActionHandler()
       let paymentNavigationActionHandler = PaymentNavigationActionHandler()
       
       headNavigationActionHandler.nextHandler = paymentNavigationActionHandler
       paymentNavigationActionHandler.nextHandler = defaultNavigationActionHandler
       
       headNavigationActionHandler.exeute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
    }
}

This way, when a request is received, it will be processed sequentially according to the handling chain we defined.

Combining the previous Builder Pattern MyWKWebViewConfigurator by exposing headNavigationActionHandler as a parameter allows external control over the processing requirements and order of this WKWebView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
extension MyWKWebViewController: WKNavigationDelegate {
    public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        configuration.headNavigationHandler?.exeute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) ?? decisionHandler(.allow)
    }
}

//...
struct MyWKWebViewConfiguratorFactory {
    enum ForType {
        case `default`
        case productPage
        case payment
    }
    
    static func make(for type: ForType) -> MyWKWebViewConfigurator {
        switch type {
        case .default:
            // Simulating default scenario with these handlers
            let deplinkManagerNavigationActionHandler = DeeplinkManagerNavigationActionHandler()
            let homePageTabSwitchNavigationActionHandler = HomePageTabSwitchNavigationActionHandler()
            let nativeViewControllerNavigationActionHandlera = NativeViewControllerNavigationActionHandler()
            let defaultNavigationActionHandler = DefaultNavigationActionHandler()
            
            deplinkManagerNavigationActionHandler.nextHandler = homePageTabSwitchNavigationActionHandler
            homePageTabSwitchNavigationActionHandler.nextHandler = nativeViewControllerNavigationActionHandlera
            nativeViewControllerNavigationActionHandlera.nextHandler = defaultNavigationActionHandler
            
            return MyWKWebViewConfigurator()
                .add(scriptMessageStrategy: PageScriptMessageStrategy())
                .add(scriptMessageStrategy: UserScriptMessageStrategy())
                .set(headNavigationHandler: deplinkManagerNavigationActionHandler)
                .set(overrideTitleFromWebView: false)
                .set(disableZoom: false)
        case .productPage:
            return Self.make(for: .default).set(disableZoom: true).set(overrideTitleFromWebView: true)
        case .payment:
            // Simulating payment page with only these handlers, and paymentNavigationActionHandler having the highest priority
            let paymentNavigationActionHandler = PaymentNavigationActionHandler()
            let deplinkManagerNavigationActionHandler = DeeplinkManagerNavigationActionHandler()
            let defaultNavigationActionHandler = DefaultNavigationActionHandler()
            
            paymentNavigationActionHandler.nextHandler = deplinkManagerNavigationActionHandler
            deplinkManagerNavigationActionHandler.nextHandler = defaultNavigationActionHandler
            
            return MyWKWebViewConfigurator().set(headNavigationHandler: paymentNavigationActionHandler)
        }
    }
}

Strategy Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

![](/assets/f4b02ee342a4/1*RiMbrBGdFG6INBRCcE_WZw.png)

> _The Strategy Pattern belongs to the **behavioral** design pattern, which abstracts the actual operation. We can implement various different operations, allowing flexibility to replace them according to different contexts._

The above diagram illustrates different payment methods. We abstract the payment as a `Payment` Protocol (Interface), and then each payment method implements its own implementation. When using `PaymentContext` (simulating external usage), based on the user's selected payment method, the corresponding Payment entity is generated and `pay()` is called to process the payment.

#### WKWebView Scenario

> _Used in the interaction between WebView and frontend pages._

> _When frontend JavaScript calls:_

> _`window.webkit.messageHandlers.Name.postMessage(Parameters);`_

> _It will go to WKWebView to find the corresponding `WKScriptMessageHandler` Class for `Name` and execute the operation._

The system already has defined Protocol and the corresponding `func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String)` method. We just need to define our own `WKScriptMessageHandler` implementation and add it to WKWebView. The system will dispatch to the corresponding concrete strategy to execute based on the received `name` following the Strategy Pattern strategy.

Here, we simply extend the Protocol with `WKScriptMessageHandler`, adding an `identifier: String` for `add(.. name:)` usage:

![](/assets/f4b02ee342a4/1*RLA13rSVDIG9cV3CsWtS3g.png)

```swift
public protocol ScriptMessageStrategy: NSObject, WKScriptMessageHandler {
    static var identifier: String { get }
}

Implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
final class PageScriptMessageStrategy: NSObject, ScriptMessageStrategy {
    static var identifier: String = "page"
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        // Simulating called from js: window.webkit.messageHandlers.page.postMessage("Close");
        print("\(Self.identifier): \(message.body)")
    }
}

//

final class UserScriptMessageStrategy: NSObject, ScriptMessageStrategy {
    static var identifier: String = "user"
    
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        // Simulating called from js: window.webkit.messageHandlers.user.postMessage("Hello");
        print("\(Self.identifier): \(message.body)")
    }
}

WKWebView Registration:

1
2
3
4
var scriptMessageStrategies: [ScriptMessageStrategy] = []
scriptMessageStrategies.forEach { scriptMessageStrategy in
  webView.configuration.userContentController.add(scriptMessageStrategy, name: type(of: scriptMessageStrategy).identifier)
}

Combining the Builder Pattern from the previous MyWKWebViewConfigurator to externally manage the registration of ScriptMessageStrategy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class MyWKWebViewConfigurator {
    //...
    
    // You can encapsulate the logic for adding rules inside
    public func add(scriptMessageStrategy: ScriptMessageStrategy) -> Self {
        // Here, only the old logic will be deleted first when implementing duplicate identifiers
        scriptMessageStrategies.removeAll(where: { type(of: $0).identifier == type(of: scriptMessageStrategy).identifier })
        scriptMessageStrategies.append(scriptMessageStrategy)
        return self
    }
    //...
}

//...

public class MyWKWebViewController: UIViewController {
    //...
    public override func viewDidLoad() {
        super.viewDidLoad()
       
        //...
        configuration.scriptMessageStrategies.forEach { scriptMessageStrategy in
            webView.configuration.userContentController.add(scriptMessageStrategy, name: type(of: scriptMessageStrategy).identifier)
        }
        //...
    }
}

Question: Can this scenario also be replaced with the Chain of Responsibility Pattern?

At this point, some friends may wonder if the Strategy Pattern here can be replaced with the Chain of Responsibility Pattern.

Both of these design patterns are behavioral and can be replaced; however, the actual choice depends on the specific requirements. In this case, the Strategy Pattern is very typical, where WKWebView determines different strategies based on the Name. If our requirement involves chain dependencies between different strategies or recovery relationships, such as if AStrategy cannot handle it and needs to pass it to BStrategy, then we would consider using the Chain of Responsibility Pattern.

Strategy v.s. Chain of Responsibility

Strategy v.s. Chain of Responsibility

  • Strategy Pattern: Clearly defined execution strategies without relationships between them.
  • Chain of Responsibility Pattern: Execution strategy is determined in individual implementations, passing to the next implementation if unable to handle.

For complex scenarios, you can combine the Chain of Responsibility Pattern inside the Strategy Pattern to achieve the desired outcome.

Final Combination

  • Simple Factory Pattern MyWKWebViewConfiguratorFactory -> Encapsulates the steps to generate MyWKWebViewConfigurator
  • Builder Pattern MyWKWebViewConfigurator -> Encapsulates MyWKWebViewConfiguration parameters and construction steps
  • Injection of MyWKWebViewConfiguration -> Used by MyWKWebViewController
  • Chain of Responsibility Pattern MyWKWebViewController’s func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Calls headNavigationHandler?.execute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) for chain execution handling
  • Strategy Pattern MyWKWebViewController’s webView.configuration.userContentController.addUserScript(XXX) dispatches the corresponding JS Caller to the respective handling strategy.

Complete Demo Repo

Further Reading

If you have any questions or suggestions, feel free to contact me.

===

本文中文版本

===

This article was first published in Traditional Chinese on Medium ➡️ View Here



This post is licensed under CC BY 4.0 by the author.

Travelogue 2024 Bangkok 🇹🇭 5-Day Free and Easy Trip

Behavior Change in Merging NSAttributedString Attributes Range in iOS ≥ 18