Files
consentos/sdks/ios/ConsentOS/Sources/ConsentOSUI/ConsentModalController.swift
James Cottrill fbf26453f2 feat: initial public release
ConsentOS — a privacy-first cookie consent management platform.

Self-hosted, source-available alternative to OneTrust, Cookiebot, and
CookieYes. Full standards coverage (IAB TCF v2.2, GPP v1, Google
Consent Mode v2, GPC, Shopify Customer Privacy API), multi-tenant
architecture with role-based access, configuration cascade
(system → org → group → site → region), dark-pattern detection in
the scanner, and a tamper-evident consent record audit trail.

This is the initial public release. Prior development history is
retained internally.

See README.md for the feature list, architecture overview, and
quick-start instructions. Licensed under the Elastic Licence 2.0 —
self-host freely; do not resell as a managed service.
2026-04-14 09:18:18 +00:00

107 lines
3.3 KiB
Swift

#if canImport(UIKit)
import UIKit
import SwiftUI
import ConsentOSCore
// MARK: - Consent Modal Controller
/// A UIKit view controller that presents the SwiftUI ``ConsentBannerView`` as a
/// bottom-sheet modal overlay.
///
/// Use this when your app is primarily UIKit-based.
///
/// ```swift
/// let modal = ConsentModalController()
/// modal.onDismiss = { [weak self] in
/// // Banner dismissed
/// }
/// present(modal, animated: true)
/// ```
public final class ConsentModalController: UIViewController {
// MARK: - Properties
/// Called when the banner is dismissed (after any consent action).
public var onDismiss: (() -> Void)?
/// The theme to apply to the banner. Defaults to ``BannerTheme/defaultTheme``.
public var theme: BannerTheme = .defaultTheme
// MARK: - Private
private var hostingController: UIHostingController<AnyView>?
// MARK: - Lifecycle
public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.4)
setupBannerView()
setupBackgroundDismiss()
}
// MARK: - Setup
private func setupBannerView() {
let bannerView = ConsentBannerView(
theme: theme,
onAcceptAll: { [weak self] in
await ConsentOS.shared.acceptAll()
await MainActor.run { self?.dismiss(animated: true) }
},
onRejectAll: { [weak self] in
await ConsentOS.shared.rejectAll()
await MainActor.run { self?.dismiss(animated: true) }
},
onSave: { [weak self] categories in
await ConsentOS.shared.acceptCategories(categories)
await MainActor.run { self?.dismiss(animated: true) }
}
)
let hosting = UIHostingController(rootView: AnyView(
VStack {
Spacer()
bannerView
.padding(.bottom, 16)
}
))
hosting.view.backgroundColor = .clear
addChild(hosting)
view.addSubview(hosting.view)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: view.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
hosting.didMove(toParent: self)
self.hostingController = hosting
}
private func setupBackgroundDismiss() {
// Tapping the dim overlay does not dismiss users must interact with the banner.
// Remove this behaviour if your design requires tap-to-dismiss.
}
// MARK: - Dismiss Override
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
super.dismiss(animated: flag) { [weak self] in
self?.onDismiss?()
completion?()
}
}
// MARK: - Accessibility
public override func accessibilityPerformEscape() -> Bool {
// Do not allow VoiceOver escape gesture to dismiss the banner without a consent choice.
return false
}
}
#endif