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
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
- Setting variables and switches in the Class makes it unclear which ones are for configuration.
- Exposing WKUserScript variables directly to the outside, we want to control the injected JS and only allow injection of specific behaviors.
- Unable to control the registration rules of WKScriptMessageHandler.
- If you need to initialize a similar WebView, you need to repeatedly write the injection parameter rules, and the parameter rules cannot be reused.
- 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)
ordecisionHandler(.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.
NavigationActionHandler (Chain of Responsibility Pattern)
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()
, returningtrue
if there is further processing, otherwisefalse
. func exeute()
is the default chain access implementation, which will traverse the entire operation chain from here. The default behavior is that whenfunc handle()
returnsfalse
(indicating that this node cannot handle it), it automatically calls theexecute()
of the nextnextHandler
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 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 generateMyWKWebViewConfigurator
- Builder Pattern
MyWKWebViewConfigurator
-> EncapsulatesMyWKWebViewConfiguration
parameters and construction steps - Injection of
MyWKWebViewConfiguration
-> Used byMyWKWebViewController
- Chain of Responsibility Pattern
MyWKWebViewController
’sfunc webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
-> CallsheadNavigationHandler?.execute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
for chain execution handling - Strategy Pattern
MyWKWebViewController
’swebView.configuration.userContentController.addUserScript(XXX)
dispatches the corresponding JS Caller to the respective handling strategy.
Complete Demo Repo
Further Reading
- Record of Practical Applications of Design Patterns
- Visitor Pattern in Swift (Share Object to XXX Example)
- Visitor Pattern in TableView
If you have any questions or suggestions, feel free to contact me.
===
===
This article was first published in Traditional Chinese on Medium ➡️ View Here