diff --git a/Package.swift b/Package.swift index a3dac7e..5252909 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "StatusBarStylist", - platforms: [.iOS(.v14), .macOS(.v13)], + platforms: [.iOS(.v15), .macOS(.v13)], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( diff --git a/README.md b/README.md index aa73b28..4c0c7b8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # StatusBarStylist -StatusBarStylist is a framework to supports changing the Status Bar style with natural SwiftUI syntax. +StatusBarStylist is a framework to support changing the Status Bar style with natural SwiftUI syntax. -Thank [Philip Trauner](https://stackoverflow.com/a/71458976/10270556) for this source code and idea. +Thanks [@Thieurom](https://github.com/Thieurom) for this perfect solution and source code. 😍 ## Installation Requirements iOS 14+ @@ -20,23 +20,7 @@ Requirements iOS 14+ ## Usage -In your ```@main``` App file, simply wrap your main view in a ```RootView```. - -```swift -@main -struct ProjectApp: App { - var body: some Scene { - WindowGroup { - RootView { - ContentView() // Change your content here 👈 - } - } - } -} -``` - -### Example -Use the ```.statusBarStyle(_ style: UIStatusBarStyle)``` method on a View. +Use the ```.preferredStatusBarStyle(_ style: UIStatusBarStyle)``` method on a View. ```swift struct ContentView: View { var body: some View { @@ -45,14 +29,14 @@ struct ContentView: View { Color.black .edgesIgnoringSafeArea(.all) .overlay(Text("Light Status Bar").foregroundColor(.white)) - .statusBarStyle(.lightContent) // set status bar style here + .preferredStatusBarStyle(.lightContent) // set status bar style here .tabItem { Text("Tab 1") } // Tab 2 will have a dark status bar Color.white .edgesIgnoringSafeArea(.all) .overlay(Text("Dark Status Bar")) - .statusBarStyle(.darkContent) // set status bar style here + .preferredStatusBarStyle(.darkContent) // set status bar style here .tabItem { Text("Tab 2") } } } diff --git a/Sources/StatusBarStylist/Interposed.swift b/Sources/StatusBarStylist/Interposed.swift deleted file mode 100644 index 6d46040..0000000 --- a/Sources/StatusBarStylist/Interposed.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// Interposed.swift -// -// -// Created by MarkG on 18/01/2024. -// - -import SwiftUI - -enum Interposed { - - case pending - case successful - case failed -} - -struct InterposedKey: EnvironmentKey { - - static let defaultValue: Interposed = .pending -} - -extension EnvironmentValues { - - var interposed: Interposed { - get { self[InterposedKey.self] } - set { self[InterposedKey.self] = newValue } - } -} diff --git a/Sources/StatusBarStylist/StatusBarStylist.swift b/Sources/StatusBarStylist/StatusBarStylist.swift index a1f1f90..2e28b99 100644 --- a/Sources/StatusBarStylist/StatusBarStylist.swift +++ b/Sources/StatusBarStylist/StatusBarStylist.swift @@ -2,125 +2,93 @@ // https://docs.swift.org/swift-book import SwiftUI +import UIKit -extension UIApplication { - - var keyWindow: UIWindow? { - connectedScenes - .compactMap { $0 as? UIWindowScene } - .flatMap(\.windows) - .first { - $0.isKeyWindow - } - } -} +private struct StatusBarViewModifier: ViewModifier { -extension UIViewController { + // This `currentStatusBarStyle` is needed to make the modifier work when + // a view is presented as a `sheet` or `fullScreenCover`. + private var currentStatusBarStyle: UIStatusBarStyle? + private let preferredStatusBarStyle: UIStatusBarStyle + private var statusBarWindow: UIWindow? - enum Holder { - static var statusBarStyleStack: [UIStatusBarStyle] = .init() + private var statusBarViewController: StatusBarViewController? { + statusBarWindow?.rootViewController as? StatusBarViewController } - func interpose() -> Bool { - let sel1: Selector = #selector( - getter: preferredStatusBarStyle - ) - let sel2: Selector = #selector( - getter: preferredStatusBarStyleModified - ) + @SwiftUI.Environment(\.isPresented) + private var isPresented - let original = class_getInstanceMethod(Self.self, sel1) - let new = class_getInstanceMethod(Self.self, sel2) + init(style: UIStatusBarStyle) { + preferredStatusBarStyle = style - if let original = original, let new = new { - method_exchangeImplementations(original, new) - - return true - } - - return false + statusBarWindow = UIApplication.shared.statusBarWindow + currentStatusBarStyle = statusBarViewController?.statusBarStyle + statusBarViewController?.statusBarStyle = preferredStatusBarStyle } - @objc dynamic var preferredStatusBarStyleModified: UIStatusBarStyle { - Holder.statusBarStyleStack.last ?? .default + func body(content: Content) -> some View { + content + .onAppear { + statusBarViewController?.statusBarStyle = preferredStatusBarStyle + } + .onDisappear { + if isPresented, let currentStatusBarStyle { + statusBarViewController?.statusBarStyle = currentStatusBarStyle + } + } } } -struct StatusBarStyle: ViewModifier { +private class StatusBarViewController: UIViewController { - @Environment(\.interposed) private var interposed - - let statusBarStyle: UIStatusBarStyle - let animationDuration: TimeInterval - - private func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle) { - UIViewController.Holder.statusBarStyleStack.append(statusBarStyle) - - UIView.animate(withDuration: animationDuration) { - UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate() + var statusBarStyle: UIStatusBarStyle = .default { + didSet { + setNeedsStatusBarAppearanceUpdate() } } - func body(content: Content) -> some View { - content - .onAppear { - setStatusBarStyle(statusBarStyle) - } - .onChange(of: statusBarStyle) { - _ = UIViewController.Holder.statusBarStyleStack.removeLast() - setStatusBarStyle($0) - } - .onDisappear { - _ = UIViewController.Holder.statusBarStyleStack.removeLast() + override var preferredStatusBarStyle: UIStatusBarStyle { + statusBarStyle + } - UIView.animate(withDuration: animationDuration) { - UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate() - } - } - // Interposing might still be pending on initial render - .onChange(of: interposed) { _ in - UIView.animate(withDuration: animationDuration) { - UIApplication.shared.keyWindow?.rootViewController?.setNeedsStatusBarAppearanceUpdate() - } - } + init() { + super.init(nibName: nil, bundle: nil) + view.backgroundColor = .clear + view.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } } public extension View { - func statusBarStyle( - _ statusBarStyle: UIStatusBarStyle, - animationDuration: TimeInterval = 0.3 - ) -> some View { - modifier(StatusBarStyle(statusBarStyle: statusBarStyle, animationDuration: animationDuration)) + func preferredStatusBarStyle(_ style: UIStatusBarStyle) -> some View { + modifier(StatusBarViewModifier(style: style)) } } -public struct RootView: View where Content: View { - - @Environment(\.scenePhase) var scenePhase - @State var interposed: Interposed = .pending - @ViewBuilder let content: () -> Content - var interposeLock = NSLock() - - public var body: some View { - content() - .environment(\.interposed, interposed) - .onChange(of: scenePhase) { phase in - if case .active = phase { - interposeLock.lock() - if case .pending = interposed, - case true = UIApplication.shared.keyWindow?.rootViewController?.interpose() { - interposed = .successful - } else { - interposed = .failed - } - interposeLock.unlock() - } - } - } +extension UIApplication { - public init(@ViewBuilder content: @escaping () -> Content) { - self.content = content + private static let statusBarWindowTag = -9999 + + fileprivate var statusBarWindow: UIWindow? { + guard let windowScene = connectedScenes.first as? UIWindowScene else { + return nil + } + + if let window = windowScene.windows.first(where: { $0.tag == Self.statusBarWindowTag }) { + return window + } else { + let window = UIWindow(windowScene: windowScene) + window.windowLevel = .statusBar + window.tag = Self.statusBarWindowTag + window.isUserInteractionEnabled = false + window.rootViewController = StatusBarViewController() + window.isHidden = false + return window + } } }