I have encountered a bug whenever UIViewControllerRepresentable is used within a SwiftUI view. I've filed a report to Feedback Assistant, but need to work around it to release my app.
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { geo in
VStack {
Text("geo.size.height: \(geo.size.height), geo.safeAreaInsets.top: \(geo.safeAreaInsets.top)")
.padding()
TestGeo()
}
}
}
}
struct TestGeo: UIViewControllerRepresentable, Equatable {
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIViewController { return UIViewController() }
func updateUIViewController(_ viewController: UIViewController, context: UIViewControllerRepresentableContext<Self>) {}
}
Note, the GeometryReader is not required for the bug to manifest but it might shed light on what is happening.
Steps to reproduce:
Run code on a notchless iPhone like the iPhone 8 Plus simulator in portrait.
Rotate simulator from portrait to landscape.
Rotate simulator from landscape to portrait.
The text reads:
geo.size.height: 736.0, geo.safeAreaInsets.top: 0.0
but should read:
geo.size.height: 716.0, geo.safeAreaInsets.top: 20.0
Also, the text position is moved 20 points too high.
An interesting way I've found of manually correcting the layout is to start to pull down the Notification Center, the layout and safe area heights will correct themselves.
Any ideas on how to make the layout correct itself?
Wrapping in a navigation controller seems to do the trick, just disable all unwanted UI effects such as the navigation bar:
func makeUIViewController(context: UIViewControllerRepresentableContext<Self>) -> UIViewController {
let navigationController = UINavigationController(rootViewController: UIViewController())
navigationController.isNavigationBarHidden = true
return navigationController
}
Related
I need to disallow the Back swipe gesture in a view that has been "pushed" in a SwiftUI NavigationView.
I am using the navigationBarBackButtonHidden(true) view modifier from the "pushed" view, which obviously hides the standard Back button, and partially solves for the requirement.
But users cans still swipe from left to right (going back), which I don't want to allow.
I have tried using interactiveDismissDisabled, but this only disables swiping down to dismiss, not "back".
Any suggestions welcome.
[UPDATE] I tried creating a new app with Xcode 14.2, and the navigationBarBackButtonHidden(true) worked as expected:
No back button
Back swipe gesture disabled
But when I modified the main class of my existing app - with the EXACT SAME CODE as the new test app, it still allowed the back swipe gesture. Here's the code:
import SwiftUI
#main
struct MyApp: App {
var body: some Scene {
WindowGroup {
MyView()
}
}
}
struct MyView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Next page") {
Text("Page 2")
.navigationBarBackButtonHidden(true)
}
}
}
}
}
At this point, I'm fairly confused. Recreating my existing project, that was created with Xcode 13, will be a substantial task, so I'm trying to figure out what's different. I'm assuming that there is some build setting or configuration option that is somehow influencing this behavior.
Again, any suggestions welcome
For some cases, it may be necessary to remove the interactivePopGestureRecognizer from the UINavigationController. The DisableSwipeBack.swift example demonstrates this here: Disable swipe-back for a NavigationLink SwiftUI
It turns out that we had the following code in our app that was thought to be unused, but really wasn't, since it was extending UINavigationController.
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
Uggh.
I'm writing a simple credential autofill extension as a means of playing around with SwiftUI on iOS. However, I'm finding that on iPadOS the SwiftUI View does not render properly. My CredentialProviderViewController is called at the beginning of the extension lifecycle, and is responsible for loading the SwiftUI View. It looks like this:
class CredentialProviderViewController: ASCredentialProviderViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
let services: [String] = serviceIdentifiers.map { $0.identifier }
let autofillView = AutofillView(services: services,
extensionContext: self.extensionContext)
let vc = UIHostingController(rootView: autofillView)
vc.view.translatesAutoresizingMaskIntoConstraints = false
vc.view.frame = view.bounds
view.addSubview(vc.view)
addChild(vc)
}
}
My SwiftUI AutofillView is very simple and looks like this:
struct AutofillView: View {
let services: [String]
var extensionContext: ASCredentialProviderExtensionContext? = nil
var body: some View {
NavigationView {
Text("LOCK SCREEN")
}
}
}
On iPhone, this renders exactly as I'd expect, with the words "LOCK SCREEN" appearing in the center of the View when the extension loads. However, on iPad the View is displayed in a modal window and the contents are not rendered properly. In fact, only the slightest bit of the "L" is displayed. (See screenshot)
I'm sure I'm missing something or not instantiating my SwiftUI View properly. I'm just not sure where. Any thoughts?
Instead of adding it into a containerview or as a subview, present the UIHostingController like you would a view controller
let services: [String] = serviceIdentifiers.map { $0.identifier }
let autofillView = AutofillView(services: services,
extensionContext: self.extensionContext)
let vc = UIHostingController(rootView: autofillView)
vc.modalPresentationStyle = .fullScreen
self.present(vc, animated: false)
I'm working on a fractal clock app that displays animated fractals based on clock hands in its main view. I want users of my app to be able to enter a fullscreen mode where all unnecessary UI is temporarily hidden and only the animation remains visible. The behavior I'm looking for is similar to Apple's Photos app where one can tap on the currently displayed image so that the navigation bar, the bottom bar, the status bar and the home indicator fade out until the image is tapped again.
Hiding the navigation bar and the status bar was as easy as finding the right view modifiers to pass the hiding condition to. But as far as I know it is currently not possible in SwiftUI to hide the home indicator without bringing in UIKit.
On Stack Overflow I found this solution by Casper Zandbergen for conditionally hiding the home indicator and adopted it for my project.
It works but sadly in comes with an unacceptable side effect: The main view now no longer extends under the status bar and the home indicator which has two implications:
When hiding the status bar with the relevant SwiftUI modifier the space for the main view grows by the height of the hidden status bar interrupting the display of the fractal animation.
In place of the hidden home indicator always remains a black bottom bar preventing the fullscreen presentation of the main view.
I hope somebody with decent UIKit experience can help me with this. Please keep in mind that I'm a beginner in SwiftUI and that I have basically no prior experience with UIKit. Thanks in advance!
import SwiftUI
struct ContentView: View {
#StateObject var settings = Settings()
#State private var showSettings = false
#State private var hideUI = false
var body: some View {
NavigationView {
GeometryReader { proxy in
let radius = 0.5 * min(proxy.size.width, proxy.size.height) - 20
FractalClockView(settings: settings, clockRadius: radius)
}
.ignoresSafeArea(.all)
.toolbar {
Button(
action: { showSettings.toggle() },
label: { Label("Settings", systemImage: "slider.horizontal.3") }
)
.popover(isPresented: $showSettings) { SettingsView(settings: settings) }
}
.navigationBarTitleDisplayMode(.inline)
.onTapGesture {
withAnimation { hideUI.toggle() }
}
.navigationBarHidden(hideUI)
.statusBar(hidden: hideUI)
.prefersHomeIndicatorAutoHidden(hideUI) // Code by Amzd
}
.navigationViewStyle(.stack)
}
}
I was able to solve the problem with the SwiftUI view not extending beyond the safe area insets for the status bar and the home indicator by completely switching to a storyboard based project template and embedding my views through a custom UIHostingController as described in this solution by Casper Zandbergen.
Before I was re-integrating the hosting controller into the SwiftUI view hierarchy by wrapping it with a UIViewRepresentable instance, which must have caused the complications in handling the safe area.
By managing the whole app through the custom UIHostingController subclass it was even easier to get the hiding of the home indicator working. As much as I love SwiftUI I had to realize that, with its current limitations, UIKit was the better option here.
Final code (optimized version of the solution linked above):
ViewController.swift
import SwiftUI
import UIKit
struct HideUIPreferenceKey: PreferenceKey {
static var defaultValue: Bool = false
static func reduce(value: inout Bool, nextValue: () -> Bool) {
value = nextValue() || value
}
}
extension View {
func userInterfaceHidden(_ value: Bool) -> some View {
preference(key: HideUIPreferenceKey.self, value: value)
}
}
class ViewController: UIHostingController<AnyView> {
init() {
weak var vc: ViewController? = nil
super.init(
rootView: AnyView(
ContentView()
.onPreferenceChange(HideUIPreferenceKey.self) {
vc?.userInterfaceHidden = $0
}
)
)
vc = self
}
#objc required dynamic init?(coder: NSCoder) {
weak var vc: ViewController? = nil
super.init(
coder: coder,
rootView: AnyView(
ContentView()
.onPreferenceChange(HideUIPreferenceKey.self) {
vc?.userInterfaceHidden = $0
}
)
)
vc = self
}
private var userInterfaceHidden = false {
didSet { setNeedsUpdateOfHomeIndicatorAutoHidden() }
}
override var prefersStatusBarHidden: Bool {
userInterfaceHidden
}
override var prefersHomeIndicatorAutoHidden: Bool {
userInterfaceHidden
}
}
TL;DR
Need to keep autorotation, but exclude one UIView from autorotating on orientation change, how?
Back story
I need to keep a UIView stationary during the animation accompanied by autorotation (which happens on orientation change). Similar to how the iOS camera app handles the rotation (i.e controls rotate in their place).
Things I've tried
Returning false from shouldAutorotate(), subscribing to UIDeviceOrientationDidChangeNotification, and trying to manually handle the rotation event for each view separately.
Works well if you don't need to change any of your UIViews' places, otherwise it's a pain figuring out where it should end up and how to get it there
Placing a non rotating UIWindow under the main UIWindow, and setting the main UIWindow background colour to clear.
This works well if it's only one item, but I don't want to manage a bunch of UIWindows
Inverse rotation I.e rotating the UIView in the opposite direction to the rotation. Not reliable, and looks weird, it's also vertigo inducing
Overriding the animation in the viewWillTransitionToSize method. Failed
And a bunch of other things that would be difficult to list here, but they all failed.
Question
Can this be done? if so, how?
I'm supporting iOS8+
Update This is how the views should layout/orient given #Casey's example:
I have faced with same problem and found example from Apple, which helps to prevent UIView from rotation: https://developer.apple.com/library/ios/qa/qa1890/_index.html
However, if UIView is not placed in the center of the screen, you should handle new position manually.
i think part of the reason this is so hard to answer is because in practice it doesn't really make sense.
say i make a view that uses autolayout to look like this in portrait and landscape:
if you wanted to prevent c from rotating like you are asking, what would you expect the final view to look like? would it be one of these 3 options?
without graphics of the portrait/landscape view you are trying to achieve and a description of the animation you are hoping for it'll be very hard to answer your question.
are you using NSLayoutConstraint, storyboard or frame based math to layout your views? any code you can provide would be great too
If you're wanting to have the same effect as the camera app, use size classes (see here and here).
If not, what is wrong with creating a UIWindow containing a view controller that doesn't rotate? The following code seems to work for me (where the UILabel represents the view you don't want to rotate).
class ViewController: UIViewController {
var staticWindow: UIWindow!
override func viewDidLoad() {
super.viewDidLoad()
showWindow()
}
func showWindow() {
let frame = CGRect(x: 10, y: 10, width: 100, height: 100)
let vc = MyViewController()
let label = UILabel(frame: frame)
label.text = "Hi there"
vc.view.addSubview(label)
staticWindow = UIWindow(frame: frame)
staticWindow.rootViewController = MyViewController()
staticWindow.windowLevel = UIWindowLevelAlert + 1;
staticWindow.makeKeyAndVisible()
staticWindow.rootViewController?.presentViewController(vc, animated: false, completion: nil)
}
}
class MyViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
override func shouldAutomaticallyForwardRotationMethods() -> Bool {
return false
}
override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
}
I'd like to use the UIViewController's input accessory view like this:
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView! {
return self.bar
}
but the issue is that I have a drawer like view and when I slide the view open, the input view stays on the window. How can I keep the input view on the center view like Slack does it.
Where my input view stays at the bottom, taking up the full screen (the red is the input view in the image below):
There are two ways to do this exactly like Slack doing it, Meiwin has a medium post here A Stickler for Details: Implementing Sticky Input Field in iOS to show how he managed to do this which he actually puts an empty UIView as an inputAccessoryView then track it’s coordinates on screen to know where to put his custom view in relation with the empty view, this way can be helpful if you are going to support SplitViewController on iPad, but if you are not interested in this way, you can see how I managed to do this like this image
Here is before swiping
Here is after
All I did was actually taking a snapshot from the inputAccessoryView window and putting it on the NavigationController of the TableViewController
I am using SideMenu from Jon Kent and it’s pretty easy to do it with the UISideMenuNavigationControllerDelegate
var isInputAccessoryViewEnabled = true {
didSet {
self.inputAccessoryView?.isHidden = !self.isInputAccessoryViewEnabled
if self.isInputAccessoryViewEnabled {self.becomeFirstResponder()}
}
}
func sideMenuWillAppear(menu: UISideMenuNavigationController, animated: Bool) {
let inputWindow = UIApplication.shared.windows.filter({$0.className == "UITextEffectsWindow"}).first
self.inputAccessoryViewSnapShot = inputWindow?.snapshotView(afterScreenUpdates: false)
if let snapShotView = self.inputAccessoryViewSnapShot, let navView = self.navigationController?.view {
navView.addSubview(snapShotView)
}
self.isInputAccessoryViewEnabled = false
}
func sideMenuDidDisappear(menu: UISideMenuNavigationController, animated: Bool) {
self.inputAccessoryViewSnapShot?.removeFromSuperview()
self.isInputAccessoryViewEnabled = true
}
I hope that helps :)