Post

Design Patterns in WKWebView: Builder, Strategy & Chain of Responsibility Explained

Explore how to apply Builder, Strategy, and Chain of Responsibility patterns to streamline WKWebView encapsulation in iOS, solving complex UI logic and improving code maintainability effectively.

Design Patterns in WKWebView: Builder, Strategy & Chain of Responsibility Explained

点击这里查看本文章简体中文版本。

點擊這裡查看本文章正體中文版本。

This post was translated with AI assistance — let me know if anything sounds off!


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

Design Patterns Used When Encapsulating iOS WKWebView (Strategy, Chain of Responsibility, Builder).

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 mention that the classic GoF 23 design patterns were published over 30 years ago (1994). With changes in tools, languages, and software development approaches, the landscape has evolved significantly. Many new design patterns have since emerged in different fields. Design Patterns are neither a cure-all nor the only solution. They serve more as a “language shorthand” to apply the right pattern in the right context, reducing collaboration barriers during development. For example, applying the Strategy Pattern here allows future maintenance and extension to follow the Strategy Pattern structure directly. Most design patterns also provide good decoupling, which greatly benefits extensibility and testability.

Mindset for Using Design Patterns

  • Not the only solution

  • Not a cure-all

  • Do not apply rigidly; choose the appropriate design pattern based on the type of problem to solve (creation? behavior? structure?) and the purpose.

  • Do not drastically alter the code. Major changes can cause confusion for future maintainers. Just like language, everyone calls Apple “Apple.” If you define it as “Banana,” it becomes an additional development cost that requires special knowledge.

  • Avoid using keywords whenever possible. For example, the Factory Pattern is commonly named XXXFactory. If it is not a factory pattern, this naming keyword should not be used.

  • Be cautious when creating your own patterns. Although there are only 23 classic ones, many new patterns have emerged through years of evolution across various fields. You can first refer to online resources to find suitable patterns (after all, three heads are better than one). If none fit, then 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 easy to maintain and extend, using design patterns is not always necessary.

  • The team must have a consensus on Design Patterns before using them.

  • Design Patterns can be combined with other Design Patterns for advanced techniques.

  • Getting started with Design Patterns requires continuous practical experience to develop a sharper sense of which scenarios are suitable or unsuitable for their application.

Helpful Tool ChatGPT

Since the arrival of ChatGPT, learning practical applications of Design Patterns has become easier. Just describe your problem clearly and ask which design patterns suit the scenario. It can provide several possible patterns along with explanations. Although not every answer is perfect, it at least offers feasible directions. We only need to explore these patterns further in the context of our own practical issues, and we can eventually find a good solution!

Practical Use Cases of WKWebView Design Patterns

This practical application of Design Patterns focuses on consolidating the WKWebView object features within the current codebase and applying Design Patterns at several suitable logical abstraction points while developing a unified WKWebView component. This is a record of insights and experiences shared.

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

Original Non-Abstract Writing Style

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 init...

    // 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 home page?
    let isHomePage: Bool
    // Simulate business logic: WKUserScript scripts to inject into WKWebView
    let userScripts: [WKUserScript]
    // Simulate business logic: WKScriptMessageHandler scripts to inject into WKWebView
    let scriptMessageHandlers: [String: WKScriptMessageHandler]
    // Allow overriding ViewController title from WebView title
    let overrideTitleFromWebView: Bool
    
    let url: URL
    
    // ... 
}
// ...
extension OldWKWebViewController: WKNavigationDelegate {
    // MARK - iOS WKWebView navigationAction Delegate, used to decide how to handle the upcoming link
    // Must call decisionHandler(.allow) or decisionHandler(.cancel) at the end
    // decisionHandler(.cancel) will stop loading the upcoming page

