Post

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

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

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

ℹ️ℹ️ℹ️ The following content is translated by OpenAI.

Click here to view the original Chinese version. | 點此查看本文中文版


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

This article discusses the scenarios of Design Patterns used when encapsulating iOS WKWebView (Strategy, Chain of Responsibility, Builder Pattern).

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’s important to note that the classic 23 design patterns from the Gang of Four (GoF) were published over 30 years ago (in 1994). The evolution of tools, languages, and software development methodologies has changed significantly since then, leading to the emergence of many new design patterns in various fields. Design Patterns are not a one-size-fits-all solution; rather, they serve as a “language shorthand” that can be applied appropriately in suitable scenarios to reduce collaboration barriers in development. For example, by applying the Strategy Pattern here, future maintainers can iterate based on the structure of the Strategy Pattern. Moreover, design patterns are generally well-decoupled, which significantly aids in extensibility and testability.

Principles for Using Design Patterns

  • They are not the only solution.
  • They are not a universal solution.
  • They should not be rigidly applied; choose the appropriate design pattern based on the type of problem to be solved (creation? behavior? structure?) and the intended purpose.
  • Avoid “magic modifications”; such changes can lead to misunderstandings for future maintainers. Just as everyone refers to Apple as “Apple,” if you define it as “Banana,” it becomes a development cost that requires special knowledge.
  • Try to avoid using keywords; for example, if the Factory Pattern is commonly named XXXFactory, then it should not be used if it is not a factory pattern.
  • Be cautious when creating your own patterns. Although there are only 23 classic patterns, many new patterns have evolved over the years across various fields. It’s advisable to refer to online resources to find suitable patterns (after all, three cobblers are better than one Zhuge Liang). If there are truly no existing patterns, propose new design patterns and try to publish them for review and adjustment by people from different fields and contexts.
  • Code is ultimately written for people to maintain; as long as it is maintainable and extensible, it does not necessarily have to use design patterns.
  • Teams should have a consensus on Design Patterns before using them.
  • Design Patterns can be combined with other Design Patterns.
  • Mastering Design Patterns requires practical experience and continuous refinement to develop sensitivity to which scenarios are suitable or unsuitable for application.

Support Tool: ChatGPT

Since the advent of ChatGPT, learning about the practical applications of Design Patterns has become much easier. By clearly describing your questions, you can ask it which design patterns are suitable for a given scenario, and it can provide several possible patterns along with explanations. While not every answer may be perfectly suitable, it at least offers several viable directions. We can then delve deeper into these patterns, combining them with our practical scenario issues to ultimately arrive at good solutions!

Practical Application Scenarios of Design Patterns in WKWebView

This practical application of Design Patterns focuses on consolidating the functional characteristics of WKWebView objects in the current codebase and developing a unified WKWebView component, sharing insights on applying Design Patterns at several appropriate logical abstraction points.

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

Original Non-Abstract 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 external initialization...

    // Simulated business logic: switch to open native pages for special paths
    let noNeedNativePresent: Bool
    // Simulated business logic: switch for DeeplinkManager checks
    let deeplinkCheck: Bool
    // Simulated business logic: is this the home page?
    let isHomePage: Bool
    // Simulated business logic: scripts to inject into WKWebView's WKUserScript
    let userScripts: [WKUserScript]
    // Simulated business logic: scripts for WKWebView's WKScriptMessageHandler
    let scriptMessageHandlers: [String: WKScriptMessageHandler]
    // Whether to allow overriding the ViewController title from the WebView
    let overrideTitleFromWebView: Bool
    
    let url: URL
    
    // ... 
}
// ...
extension OldWKWebViewController: WKNavigationDelegate {
    // MARK - iOS WKWebView's navigationAction Delegate, used to decide how to handle the link to be loaded
    // Always call decisionHandler(.allow) or decisionHandler(.cancel) at the end
    // decisionHandler(.cancel) will interrupt the loading of the page

