Last active
November 14, 2024 10:01
-
-
Save Amzd/01e1f69ecbc4c82c8586dcd292b1d30d to your computer and use it in GitHub Desktop.
PreferenceUIHostingController. Adds hiding home indicator and deferring system edge gestures to SwiftUI. (Don't work at the same time but I think that's normal?)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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 | |
} | |
} |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@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?