-
-
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 | |
} | |
} |
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
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.