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).
ℹ️ℹ️ℹ️ 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
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
- The variables and switches are scattered throughout the class, making it unclear which are for configuration.
- WKUserScript variable settings are directly exposed to the outside, whereas we want to control the injected JS and only allow specific behaviors.
- There is no control over the registration rules for WKScriptMessageHandler.
- If similar WebViews need to be initialized, the injection parameter rules must be rewritten, and the parameter rules cannot be reused.
- 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)
ordecisionHandler(.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.
NavigationActionHandler (Chain of Responsibility Pattern)
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, returntrue
; otherwise, returnfalse
. func execute()
is the default chain access implementation, which traverses the entire operation chain. The default behavior is that whenfunc handle()
returnsfalse
(indicating this node cannot handle it), it automatically calls the nextnextHandler
’sexecute()
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
’sWKScriptMessageHandler
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 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 createMyWKWebViewConfigurator
- Builder Pattern
MyWKWebViewConfigurator
-> Encapsulates the parameters and construction steps forMyWKWebViewConfiguration
MyWKWebViewConfiguration
is injected -> For use inMyWKWebViewController
- Chain of Responsibility Pattern in
MyWKWebViewController
’sfunc webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
-> CallsheadNavigationHandler?.execute(webView: webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
to chain execute processing - Strategy Pattern in
MyWKWebViewController
’swebView.configuration.userContentController.addUserScript(XXX)
dispatches the corresponding JS Caller to the appropriate handling strategy
Complete Demo Repo
Further Reading
- Practical Applications of Design Patterns
- Visitor Pattern in Swift (Share Object to XXX Example)
- Visitor Pattern in TableView
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.