    // Here simulates different logic based on different variables and switches:

    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 (means need to check with DeepLinkManager and open page)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // Simulate DeepLinkManager logic, if URL can be opened successfully, open it and end the process.
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // Simulate business logic: WebViewController isHomePage == true (means it is home page) & WebView is browsing home page, then 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 (means 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)
    }
}
// ...

Question

  1. Setting variables and toggles openly within a class makes it unclear which are meant for configuration.

  2. Directly exposing the WKUserScript variable externally is not recommended; we want to control the injected JS and only allow specific behaviors to be injected.

  3. Unable to control WKScriptMessageHandler registration rules.

  4. If you need to initialize similar WebViews, you must repeatedly write the injection parameter rules, and the parameter rules cannot be reused.

  5. Inside the navigationAction Delegate, the flow is controlled by variables. To delete or change the flow or order, you must modify the entire code, which may break the originally working flow.

Builder Pattern 建造者模式

Builder Pattern belongs to the creational design patterns. It separates the steps and logic of object creation, allowing the user to set parameters step by step and reuse configurations, ultimately creating the target object. Additionally, the same creation steps can produce different object implementations.

The above diagram uses making Pizza as an example. First, the steps to make Pizza are divided into several methods and declared in the PizzaBuilder Protocol (Interface). The ConcretePizzaBuilder is the object that actually makes the Pizza, which could be a Vegetarian PizzaBuilder or a Meat PizzaBuilder. Different Builders may use different ingredients, but they all ultimately build() to produce a Pizza object.

WKWebView Scenario

Back to the WKWebView scenario, our final output object is MyWKWebViewConfiguration. We unify all the variables needed for WKWebView settings 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 exposed only within the Module (Internal)

MyWKWebViewConfigurator (Builder Pattern)

Here, since I only need to build for MyWKWebView, I did not further split MyWKWebViewConfigurator into a Protocol (Interface).

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
    }
    
    // You can encapsulate additional logic rules 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)
    }
}

Adding an extra layer of separation can better utilize Access Control to restrict parameter usage. In this scenario, we want to allow direct injection of WKUserScript into MyWKWebView, but we don’t want to open it up so much that users can inject scripts arbitrarily. By combining the Builder Pattern with Swift Access Control, once MyWKWebView is placed inside a module, MyWKWebViewConfigurator exposes an operation method func set(disableZoom: Bool) externally. Internally, when creating MyWKWebViewConfiguration, it attaches the WKUserScript. All parameters of MyWKWebViewConfiguration are immutable from outside and can only be generated through MyWKWebViewConfigurator.

MyWKWebViewConfigurator + Simple Factory

After having the MyWKWebViewConfigurator Builder, we can create a simple factory to encapsulate and reuse the setup process.

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 behavioral design patterns. It encapsulates object handling operations and links them in a chain structure. Requests are passed along the chain until one handles them. The linked operation encapsulations can be freely combined and reordered.

The Chain of Responsibility focuses on whether you need to handle the incoming item; if not, just Skip , so it cannot process partially or modify the input object before passing it to the next. If that is the requirement, then it is another Interceptor Pattern.

The above diagram uses Tech Support (or OnCall…) as an example. When an issue arrives, it first goes through CustomerService. If they can’t handle it, it is passed down to the next level, Supervisor. If still unresolved, it continues down to TechSupport. Additionally, different chains of responsibility can be formed for different issues. For example, problems from major clients may be handled directly starting from Supervisor. The Responder Chain in Swift UIKit also uses the Chain of Responsibility pattern to respond to user actions 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 passes through this method for us to decide whether to allow navigation, and calls decisionHandler(.allow) or decisionHandler(.cancel) at the end to indicate the result.