    // Here, different variables and switches simulate different logical processes:

    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 a need to check via DeepLinkManager and open the page)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // Simulated DeepLinkManager logic; if the URL can be opened successfully, open it and end the process.
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // Simulated business logic: WebViewController isHomePage == true (indicating it is the home page) & WebView is browsing the home page, 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 a 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. The variables and switches are scattered throughout the class, making it unclear which are for configuration.
  2. WKUserScript variable settings are directly exposed to the outside, whereas we want to control the injected JS and only allow specific behaviors.
  3. There is no control over the registration rules for WKScriptMessageHandler.
  4. If similar WebViews need to be initialized, the injection parameter rules must be rewritten, and the parameter rules cannot be reused.
  5. The navigationAction Delegate relies on variables to control the flow; if the flow or order needs to be modified, the entire code must be altered, which could disrupt previously functioning processes.

Builder Pattern

The Builder Pattern is a creational design pattern that separates the steps and logic of object creation, allowing the operator to set parameters step by step and reuse settings, ultimately creating the target object. Additionally, the same creation steps can produce different objects.

The above diagram uses the example of making a Pizza, breaking down the steps into several methods declared in the PizzaBuilder protocol (interface). ConcretePizzaBuilder is the actual object that makes the Pizza, which could be a VegetarianPizzaBuilder or a MeatPizzaBuilder; different builders may use different ingredients, but they all ultimately build() a Pizza object.

WKWebView Scenario

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

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 exposed only within the module (Internal)

MyWKWebViewConfigurator (Builder Pattern)

In this case, since I only need to build for MyWKWebView, I did not further separate MyWKWebViewConfigurator into 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() {
        
    }
    
    // Parameter encapsulation and 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
    }
    
    // New logical rules can be encapsulated here
    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)
    }
}

By adding another layer, we can better utilize access control to isolate parameter usage rights. In this scenario, we want to allow the injection of WKUserScript into MyWKWebView, but we do not want to open it up too broadly for users to inject freely. Therefore, by combining the Builder Pattern with Swift Access Control, when MyWKWebView is placed within the module, MyWKWebViewConfigurator encapsulates the operational methods like func set(disableZoom: Bool), while internally, it adds WKUserScript when generating MyWKWebViewConfiguration. All parameters of MyWKWebViewConfiguration are immutable from the outside and can only be generated through MyWKWebViewConfigurator.

MyWKWebViewConfigurator + Simple Factory

Once we have the MyWKWebViewConfigurator Builder, we can create a simple factory to encapsulate and reuse the creation 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 is a behavioral design pattern that encapsulates the operations of object handling and links them in a chain structure. Request operations are passed along the chain until they are handled; the linked operations can be freely and flexibly combined and reordered.

The Chain of Responsibility focuses on whether you want to handle something when it comes in; if not, just skip it. Therefore, it cannot handle partially or modify the input object before passing it to the next; if that is the requirement, it is another Interceptor Pattern.

The above diagram uses Tech Support (or OnCall) as an example. When a problem object comes in, it first goes through CustomerService. If they cannot handle it, it is passed to the next level, Supervisor. If they 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 major client’s issue, it will be handled starting from Supervisor. The Responder Chain in Swift UIKit also uses the Chain of Responsibility Pattern.

WKWebView Scenario

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

When the system receives a URL request, it goes through this method to decide whether to allow the jump, and at the end of the processing, it calls decisionHandler(.allow) or decisionHandler(.cancel) to inform the result.

In the implementation of WKWebView, there are many conditions to check, and some pages require different handling that needs 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
        }
        
        // Simulating business logic: WebViewController deeplinkCheck == true (indicating a check with DeepLinkManager is needed to open the page)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // Simulating DeepLinkManager logic; if the URL can be opened successfully, open it and end the process.
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // Simulating business logic: WebViewController isHomePage == true (indicating it's the home page) & WebView is browsing the home page, then switch TabBar Index
        if isHomePage {
            if url.absoluteString == "https://zhgchg.li" {
                print("Switch UITabBarController to Index 0")
                decisionHandler(.cancel)
            }
        }
        
        // Simulating business logic: WebViewController noNeedNativePresent == false (indicating a special path needs to open a native page)
        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 on, the functionality becomes increasingly complex, and the logic here will grow as well. If the order of handling also needs to differ, it can turn into a disaster.

