-
-
Save Amzd/01e1f69ecbc4c82c8586dcd292b1d30d to your computer and use it in GitHub Desktop.
extension View { | |
/// Controls the application's preferred home indicator auto-hiding when this view is shown. | |
func prefersHomeIndicatorAutoHidden(_ value: Bool) -> some View { | |
preference(key: PreferenceUIHostingController.PrefersHomeIndicatorAutoHiddenPreferenceKey.self, value: value) | |
} | |
/// Controls the application's preferred screen edges deferring system gestures when this view is shown. Default is UIRectEdgeNone. | |
func edgesDeferringSystemGestures(_ edge: UIRectEdge) -> some View { | |
preference(key: PreferenceUIHostingController.PreferredScreenEdgesDeferringSystemGesturesPreferenceKey.self, value: edge) | |
} | |
} | |
class PreferenceUIHostingController: UIHostingController<AnyView> { | |
init<V: View>(wrappedView: V) { | |
weak var weakSelf: PreferenceUIHostingController? | |
super.init(rootView: AnyView(wrappedView | |
.onPreferenceChange(PrefersHomeIndicatorAutoHiddenPreferenceKey.self) { | |
weakSelf?._prefersHomeIndicatorAutoHidden = $0 | |
} | |
.onPreferenceChange(PreferredScreenEdgesDeferringSystemGesturesPreferenceKey.self) { | |
weakSelf?._preferredScreenEdgesDeferringSystemGestures = $0 | |
} | |
)) | |
weakSelf = self | |
} | |
@objc required dynamic init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
// MARK: Prefers Home Indicator Auto Hidden | |
fileprivate struct PrefersHomeIndicatorAutoHiddenPreferenceKey: PreferenceKey { | |
typealias Value = Bool | |
static var defaultValue: Value = false | |
static func reduce(value: inout Value, nextValue: () -> Value) { | |
value = nextValue() || value | |
} | |
} | |
private var _prefersHomeIndicatorAutoHidden = false { | |
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() } | |
} | |
override var prefersHomeIndicatorAutoHidden: Bool { | |
_prefersHomeIndicatorAutoHidden | |
} | |
// MARK: Preferred Screen Edges Deferring SystemGestures | |
fileprivate struct PreferredScreenEdgesDeferringSystemGesturesPreferenceKey: PreferenceKey { | |
typealias Value = UIRectEdge | |
static var defaultValue: Value = [] | |
static func reduce(value: inout Value, nextValue: () -> Value) { | |
value.formUnion(nextValue()) | |
} | |
} | |
private var _preferredScreenEdgesDeferringSystemGestures: UIRectEdge = [] { | |
didSet { setNeedsUpdateOfScreenEdgesDeferringSystemGestures() } | |
} | |
override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { | |
_preferredScreenEdgesDeferringSystemGestures | |
} | |
} |
/// If you are unable to access window.rootViewController this is a method using swizzling | |
struct PreferenceUIHostingControllerView<Wrapped: View>: UIViewControllerRepresentable { | |
init(@ViewBuilder wrappedView: @escaping () -> Wrapped) { | |
_ = UIViewController.preferenceSwizzling | |
self.wrappedView = wrappedView | |
} | |
var wrappedView: () -> Wrapped | |
func makeUIViewController(context: Context) -> PreferenceUIHostingController { | |
PreferenceUIHostingController(wrappedView: wrappedView()) | |
} | |
func updateUIViewController(_ uiViewController: PreferenceUIHostingController, context: Context) {} | |
} | |
import SwizzleSwift // I have a fork of this for SPM (Amzd/SwizzleSwift) | |
extension UIViewController { | |
static var preferenceSwizzling: Void = { | |
Swizzle(UIViewController.self) { | |
#selector(getter: childForScreenEdgesDeferringSystemGestures) <-> #selector(childForScreenEdgesDeferringSystemGestures_Amzd) | |
#selector(getter: childForHomeIndicatorAutoHidden) <-> #selector(childForHomeIndicatorAutoHidden_Amzd) | |
} | |
}() | |
} | |
extension UIViewController { | |
@objc func childForScreenEdgesDeferringSystemGestures_Amzd() -> UIViewController? { | |
if self is PreferenceUIHostingController { | |
// dont continue searching | |
return nil | |
} else { | |
return search() | |
} | |
} | |
@objc func childForHomeIndicatorAutoHidden_Amzd() -> UIViewController? { | |
if self is PreferenceUIHostingController { | |
// dont continue searching | |
return nil | |
} else { | |
return search() | |
} | |
} | |
private func search() -> PreferenceUIHostingController? { | |
if let result = children.compactMap({ $0 as? PreferenceUIHostingController }).first { | |
return result | |
} | |
for child in children { | |
if let result = child.search() { | |
return result | |
} | |
} | |
return nil | |
} | |
} |
Can't seem to work for me either on SwiftUI. It compiles fine but nothing happens. Any help would be great thanks !
Did you guys replace UIHostingController with PreferenceUIHostingController in the SceneDelegate/AppDelegate? @lezone @amrmarzouk
Hi, does this work with SwiftUI 2.0? Having trouble getting it to go. Likely operator error but thought I'd check.
Can't seem to work for me either on SwiftUI. It compiles fine but nothing happens. Any help would be great thanks !
Did you do this in your SceneDelegate? The functions return a new SwiftUI View, so you can't use them inside the view itself
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView().prefersHomeIndicatorAutoHidden(true)
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = PreferenceUIHostingController(wrappedView: contentView)
self.window = window
window.makeKeyAndVisible()
}
@NoahKamara you CAN use them inside your views? It’s just a modifier like any other modifier
@NoahKamara you CAN use them inside your views? It’s just a modifier like any other modifier
In that case it unfortunately doesn't work for me. Do you have a working example for me to take a look at?
struct Example: View {
@State var deferSystemGestures = false
var body: some View {
VStack {
Toggle("Toggle System Gesture", isOn: $deferSystemGestures)
}.edgesDeferringSystemGestures(deferSystemGestures ? .all : [])
}
}
Just confirmed this works on iOS 14 too.
Thanks, works great if I have a SceneDelegate. Is it possible to make it work with @UIApplicationDelegateAdaptor?
I have the same issue as @patrick ^. I am using the new SwiftUI App Life Cycle, so no Scene/App Delegates.
I think I got it to work.
MainApp.swift
import SwiftUI
import Foundation
@main
struct MainApp: App {
@UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
init() {
}
var body: some Scene {
WindowGroup{
// MainView() // created this in MySceneDelegate below instead
}
}
}
then in MyAppDelegate.swift
import UIKit
class MyAppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
let config = UISceneConfiguration(name: "My Scene Delegate", sessionRole: connectingSceneSession.role)
config.delegateClass = MySceneDelegate.self
return config
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
and in MySceneDelegate
import SwiftUI
//
class MySceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let contentView = MainView()
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = PreferenceUIHostingController(wrappedView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
and finally my MainView.swift
import SwiftUI
struct MainView: View {
@State var deferSystemGestures = false
var body: some View {
VStack {
Toggle("Toggle System Gesture", isOn: $deferSystemGestures)
}.edgesDeferringSystemGestures(deferSystemGestures ? .all : [])
}
}
Thanks for the Google suggestion. I tried, failed, and posted the code and a stack overflow question about it: https://stackoverflow.com/questions/67474403/defer-system-edge-gestures-in-only-some-view-controllers-swiftui
Ah okay so I missed this detail https://developer.apple.com/documentation/uikit/uiviewcontroller/2887511-childforscreenedgesdeferringsyst
Apparently the system only asks the first UIViewController and if that vc doesn't return a child that the system should ask too then that's it.
You could introspect a UIView > get its window > check if the rootViewController is a PreferenceUIHostingController > if not wrap the root in a PreferenceUIHostingController.
This assumes SwiftUI doesn't change the rootViewController after the initial change and is not very safe but I can't think of a better way.
So I wrote a fix that makes this work with SwiftUI only using swizzling. It might be possible to do without swizzling but this works.
I added it as second file to this gist.
Use by wrapping your views with PreferenceUIHostingControllerView
PreferenceUIHostingControllerView {
ExampleView()
}
Hi. In the second file, I think the second 'swizzle' should be:
#selector(getter: childForHomeIndicatorAutoHidden) <-> #selector(childForHomeIndicatorAutoHidden_Amzd)
I have tested it with this change using the ios14 SwiftUI life cycle and it seems to work well.
You are correct @smithi01
I updated the gist
I am most grateful for this code - thank you. I dim a view in my app if it is idle for a while and use my own .dimIfIdle() modifier along with your .prefersHomeIndicatorAutoHidden(timer.idle). 'timer' is my StateObject. I simply code the view that all this happens in with "PreferenceUIHostingControllerView { PanelView() }". This is wonderful because it let me move away from AppDelegate and SceneDelegate.
Has anybody had any luck adapting this to the iOS 16 life cycle? This no longer works for iOS 16 and while .persistentSystemOverlays(.hidden)
is available, this is more preferential.
@LePips Do you have any more info on why this doesn't work on iOS 16? is there a new api for deferring system gestures in UIKit? as it should just use that right? or does the swizzle no longer work?
I do not have an idea why they don't work, the swizzled methods just aren't called. Most specifically, I am looking at the home indicator but I strongly assume this would also apply to all other methods.
Here is a minimal example that works for iOS 15 but not iOS 16:
Example
The commented out appDelegate
is explained below.
@main
struct PreferenceHostingDevApp: App {
// @UIApplicationDelegateAdaptor(MyAppDelegate.self)
// var appDelegate
var body: some Scene {
WindowGroup {
PreferenceUIHostingControllerView {
ContentView()
}
}
}
}
struct ContentView: View {
@State
private var showSecondView: Bool = false
var body: some View {
ZStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
Button {
showSecondView = true
} label: {
Text("Present")
}
}
}
.ignoresSafeArea()
.fullScreenCover(isPresented: $showSecondView) {
SecondView(showSecondView: $showSecondView)
}
}
}
struct SecondView: View {
@Binding
var showSecondView: Bool
var body: some View {
VStack {
Text("Hello There")
Button {
showSecondView = false
} label: {
Text("Dismiss")
}
}
.prefersHomeIndicatorAutoHidden(true)
}
}
Other things attempted:
- setting the scene window root view controller (via the
appDelegate
) - create another solution that uses a proxy
ObservableObject
which will manually call the update method on the view controller - wrapping
SecondView
in aPreferenceUIHostingControllerView
@LePips Hmm, is it possible that the preference key doesn’t forward from a presented view? Have you tried without the presented view?
If you mean trying to hide the home indicator on ContentView
, that does work
Hi, does this work with SwiftUI 2.0? Having trouble getting it to go. Likely operator error but thought I'd check.