Home Design Patterns 的實戰應用紀錄—In WKWebView with Builder, Strategy & Chain of Responsibility Pattern
Post
Cancel

Design Patterns 的實戰應用紀錄—In WKWebView with Builder, Strategy & Chain of Responsibility Pattern

Design Patterns 的實戰應用紀錄—In WKWebView with Builder, Strategy & Chain of Responsibility Pattern

封裝 iOS WKWebView 時使用到的 Design Patterns 場景 (策略、責任鏈、建造模式)。

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

每次講 Design Patterns 之前都要提一下,最經典的 GoF 23 種設計模式發表至今已過去 30 年 (1994 年發行),工具、語言的變化、軟體開發模式的變遷已經不可同日而語,後續在不同領域也延伸出許多新的設計模式;Design Patterns 並不是萬能解、也不是唯一解,他的存在更像是一種「語言代稱」在適合的場景套用適合的設計模式,可以減少開發協作的障礙,例如:這邊套用策略模式,後續維護擴充的人,就可以直接依照策略模式的架構進行迭代,並且設計模式多半都解耦的不錯,對於擴充性、測試性也有顯著的幫助。

Design Patterns 的使用心法

  • 不是唯一解
  • 不是萬能解
  • 不能硬套,需按照要解決問題的類型(創建?行為?結構?)、目的選擇對應的設計模式
  • 不能魔改,魔改容易造成後續維護的人誤會,跟語言一樣大家都用蘋果都叫 Apple,如果自己定義叫 Banana 就會變成是一個需要特別知道的開發成本
  • 盡可能避開關鍵字,例如 Factory Pattern 習慣命名為 XXXFactory ,那如果不是工廠模式就不該使用此命名關鍵字
  • 謹慎自己創造模式 ,同前述雖然經典的只有 23 種,但經歷各個領域多年的演化也有很多新的模式,可以先參考網路資料找到適合的模式(畢竟三個臭皮匠勝過一個諸葛亮),真的沒有再來提出新的設計模式並盡可能發表讓不同領域、不同場境的人一起檢視跟調整
  • 程式終究是寫給人維護的,只要好維護、好擴充,不一定要使用設計模式
  • 團隊要有 Design Patterns 的共識才適合使用
  • Design Pattern 可以再套 Design Pattern 組合技
  • Design Patterns 上手要經過實務不斷地淬鍊,才會越來越有什麼場景適合或不適合套用的敏銳度

輔助神器 ChatGPT

自從有了 ChatGPT 學習 Design Patterns 設計模式的實務應用就更容易,只要把你的問題具體的描述給他,問他有哪些設計模式適合這個場景,他都能給出幾個可能適合的模式並且附上說明;雖然不是每個答案都那麼適合,但他至少給出了幾個可行方向,我們只要再深入這幾個模式結合自己實務場景的問題,最後都能選到不錯的解法!

WKWebView 的 Design Patterns 實戰應用場景

這次的 Design Patterns 實戰應用是在收斂目前 Codebase 中的 WKWebView 物件功能特性,並開發統一的 WKWebView 元件時在幾個合適的邏輯抽象點套用 Design Patterns 的心得紀錄分享。

完整 Demo 專案程式碼會附在文末。

原始無抽象的寫法

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 - 定義一些變數、開關 讓外部 init 時注入特性...

    // 模擬商業邏輯:開關 Match 特殊路徑開原生頁面
    let noNeedNativePresent: Bool
    // 模擬商業邏輯:開關 DeeplinkManager 檢查
    let deeplinkCheck: Bool
    // 模擬商業邏輯:是開首頁嗎?
    let isHomePage: Bool
    // 模擬商業邏輯:要注入到 WKWebView 的 WKUserScript 的腳本
    let userScripts: [WKUserScript]
    // 模擬商業邏輯:要注入到 WKWebView 的 WKScriptMessageHandler 的腳本
    let scriptMessageHandlers: [String: WKScriptMessageHandler]
    // 是否允許從 WebView 取得 Title 複寫 ViewController Title
    let overrideTitleFromWebView: Bool
    
    let url: URL
    
    // ... 
}
// ...
extension OldWKWebViewController: WKNavigationDelegate {
    // MARK - iOS WKWebView 的 navigationAction Delegate,用於讓我們決定即將載入的連結要怎麼處理
    // 結束務必呼叫 decisionHandler(.allow) or decisionHandler(.cancel)
    // decisionHandler(.cancel) 將中斷載入即將載入的頁面

