From 673b25eb4c5e95759951fa84e307eef557493418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Destro?= Date: Mon, 19 Aug 2024 14:58:14 +0100 Subject: [PATCH] feat: implement overlay and background color in iOS --- .../CAPBridgeViewController.swift | 14 ++ .../StatusBarPlugin/CAPNotifications.swift | 6 + .../Sources/StatusBarPlugin/StatusBar.swift | 175 ++++++++++++++++++ .../StatusBarPlugin/StatusBarConfig.swift | 7 + .../StatusBarPlugin/StatusBarInfo.swift | 9 + .../StatusBarPlugin/StatusBarPlugin.swift | 113 +++++------ .../ios/Sources/StatusBarPlugin/UIColor.swift | 40 ++++ 7 files changed, 308 insertions(+), 56 deletions(-) create mode 100644 status-bar/ios/Sources/StatusBarPlugin/CAPBridgeViewController.swift create mode 100644 status-bar/ios/Sources/StatusBarPlugin/CAPNotifications.swift create mode 100644 status-bar/ios/Sources/StatusBarPlugin/StatusBar.swift create mode 100644 status-bar/ios/Sources/StatusBarPlugin/StatusBarConfig.swift create mode 100644 status-bar/ios/Sources/StatusBarPlugin/StatusBarInfo.swift create mode 100644 status-bar/ios/Sources/StatusBarPlugin/UIColor.swift diff --git a/status-bar/ios/Sources/StatusBarPlugin/CAPBridgeViewController.swift b/status-bar/ios/Sources/StatusBarPlugin/CAPBridgeViewController.swift new file mode 100644 index 000000000..0786977bf --- /dev/null +++ b/status-bar/ios/Sources/StatusBarPlugin/CAPBridgeViewController.swift @@ -0,0 +1,14 @@ +import Capacitor + +extension CAPBridgeViewController { + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + NotificationCenter.default.post(Notification(name: .capacitorViewDidAppear)) + } + + open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + NotificationCenter.default.post(Notification(name: .capacitorViewWillTransition)) + } +} diff --git a/status-bar/ios/Sources/StatusBarPlugin/CAPNotifications.swift b/status-bar/ios/Sources/StatusBarPlugin/CAPNotifications.swift new file mode 100644 index 000000000..64a937fe9 --- /dev/null +++ b/status-bar/ios/Sources/StatusBarPlugin/CAPNotifications.swift @@ -0,0 +1,6 @@ +import Capacitor + +extension Notification.Name { + public static let capacitorViewDidAppear = Notification.Name(rawValue: "CapacitorViewDidAppear") + public static let capacitorViewWillTransition = Notification.Name(rawValue: "CapacitorViewWillTransition") +} diff --git a/status-bar/ios/Sources/StatusBarPlugin/StatusBar.swift b/status-bar/ios/Sources/StatusBarPlugin/StatusBar.swift new file mode 100644 index 000000000..6080b8094 --- /dev/null +++ b/status-bar/ios/Sources/StatusBarPlugin/StatusBar.swift @@ -0,0 +1,175 @@ +import Foundation +import Capacitor + +public extension Notification.Name { + static let statusBarVisibilityChanged = Notification.Name("statusBarVisibilityChanged") + static let statusBarOverlayChanged = Notification.Name("statusBarOverlayChanged") +} + +@objc public class StatusBar: NSObject { + + private var bridge: CAPBridgeProtocol + private var isOverlayingWebview = true + private var backgroundColor = UIColor.black + private var backgroundView: UIView? + private var observers: [NSObjectProtocol] = [] + + init(bridge: CAPBridgeProtocol, config: StatusBarConfig) { + self.bridge = bridge + super.init() + setupObservers(with: config) + } + + deinit { + observers.forEach { NotificationCenter.default.removeObserver($0) } + } + + private func setupObservers(with config: StatusBarConfig) { + observers.append(NotificationCenter.default.addObserver(forName: .capacitorViewDidAppear, object: .none, queue: .none) { [weak self] _ in + self?.handleViewDidAppear(config: config) + }) + observers.append(NotificationCenter.default.addObserver(forName: .capacitorStatusBarTapped, object: .none, queue: .none) { [weak self] _ in + self?.bridge.triggerJSEvent(eventName: "statusTap", target: "window") + }) + observers.append(NotificationCenter.default.addObserver(forName: .capacitorViewWillTransition, object: .none, queue: .none) { [weak self] _ in + self?.handleViewWillTransition() + }) + } + + private func handleViewDidAppear(config: StatusBarConfig) { + setStyle(config.style) + setBackgroundColor(config.backgroundColor) + setOverlaysWebView(config.overlaysWebView) + } + + private func handleViewWillTransition() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.resizeStatusBarBackgroundView() + self?.resizeWebView() + } + } + + func setStyle(_ style: UIStatusBarStyle) { + bridge.statusBarStyle = style + } + + func setBackgroundColor(_ color : UIColor) { + backgroundColor = color + backgroundView?.backgroundColor = color + } + + func setAnimation(_ animation: String) { + if animation == "SLIDE" { + bridge.statusBarAnimation = .slide + } else if animation == "NONE" { + bridge.statusBarAnimation = .none + } else { + bridge.statusBarAnimation = .fade + } + } + + func hide(animation: String) { + setAnimation(animation) + if bridge.statusBarVisible { + bridge.statusBarVisible = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.resizeWebView() + self?.backgroundView?.removeFromSuperview() + self?.backgroundView?.isHidden = true + } + NotificationCenter.default.post(name: .statusBarVisibilityChanged, object: getInfo()) + } + } + + func show(animation: String) { + setAnimation(animation) + if !bridge.statusBarVisible { + bridge.statusBarVisible = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [self] in + resizeWebView() + if !isOverlayingWebview { + resizeStatusBarBackgroundView() + bridge.webView?.superview?.addSubview(backgroundView!) + } + backgroundView?.isHidden = false + } + NotificationCenter.default.post(name: .statusBarVisibilityChanged, object: getInfo()) + } + } + + func getInfo() -> StatusBarInfo { + let style: String + switch bridge.statusBarStyle { + case .default: + style = "DEFAULT" + case .lightContent: + style = "DARK" + case .darkContent: + style = "LIGHT" + @unknown default: + style = "DEFAULT" + } + + return StatusBarInfo( + overlays: isOverlayingWebview, + visible: bridge.statusBarVisible, + style: style, + color: UIColor.capacitor.hex(fromColor: backgroundColor), + height: getStatusBarFrame().size.height + ) + } + + func setOverlaysWebView(_ overlay: Bool) { + if overlay == isOverlayingWebview { return } + isOverlayingWebview = overlay + if overlay { + backgroundView?.removeFromSuperview() + } else { + initializeBackgroundViewIfNeeded() + bridge.webView?.superview?.addSubview(backgroundView!) + } + resizeWebView() + NotificationCenter.default.post(name: .statusBarOverlayChanged, object: getInfo()) + } + + private func resizeWebView() { + guard + let webView = bridge.webView, + let bounds = bridge.viewController?.view.window?.windowScene?.screen.bounds + else { return } + bridge.viewController?.view.frame = bounds + webView.frame = bounds + let statusBarHeight = getStatusBarFrame().size.height; + var webViewFrame = webView.frame; + + if isOverlayingWebview { + let safeAreaTop = webView.safeAreaInsets.top; + if (statusBarHeight >= safeAreaTop && safeAreaTop > 0) { + webViewFrame.origin.y = safeAreaTop == 40 ? 20 : statusBarHeight - safeAreaTop + } else { + webViewFrame.origin.y = 0 + } + } else { + webViewFrame.origin.y = statusBarHeight; + } + webViewFrame.size.height -= webViewFrame.origin.y + webView.frame = webViewFrame + } + + private func resizeStatusBarBackgroundView() { + backgroundView?.frame = getStatusBarFrame() + } + + private func getStatusBarFrame() -> CGRect { + return UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.windowScene?.statusBarManager?.statusBarFrame ?? .zero + } + + private func initializeBackgroundViewIfNeeded() { + if backgroundView == nil { + backgroundView = UIView(frame: getStatusBarFrame()) + backgroundView!.backgroundColor = backgroundColor + backgroundView!.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin] + backgroundView!.isHidden = !bridge.statusBarVisible + } + } +} diff --git a/status-bar/ios/Sources/StatusBarPlugin/StatusBarConfig.swift b/status-bar/ios/Sources/StatusBarPlugin/StatusBarConfig.swift new file mode 100644 index 000000000..7e0bc4c53 --- /dev/null +++ b/status-bar/ios/Sources/StatusBarPlugin/StatusBarConfig.swift @@ -0,0 +1,7 @@ +import UIKit + +public struct StatusBarConfig { + var overlaysWebView = true + var backgroundColor: UIColor = .black + var style: UIStatusBarStyle = .default +} diff --git a/status-bar/ios/Sources/StatusBarPlugin/StatusBarInfo.swift b/status-bar/ios/Sources/StatusBarPlugin/StatusBarInfo.swift new file mode 100644 index 000000000..7e1b8b1a7 --- /dev/null +++ b/status-bar/ios/Sources/StatusBarPlugin/StatusBarInfo.swift @@ -0,0 +1,9 @@ +import Foundation + +public struct StatusBarInfo { + public var overlays: Bool? + public var visible: Bool? + public var style: String? + public var color: String? + public var height: CGFloat? +} diff --git a/status-bar/ios/Sources/StatusBarPlugin/StatusBarPlugin.swift b/status-bar/ios/Sources/StatusBarPlugin/StatusBarPlugin.swift index 6634d064c..9c0a4ad64 100644 --- a/status-bar/ios/Sources/StatusBarPlugin/StatusBarPlugin.swift +++ b/status-bar/ios/Sources/StatusBarPlugin/StatusBarPlugin.swift @@ -15,92 +15,93 @@ public class StatusBarPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "show", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "hide", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getInfo", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "setOverlaysWebView", returnType: CAPPluginReturnPromise) + CAPPluginMethod(name: "setOverlaysWebView", returnType: CAPPluginReturnPromise), ] - private var observer: NSObjectProtocol? - + private var statusBar: StatusBar? + override public func load() { - observer = NotificationCenter.default.addObserver(forName: Notification.Name.capacitorStatusBarTapped, object: .none, queue: .none) { [weak self] _ in - self?.bridge?.triggerJSEvent(eventName: "statusTap", target: "window") + guard let bridge = bridge else { return } + statusBar = StatusBar(bridge: bridge, config: statusBarConfig()) + } + + private func statusBarConfig() -> StatusBarConfig { + var config = StatusBarConfig() + config.overlaysWebView = getConfig().getBoolean("StatusBarOverlaysWebView", config.overlaysWebView) + if let colorConfig = getConfig().getString("StatusBarBackgroundColor"), let color = UIColor.capacitor.color(fromHex: colorConfig) + { + config.backgroundColor = color + } + if let configStyle = getConfig().getString("StatusBarStyle") { + config.style = style(fromString: configStyle) } + return config } - - deinit { - if let observer = observer { - NotificationCenter.default.removeObserver(observer) + + private func style(fromString: String) -> UIStatusBarStyle { + switch fromString.lowercased() { + case "dark", "lightcontent": + return .lightContent + case "light", "darkcontent": + return .darkContent + case "default": + return .default + default: + return .default } } @objc func setStyle(_ call: CAPPluginCall) { let options = call.options! - - if let style = options["style"] as? String { - if style == "DARK" { - bridge?.statusBarStyle = .lightContent - } else if style == "LIGHT" { - bridge?.statusBarStyle = .darkContent - } else if style == "DEFAULT" { - bridge?.statusBarStyle = .default - } + if let styleString = options["style"] as? String { + statusBar?.setStyle(style(fromString: styleString)) } - call.resolve([:]) } @objc func setBackgroundColor(_ call: CAPPluginCall) { - call.unimplemented() - } - - func setAnimation(_ call: CAPPluginCall) { - let animation = call.getString("animation", "FADE") - if animation == "SLIDE" { - bridge?.statusBarAnimation = .slide - } else if animation == "NONE" { - bridge?.statusBarAnimation = .none - } else { - bridge?.statusBarAnimation = .fade + guard + let hexString = call.options["color"] as? String, + let color = UIColor.capacitor.color(fromHex: hexString) + else { return } + DispatchQueue.main.async { [weak self] in + self?.statusBar?.setBackgroundColor(color) } + call.resolve() } - + @objc func hide(_ call: CAPPluginCall) { - setAnimation(call) - bridge?.statusBarVisible = false + let animation = call.getString("animation", "FADE") + DispatchQueue.main.async { [weak self] in + self?.statusBar?.hide(animation: animation) + } call.resolve() } @objc func show(_ call: CAPPluginCall) { - setAnimation(call) - bridge?.statusBarVisible = true + let animation = call.getString("animation", "FADE") + DispatchQueue.main.async { [weak self] in + self?.statusBar?.show(animation: animation) + } call.resolve() } - + @objc func getInfo(_ call: CAPPluginCall) { DispatchQueue.main.async { [weak self] in - guard let bridge = self?.bridge else { - return - } - let style: String - switch bridge.statusBarStyle { - case .default: - if bridge.userInterfaceStyle == UIUserInterfaceStyle.dark { - style = "DARK" - } else { - style = "LIGHT" - } - case .lightContent: - style = "DARK" - default: - style = "LIGHT" - } - + guard let info = self?.statusBar?.getInfo() else { return } call.resolve([ - "visible": bridge.statusBarVisible, - "style": style + "visible": info.visible!, + "style": info.style!, + "color": info.color!, + "overlays": info.overlays! ]) } } @objc func setOverlaysWebView(_ call: CAPPluginCall) { - call.unimplemented() + guard let overlay = call.options["overlay"] as? Bool else { return } + DispatchQueue.main.async { [weak self] in + self?.statusBar?.setOverlaysWebView(overlay) + } + call.resolve() } } diff --git a/status-bar/ios/Sources/StatusBarPlugin/UIColor.swift b/status-bar/ios/Sources/StatusBarPlugin/UIColor.swift new file mode 100644 index 000000000..584afcc4f --- /dev/null +++ b/status-bar/ios/Sources/StatusBarPlugin/UIColor.swift @@ -0,0 +1,40 @@ +import Capacitor + +public extension CapacitorExtensionTypeWrapper where T: UIColor { + + static func hex(fromColor: UIColor) -> String? { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + guard fromColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { + assertionFailure("Failed to get RGBA components from UIColor") + return nil + } + + red = max(0, min(1, red)) + green = max(0, min(1, green)) + blue = max(0, min(1, blue)) + alpha = max(0, min(1, alpha)) + + if alpha == 1 { + // RGB + return String( + format: "#%02lX%02lX%02lX", + Int(round(red * 255)), + Int(round(green * 255)), + Int(round(blue * 255)) + ) + } else { + // RGBA + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(round(red * 255)), + Int(round(green * 255)), + Int(round(blue * 255)), + Int(round(alpha * 255)) + ) + } + } +}