Skip to content

Commit c6fff5d

Browse files
Merge branch 'webView-customization'
* webView-customization: Updated REAMDE file with "WebView consent synchronization" article. Added cleanup method for safely releasing webview resources and detach handlers. CR updates: provided read-only behavior for custom webView, removed navigation block for custom webView, refactored WebViewController. JS method for dismissing custom WebView was renamed to closeCustomWebView Added closeWebView method to forcibly close custom webView and it's JS analog, removed default autodismiss on "ready" call, Added webViewLoadUrl method for custom WebView appearance.
2 parents 1eb9e4a + 95b3a05 commit c6fff5d

5 files changed

Lines changed: 364 additions & 62 deletions

File tree

ClickioConsentSDKManager.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
C41A0DF82D88B29A00519174 /* AirBridgeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A0DF72D88B29A00519174 /* AirBridgeChecker.swift */; };
1313
C41A0DFB2D88B63E00519174 /* AppsFlyerChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41A0DFA2D88B63E00519174 /* AppsFlyerChecker.swift */; };
1414
C4325CAD2D9AB7D500F72035 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = C4325CAC2D9AB7D500F72035 /* PrivacyInfo.xcprivacy */; };
15+
C46D06A42E2FFBE000080C3B /* WebViewConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46D06A32E2FFBE000080C3B /* WebViewConfig.swift */; };
1516
C4914DD62DD6298800466C25 /* NetworkStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4914DD52DD6298800466C25 /* NetworkStatusChecker.swift */; };
1617
C4CF27102D83396E008E8BA2 /* WebViewManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CF270D2D83396E008E8BA2 /* WebViewManager.swift */; };
1718
C4CF27112D83396E008E8BA2 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CF270B2D83396E008E8BA2 /* WebViewController.swift */; };
@@ -29,6 +30,7 @@
2930
C41A0DF72D88B29A00519174 /* AirBridgeChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirBridgeChecker.swift; sourceTree = "<group>"; };
3031
C41A0DFA2D88B63E00519174 /* AppsFlyerChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsFlyerChecker.swift; sourceTree = "<group>"; };
3132
C4325CAC2D9AB7D500F72035 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
33+
C46D06A32E2FFBE000080C3B /* WebViewConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewConfig.swift; sourceTree = "<group>"; };
3234
C4914DD52DD6298800466C25 /* NetworkStatusChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStatusChecker.swift; sourceTree = "<group>"; };
3335
C4CF26F02D8338FE008E8BA2 /* ClickioConsentSDKManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ClickioConsentSDKManager.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3436
C4CF26FE2D83396E008E8BA2 /* ATTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ATTManager.swift; sourceTree = "<group>"; };
@@ -96,6 +98,14 @@
9698
path = AppsFlyerChecker;
9799
sourceTree = "<group>";
98100
};
101+
C46D06A22E2FFBD000080C3B /* WebViewConfig */ = {
102+
isa = PBXGroup;
103+
children = (
104+
C46D06A32E2FFBE000080C3B /* WebViewConfig.swift */,
105+
);
106+
path = WebViewConfig;
107+
sourceTree = "<group>";
108+
};
99109
C4914DD42DD6297700466C25 /* NetworkStatusChecker */ = {
100110
isa = PBXGroup;
101111
children = (
@@ -211,6 +221,7 @@
211221
C4CF270F2D83396E008E8BA2 /* WebView */ = {
212222
isa = PBXGroup;
213223
children = (
224+
C46D06A22E2FFBD000080C3B /* WebViewConfig */,
214225
C4CF270C2D83396E008E8BA2 /* WebViewController */,
215226
C4CF270E2D83396E008E8BA2 /* WebViewManager */,
216227
);
@@ -312,6 +323,7 @@
312323
C4CF27152D83396E008E8BA2 /* ATTManager.swift in Sources */,
313324
C4CF27162D83396E008E8BA2 /* EventLogger.swift in Sources */,
314325
C4CF27172D83396E008E8BA2 /* ExportData.swift in Sources */,
326+
C46D06A42E2FFBE000080C3B /* WebViewConfig.swift in Sources */,
315327
);
316328
runOnlyForDeploymentPostprocessing = 0;
317329
};

README.md

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,6 @@ Analytics.setConsent(consentSettings)
472472

473473
# Delaying Google Mobile Ads display until ATT and User Consent
474474

475-
476-
477475
Sometimes you need to ensure that both Apple's App Tracking Transparency prompt and user consent decision have been recorded before initializing and loading Google Mobile Ads. To implement this flow:
478476

479477
1. **Wait for ATT authorization and CMP readiness**
@@ -533,3 +531,158 @@ private func tryStartAdsIfAllowed() {
533531
adsStarted = true
534532
}
535533
```
534+
535+
536+
# WebView consent synchronization
537+
538+
### Overview
539+
When an iOS app contains both native UI (built with Swift/Objective-C) and web content inside a WKWebView (for example, embedded pages or widgets), there is a risk the Consent Management Platform (CMP) dialog will appear twice — once from the native SDK and again inside the web page.
540+
541+
The `webViewLoadUrl(urlString:config:)` method helps synchronize consent between the native and web layers. It configures a WKWebView (injecting a small clickioSDK JS bridge and message handler), ensures the web page can read/write the same saved consent state, and returns a ready-to-use WebViewController for your integration.
542+
543+
It does not present the controller for you — **the host app is responsible for how to show and hide it** (present (modally or not), add as a child/overlay, or embed as a platform view in Flutter/React-Native) **and for cleaning it up when it’s no longer needed**.
544+
545+
#### Tip: use `webViewLoadUrl` whenever you need to display web content that must respect the user’s consent already stored natively.
546+
547+
```swift
548+
func webViewLoadUrl(
549+
urlString: String, // accepts your webView's URL
550+
config: WebViewConfig = WebViewConfig() // accepts config object that describes your WebView's parameters: backgroundColor, width, height, gravity.
551+
) -> WebViewController
552+
```
553+
554+
### Configuration
555+
```swift
556+
public struct WebViewConfig {
557+
public var backgroundColor: UIColor = .clear // default value - .clear
558+
public var width: CGFloat? // nil = full width
559+
public var height: CGFloat? // nil = full height
560+
public var gravity: WebViewGravity = .center // default value - .center
561+
}
562+
563+
public enum WebViewGravity {
564+
case top, center, bottom
565+
}
566+
```
567+
568+
### Intregration examples
569+
570+
#### UIKit example
571+
```swift
572+
// WebViewController set-up
573+
let config = WebViewConfig(
574+
backgroundColor: .cyan,
575+
width: 280,
576+
height: 280,
577+
gravity: .center
578+
)
579+
580+
let webVC = ClickioConsentSDK.shared.webViewLoadUrl(
581+
urlString: "https://example.com", // your's webView URL
582+
config: config
583+
)
584+
585+
// Embed webVC as a child view controller and insert its view into this controller's view hierarchy.
586+
// Call didMove(toParent:) to finish the containment handshake so the child is fully active.
587+
addChild(webVC)
588+
view.addSubview(webVC.view)
589+
webVC.didMove(toParent: self)
590+
591+
addChild(webVC)
592+
view.addSubview(webVC.view)
593+
webVC.didMove(toParent: self)
594+
595+
// Constraints set-up
596+
webVC.view.translatesAutoresizingMaskIntoConstraints = false
597+
NSLayoutConstraint.activate([
598+
webVC.view.widthAnchor.constraint(equalToConstant: 280),
599+
webVC.view.heightAnchor.constraint(equalToConstant: 280),
600+
webVC.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
601+
webVC.view.centerYAnchor.constraint(equalTo: view.centerYAnchor)
602+
])
603+
```
604+
605+
#### When removing, don't forget to call the appropriate methods and clean up delegates/handlers through SDK's `cleanup` method that safely releases webview resources and detach handlers:
606+
```
607+
webVC.willMove(toParent: nil)
608+
webVC.cleanup() // cleanup the resources
609+
webVC.view.removeFromSuperview()
610+
webVC.removeFromParent()
611+
```
612+
613+
#### SwiftUI example
614+
Create a `UIViewControllerRepresentable` wrapper that returns the WebViewController produced by `ClickioConsentSDK.shared.webViewLoadUrl(...)`.
615+
```swift
616+
// MARK: - UIViewControllerRepresentable wrapper
617+
struct CustomWebViewControllerRepresentable: UIViewControllerRepresentable {
618+
let urlString: String // your webView's URL
619+
let config: WebViewConfig
620+
621+
// returns the prepared WebViewController from webViewLoadUrl
622+
func makeUIViewController(context: Context) -> WebViewController {
623+
// Create WebViewController through the SDK
624+
let webVC = ClickioConsentSDK.shared.webViewLoadUrl(
625+
urlString: urlString,
626+
config: config
627+
)
628+
return webVC
629+
}
630+
631+
func updateUIViewController(_ uiViewController: WebViewController, context: Context) { }
632+
633+
static func dismantleUIViewController(_ uiViewController: WebViewController, coordinator: ()) {
634+
DispatchQueue.main.async {
635+
// Best practice: call a cleanup() method on the controller that cancels loads,
636+
// clears delegates and removes script message handlers.
637+
uiViewController.cleanup()
638+
}
639+
}
640+
}
641+
```
642+
#### Note: To close the overlay, either (a) add an on-screen close button that sets the flag to false, or (b) provide an SDK callback/registration so the web content can request closing (recommended if you need web-driven close).
643+
#### When removed, SwiftUI calls `dismantleUIViewController`, so call `webVC.cleanup()` there — stop loading, nil delegates, and remove the script message handler.
644+
645+
Use a @State flag (for example showCustomWeb) to show/hide the wrapper in your view tree (sheet, overlay, inline, etc.).
646+
```swift
647+
@State private var showCustomWeb = false
648+
```
649+
650+
Show / hide in SwiftUI (overlay example):
651+
```swift
652+
var body: some View {
653+
ZStack {
654+
// your main UI...
655+
656+
if showCustomWeb {
657+
ZStack(alignment: .topTrailing) {
658+
// The representable wrapper that embeds the native WebViewController.
659+
CustomWebViewControllerRepresentable(
660+
urlString: "https://example.com", // your webView's URL
661+
config: WebViewConfig(
662+
backgroundColor: UIColor.cyan,
663+
width: 280,
664+
height: 280,
665+
gravity: .center
666+
)
667+
)
668+
.frame(width: 280, height: 280)
669+
.cornerRadius(12)
670+
.shadow(radius: 6)
671+
672+
// OPTIONAL: Add a close button overlay that simply flips the SwiftUI state flag.
673+
// When `showCustomWeb` becomes false, SwiftUI removes the representable and calls dismantle().
674+
Button(action: { showCustomWeb = false }) {
675+
Image(systemName: "xmark.circle.fill")
676+
.resizable()
677+
.frame(width: 28, height: 28)
678+
.foregroundColor(.black)
679+
}
680+
.padding()
681+
.offset(x: 8, y: -8)
682+
}
683+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
684+
.zIndex(1)
685+
}
686+
}
687+
}
688+
```

Sources/ClickioConsentSDKClass/ClickioConsentSDK.swift

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ import Combine
152152
* - Parameter language: optional, two-letter language code (e.g. en) - forces UI language.
153153
* - Parameter in parentViewController: optional, defines view controller on which WebView should be presented.
154154
* - Parameter attNeeded: `true` if ATT is necessary.
155-
* - language: An optional parameter to force the UI language.
156155
*/
157156
public func openDialog(
158157
mode: DialogMode = .default,
@@ -213,7 +212,6 @@ import Combine
213212
logger.log("Showing ATT dialog first, then displaying CMP in resurface mode only if ATT consent is granted", level: .info)
214213
ATTManager.shared.requestPermission { isGranted in
215214
if isGranted {
216-
// MARK: - ОТРАБОТАЛО!!!
217215
self.showResurfaceDialog(
218216
mode: mode,
219217
in: presentingVC,
@@ -226,7 +224,7 @@ import Combine
226224
}
227225
} else {
228226
logger.log("Bypassing ATT flow as not required and showing CMP in resurface mode", level: .info)
229-
self.showResurfaceDialog(
227+
showResurfaceDialog(
230228
mode: mode,
231229
in: presentingVC,
232230
language: language,
@@ -477,6 +475,28 @@ extension ClickioConsentSDK {
477475
}
478476
}
479477

478+
// MARK: - Custom WebView manipulations
479+
public extension ClickioConsentSDK {
480+
/**
481+
* Returns a custom WebViewController with provided URL and layout config.
482+
* - Parameter urlString: webView URL.
483+
* - Parameter config: config object that describes WebView parameters: backgroundColor, width, height, gravity.
484+
*/
485+
func webViewLoadUrl(
486+
urlString: String,
487+
config: WebViewConfig = WebViewConfig()
488+
) -> WebViewController {
489+
guard let url = URL(string: urlString) else {
490+
fatalError("Invalid URL: \(urlString)")
491+
}
492+
493+
let webViewController = WebViewController()
494+
webViewController.url = url
495+
webViewController.customConfig = config
496+
return webViewController
497+
}
498+
}
499+
480500
// MARK: - Enums
481501
extension ClickioConsentSDK {
482502
// MARK: ConsentState
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// WebViewConfig.swift
3+
// ClickioConsentSDKManager
4+
//
5+
6+
import UIKit
7+
8+
// MARK: - WebViewConfig
9+
public struct WebViewConfig {
10+
// MARK: Properties
11+
public var backgroundColor: UIColor = .clear
12+
public var width: CGFloat? // nil = full width
13+
public var height: CGFloat? // nil = full height
14+
public var gravity: WebViewGravity = .center
15+
16+
// MARK: Init
17+
public init(
18+
backgroundColor: UIColor = .clear,
19+
width: CGFloat? = nil,
20+
height: CGFloat? = nil,
21+
gravity: WebViewGravity = .center
22+
) {
23+
self.backgroundColor = backgroundColor
24+
self.width = width
25+
self.height = height
26+
self.gravity = gravity
27+
}
28+
}
29+
30+
// MARK: - WebViewGravity
31+
public enum WebViewGravity {
32+
case top, center, bottom
33+
}

0 commit comments

Comments
 (0)