    // 這邊模擬了不同的變數、開關會有不同的邏輯處理:

    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }
        
        // 模擬商業邏輯:WebViewController deeplinkCheck == true (代表需要過 DeepLinkManager 檢查並開啟頁面)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // 模擬 DeepLinkManager 邏輯,URL 能成功打開則打開並結束流程。
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // 模擬商業邏輯:WebViewController isHomePage == true (代表是開主頁) & WebView 正在瀏覽首頁,則切換 TabBar Index
        if isHomePage {
            if url.absoluteString == "https://zhgchg.li" {
                print("Switch UITabBarController to Index 0")
                decisionHandler(.cancel)
            }
        }
        
        // 模擬商業邏輯:WebViewController noNeedNativePresent == false (代表需要 Match 特殊路徑開原生頁面)
        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)
    }
}
// ...

問題

  1. 設定變數、開關攤在 Class 當中,不清楚哪些是設定使用
  2. 直接暴露 WKUserScript 變數設定給外部,我們希望能管控注入的 JS,只允許注入特定行為
  3. 無法控制 WKScriptMessageHandler 的註冊規則
  4. 如果要 init 差不多的 WebView 需要重複寫注入參數的規則,參數規則無法復用
  5. navigationAction Delegate 內部靠變數控制流程,如果要刪改流程或順序都要動到整個 Code,也可能改壞本來就正常的流程

Builder Pattern 建造者模式

Builder Pattern(建造者模式) 屬於 創建型 設計模式,將創建物件的步驟與邏輯分離,操作者可一步一步設定參數並且復用設定,並在最後創建出目標物件,另外同樣的創建步驟也可以創建出不同的對象實現。

上圖以製作 Pizza 為例,先將 Pizza 製作的步驟拆成好幾個方法,並宣告在 PizzaBuilder 這個 Protocol (Interface), ConcretePizzaBuilder 為實際製作 Pizza 的物件,可能為 素食 PizzaBuilder & 葷食 PizzaBuilder ;不同的 Builder 原料可能不一樣,但最終都會 build() 產出 Pizza 物件。

WKWebView 場景

回到 WKWebView 場景,我們的最終產出物件是 MyWKWebViewConfiguration ,我們把所有 WKWebView 會需要設定的變數全統一放到這個物件當中,並使用 Builder Pattern MyWKWebViewConfigurator 逐步完成 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
}
// 全部參數都只對 Module 內暴露 (Internal)

MyWKWebViewConfigurator (Builder Pattern)

這邊因為我只有 Build for MyWKWebView 的需求,因此沒有再把 MyWKWebViewConfigurator 多拆 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() {
        
    }
    
    // 參數封裝、內控
    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
    }
    
    // 可以把新增邏輯規則封裝在裡面
    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] = []
        // 產生時才附加
        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)
    }
}

多拆了一層也可以更好的使用 Access Control 隔離參數的使用權限,以本場景為例就是我們希望依然可以直接注入 WKUserScriptMyWKWebView 當中,但我們又不希望把開口開的這麼大讓使用的人可以隨意注入,因此結合 Builder Pattern + Swift Access Control,當 MyWKWebView 已經被放 Module 中後 MyWKWebViewConfigurator 對外封裝成操作方法 func set(disableZoom: Bool) ,對內在產生 MyWKWebViewConfiguration 時再附加上 WKUserScriptMyWKWebViewConfiguration 所有參數對外都是不可更改並且只能透過 MyWKWebViewConfigurator 產生。