In the implementation of WKWebView, many conditions arise, or some pages need to be handled differently and 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
        }
        
        // Simulate business logic: WebViewController deeplinkCheck == true (means need to check and open page via DeepLinkManager)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // Simulate DeepLinkManager logic, if URL can be opened successfully, open it and end the process.
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // Simulate business logic: WebViewController isHomePage == true (means opening home page) & WebView is browsing home page, then 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 (means 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, features become more complex, and the logic here increases. If the processing order also needs to change, it will 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 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 will be implemented in func handle(). Return true if there are further processes, otherwise return false.

  • func execute() is the default chain access implementation. It traverses the entire operation chain from here. The default behavior is that when func handle() returns false (meaning this node cannot handle it), it automatically calls the next nextHandler’s execute() 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-factor 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, if 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.exeute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
    }
}

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

Combine with the previous Builder Pattern MyWKWebViewConfigurator by making headNavigationActionHandler a parameter, allowing external control over the WKWebView’s handling requirements and order:

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:
            // Simulate default case 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:
            // Simulate payment page only needs these handlers, with paymentNavigationActionHandler having 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 part of behavioral design patterns. It abstracts the actual operations, enabling the implementation of multiple operations that can be flexibly swapped according to different scenarios.

The above diagram uses different payment methods as examples. We abstract payment as a Payment Protocol (Interface), then each payment method implements its own version. In PaymentContext (simulating external use), the corresponding Payment instance is created based on the user’s chosen payment method, and pay() is called uniformly to process the payment.

WKWebView Scenario

Used in interaction between WebView and frontend pages.

When the frontend JavaScript calls:

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

It will enter WKWebView to find the corresponding WKScriptMessageHandler class with the given Name and execute the operation.

The system already defines the Protocol and the corresponding func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String) method. We only need to implement our own WKScriptMessageHandler and add it to the WKWebView. The system will then dispatch to the appropriate concrete strategy based on the received name, following the Strategy Pattern.

Here is a simple Protocol extension of WKScriptMessageHandler with an additional identifier:String for add(.. name:) usage:

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) {
        // Simulate 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) {
        // Simulate called from js: window.webkit.messageHandlers.user.postMessage("Hello");
        print("\(Self.identifier): \(message.body)")
    }
}

WKWebView Registration Usage:

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 manages the registration of ScriptMessageStrategy from 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 {
    //...
    
    // You can encapsulate additional logic rules here
    public func add(scriptMessageStrategy: ScriptMessageStrategy) -> Self {
        // Here, the logic is to remove the old one first if there is a duplicate identifier
        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 implemented using the Chain of Responsibility Pattern?

At this point, some may ask if the Strategy Pattern here can be replaced by the Chain of Responsibility Pattern?

These two design patterns are both behavioral and can be interchangeable; however, the choice depends on the specific scenario. Here, it is a typical Strategy Pattern where WKWebView selects different strategies based on the Name. If the requirement involves chained dependencies or recovery relationships between strategies—for example, if AStrategy does not handle it and passes it to BStrategy—then the Chain of Responsibility Pattern should be considered.

Strategy v.s. Chain of Responsibility

Strategy vs. Chain of Responsibility

  • Strategy Pattern: There is a clear dispatch execution strategy, and the strategies are independent of each other.

  • Chain of Responsibility Pattern: The execution strategy is determined within each implementation. If it cannot handle the request, it passes it to the next implementation.

Complex scenarios can be achieved by applying the Chain of Responsibility Pattern within the Strategy Pattern.

Final Combination

  • Simple Factory Pattern MyWKWebViewConfiguratorFactory -> Encapsulates the creation process of MyWKWebViewConfigurator

  • Builder Pattern MyWKWebViewConfigurator -> Encapsulates MyWKWebViewConfiguration parameters and build steps

  • MyWKWebViewConfiguration Injection -> Used by MyWKWebViewController

  • Chain of Responsibility Pattern MyWKWebViewController’s func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> calls headNavigationHandler?.exeute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) to execute the chain processing

  • Strategy Pattern MyWKWebViewController uses webView.configuration.userContentController.addUserScript(XXX) to dispatch corresponding JS Callers to the appropriate handling strategies

Complete Demo Repo

Further Reading

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


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

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