First, define the Handler Protocol:

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 execute(webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
}

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

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
// 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
        }
        
        // Simulating business logic: Payment related, two-factor authentication 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
        }
        
        // Simulating DeepLinkManager logic; if the URL can be opened successfully, open it and end the process.
        // if DeepLinkManager.open(url) == true {
            decisionHandler(.cancel)
            return true
        // } else {
            return false
        //
    }
}

// More...

Usage:

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.execute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
    }
}

This way, when a request is received, it will be processed in the order defined by our handling chain.

Combining with the previous Builder Pattern, MyWKWebViewConfigurator, we can expose headNavigationActionHandler as a parameter, allowing external determination of the handling requirements and order for 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?.execute(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 situation with these handlers
            let deplinkManagerNavigationActionHandler = DeeplinkManagerNavigationActionHandler()
            let homePageTabSwitchNavigationActionHandler = HomePageTabSwitchNavigationActionHandler()
            let nativeViewControllerNavigationActionHandler = NativeViewControllerNavigationActionHandler()
            let defaultNavigationActionHandler = DefaultNavigationActionHandler()
            
            deplinkManagerNavigationActionHandler.nextHandler = homePageTabSwitchNavigationActionHandler
            homePageTabSwitchNavigationActionHandler.nextHandler = nativeViewControllerNavigationActionHandler
            nativeViewControllerNavigationActionHandler.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 needing only these handlers, with 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

The Strategy Pattern is a behavioral design pattern that abstracts actual operations, allowing us to implement various operations that can be flexibly replaced based on different contexts.

The above diagram uses different payment methods as an example. We abstract payment as a Payment Protocol (Interface), and various payment methods implement their own versions. In PaymentContext (simulating external usage), based on the user’s selected payment method, the corresponding Payment instance is created, and pay() is called to proceed with the payment.

WKWebView Scenario

Used in the interaction between WebView and front-end pages.

When the front-end JavaScript calls:

window.webkit.messageHandlers.Name.postMessage(Parameters);

It will enter WKWebView, find the corresponding Name’s WKScriptMessageHandler Class, and execute the operation.

The system already has defined Protocols 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 then use the Strategy Pattern to dispatch to the corresponding concrete strategy based on the received name.

Here, we simply extend the Protocol WKScriptMessageHandler to include an additional identifier:String for use in add(.. name:):

1
2
3
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)")
    }
}

Registering in WKWebView:

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

Combining with the previous Builder Pattern, MyWKWebViewConfigurator, to manage the registration of ScriptMessageStrategy from the outside:

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 {
    //...
    
    // Logic for adding can be encapsulated here
    public func add(scriptMessageStrategy: ScriptMessageStrategy) -> Self {
        // Here we implement logic to remove old ones if the identifier is duplicated
        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 may wonder if the Strategy Pattern here can be replaced with the Chain of Responsibility Pattern.

Both design patterns are behavioral and can be interchangeable; however, it depends on the specific requirements of the scenario. In this case, it is a typical Strategy Pattern, where WKWebView decides which different Strategy to enter based on the Name. If our requirement is that different Strategies may have a chain dependency or recovery relationship, for example, if AStrategy does not handle it, it should be passed to BStrategy, then we would consider using the Chain of Responsibility Pattern.

Strategy vs. Chain of Responsibility

  • Strategy Pattern: There is a clear dispatch to execute strategies, and there is no relationship between strategies.
  • Chain of Responsibility Pattern: The execution strategy is determined within individual implementations; if it cannot be handled, it is passed down to the next implementation.

In complex scenarios, you can use the Strategy Pattern combined with the Chain of Responsibility Pattern to achieve the desired outcome.

Final Combination

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

Complete Demo Repo

Further Reading

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


This article was first published on Medium ➡️ Click Here

Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.

Improve this page on Github.

Buy me a beer

405 Total Views
Last Statistics Date: 2025-03-11 | 372 Views on Medium.
This post is licensed under CC BY 4.0 by the author.