MyWKWebViewConfigurator + Simple Factory 簡單工廠

當有了 MyWKWebViewConfigurator Builder 之後我們可以再建立一個簡單工廠封裝、復用建立步驟。

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 責任鏈模式

責任鏈模式(Chain of Responsibility Pattern)屬於 行為型 設計模式,它將對象處理的操作封裝並使用鏈式結構串聯起來,請求操作會沿著鏈條傳遞,直到有被處理為止;串聯的操作封裝可以自由彈性的組合、更改順序。

責任鏈專注在東西進來你有沒有要處理,沒有就 Skip ,因此不能處理一半或是修改了輸入物件然後丟給下一個;如果是這種需求那是另一個 Interceptor Pattern

上圖是以 Tech Support (or OnCall. . ) 為例,問題物件進來之後會先經過 CustomerService 如果他不能處理就往下一層 Supervisor 丟,如果還是不能處理再繼續往下到 TechSupport ;另外也可以針對不同問題組成不同的責任鏈,例如如果是大客戶的問題會直接從 Supervisor 開始處理;在 Swift UIKit 的 Responder Chain 也是使用了責任鏈模式,回應使用者在 UI 上的操作。

WKWebView 場景

在我們 WKWebView 的場景中,主要是套用在 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) 這個 Delegate 方法。

當系統收到網址請求時會經過這個方法讓我們決定是否要允許跳轉,並在結束處理後呼叫 decisionHandler(.allow) or decisionHandler(.cancel) 告知結果。

在 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
// 原始寫法...
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.allow)
            return
        }
        
        // 模擬商業邏輯:WebViewController deeplinkCheck == true (代表需要過 DeepLinkManager 檢查並開啟頁面)
        if deeplinkCheck {
            print("DeepLinkManager.open(\(url.absoluteString)")
            // 模擬 DeepLinkManager 邏輯,URL 能成功打開則打開並結束流程。
            // if DeepLinkManager.open(url) == true {
                decisionHandler(.cancel)
                return
            // }
        }
        
        // 模擬商業邏輯:WebViewController isHomePage == true (代表是開主頁) & WebView 正在瀏覽首頁,則切換 TabBar Index
        if isHomePage {
            if url.absoluteString == "https://zhgchg.li" {
                print("Switch UITabBarController to Index 0")
                decisionHandler(.cancel)
            }
        }
        
        // 模擬商業邏輯:WebViewController noNeedNativePresent == false (代表需要 Match 特殊路徑開原生頁面)
        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)
}

隨著時間推移功能越來越複雜,這邊的邏輯也會越來越多,如果又扯到處理順序也要不一樣就會變成一場災難。

先定義好 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)
        }
    }
}
  • 操作會在 func handle() 實現,如果有接下來處理則回傳 true 否則回傳 false
  • func exeute() 是預設的鏈訪問實現,會從這邊執行遍歷整個操作鏈,預設行為是當 func handle()false (代表此節點無法處理) 則自動呼叫下一個 nextHandlerexecute() 繼續處理,直到結束。

實現:

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
// 預設實現,通常放到最後
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
        }
        
        // 模擬商業邏輯:Payment 付款相關、兩階段驗證 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
        }
        
        
        // 模擬 DeepLinkManager 邏輯,URL 能成功打開則打開並結束流程。
        // 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)
    }
}

這樣當請求收到後,就會照著我們定義的處理鏈依序處理。

結合前面的 Builder Pattern MyWKWebViewConfigurator headNavigationActionHandler 開成參數出去,就能從外部決定這個 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:
            // 模擬預設情況有這些 handler
            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:
            // 模擬付款頁面只需要這些 handler,並且 paymentNavigationActionHandler 優先權最高
            let paymentNavigationActionHandler = PaymentNavigationActionHandler()
            let deplinkManagerNavigationActionHandler = DeeplinkManagerNavigationActionHandler()
            let defaultNavigationActionHandler = DefaultNavigationActionHandler()
            
            paymentNavigationActionHandler.nextHandler = deplinkManagerNavigationActionHandler
            deplinkManagerNavigationActionHandler.nextHandler = defaultNavigationActionHandler
            
            return MyWKWebViewConfigurator().set(headNavigationHandler: paymentNavigationActionHandler)
        }
    }
}

Strategy Pattern 策略模式

策略模式(Strategy Pattern)屬於 行為型 設計模式,它將實際操作抽象出來,我們可以實現多種不同的操作,讓外部可以根據不同場境彈性的替換使用。

上圖以不同支付方式為例,我們把支付抽象為 Payment Protocol (Interface),然後各種支付方式去實現自己的實作,在 PaymentContext (模擬外部使用)時 依據使用者選擇的付款方式,產生對應的 Payment 實體並統一呼叫 pay() 進行支付。

WKWebView 場景

在 WebView 與 前端頁面的交互中使用。

當前端 JavaScript 呼叫:

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

就會進到 WKWebView 找到對應 NameWKScriptMessageHandler Class 進入執行操作。

系統已經有定義好的 Protocol 跟相應的 func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String) 方法,我們只需要定義好自己的 WKScriptMessageHandler 實現,並加入到 WKWebView,系統就會依照 Strategy Pattern 策略模式,根據收到的 name 派發給對應的 具體策略 執行。

這邊只做簡單的 Protocol extend WKScriptMessageHandler ,多一個 identifier:String for add(.. name:) 使用:

1
2
3
public protocol ScriptMessageStrategy: NSObject, WKScriptMessageHandler {
    static var identifier: String { get }
}

實現:

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

WKWebView 註冊使用:

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

結合前面的 Builder Pattern MyWKWebViewConfigurator 從外部管理 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 {
    //...
    
    // 可以把新增邏輯規則封裝在裡面
    public func add(scriptMessageStrategy: ScriptMessageStrategy) -> Self {
        // 這邊只有實現重複 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: 這個場景也可以改用 Chain of Responsibility Pattern 責任鏈模式嗎?

到這邊有朋友可能會想問,那這邊的 Strategy Pattern 可以用 Chain of Responsibility Pattern 取代嗎?

這兩個設計模式同樣是行為型,可以取代;但實際要看需求場景,在這邊是很典型的 Strategy Pattern,WKWebView 依照 Name 去決定要進入的不同 Strategy;如果我們的需求是不同的 Strategy 之間可能有鏈式依賴或是 recover 關係,例如 AStrategy 如果不做要丟給 BStrategy 做,這時候才會考慮使用 Chain of Responsibility Pattern。

Strategy v.s. Chain of Responsibility

Strategy v.s. Chain of Responsibility

  • Strategy Pattern:已有明確派發執行策略且策略與策略之間沒有關係。
  • Chain of Responsibility Pattern:執行策略是在個別實現中決定,如果無法處理則往下丟給下一個實現。

複雜場景可以用 Strategy Pattern 裡面再套用 Chain of Responsibility Pattern 組合達成。

最終組合

  • Simple Factory 簡單工廠模式 MyWKWebViewConfiguratorFactory -> 封裝 MyWKWebViewConfigurator 產生步驟
  • Builder Pattern 建造者模式 MyWKWebViewConfigurator -> 封裝 MyWKWebViewConfiguration 參數、構建步驟
  • MyWKWebViewConfiguration 注入 -> 給 MyWKWebViewController 使用
  • Chain of Responsibility Pattern 責任鏈模式 MyWKWebViewControllerfunc webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> 呼叫 headNavigationHandler?.exeute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) 鏈執行處理
  • Strategy Pattern 策略模式 MyWKWebViewControllerwebView.configuration.userContentController.addUserScript(XXX) 派發對應的 JS Caller 到對應處理的策略中

完整 Demo Repo

===

View the English version of this article here.

本文首次發表於 Medium ➡️ 前往查看



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

遊記 2024 泰國曼谷 Bangkok 🇹🇭 5 日自由行

iOS ≥ 18 NSAttributedString attributes Range 合併的一個行為改變