I am struggling to hide the navigationBar, which would properly be hidden if the root controller wasn't a SwiftUI UIHostingController.
I tried the following:
Setting navigationController.isNavigationBarHidden = true after creating it, at viewDidLoad and viewWillAppear.
Adding both .navigationBarHidden(true) and .navigationBarBackButtonHidden(true) for the UIHostingController's rootView.
Could it be an Apple bug? I am using Xcode 11.6.
All my attempts together:
class LoginController: UINavigationController, ObservableObject
{
static var newAccount: LoginController
{
let controller = LoginController()
let view = LoginViewStep1()
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
controller.viewControllers = [UIHostingController(rootView: view)]
controller.isNavigationBarHidden = true
return controller
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.isNavigationBarHidden = true
}
override func viewDidLoad()
{
super.viewDidLoad()
self.isNavigationBarHidden = true
}
}
struct LoginViewStep1: View
{
// ...
var body: some View
{
VStack {
// ...
}
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
Modified your code:
class LoginController: UINavigationController, ObservableObject
{
static var newAccount: LoginController
{
let controller = LoginController()
let view = LoginViewStep1()
controller.viewControllers = [UIHostingController(rootView: view)]
// make it delayed, so view hierarchy become constructed !!!
DispatchQueue.main.async {
controller.isNavigationBarHidden = true
}
return controller
}
}
struct LoginViewStep1: View
{
var body: some View
{
VStack {
Text("Hello World!")
}
}
}
tested part in SceneDelegate
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = LoginController.newAccount
self.window = window
window.makeKeyAndVisible()
}
Alternate solution is to use UINavigationControllerDelegate:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// Required because pushing UIHostingController makes the navigationBar appear again
navigationController.isNavigationBarHidden = true
}
Tested with iOS 14.5 - Xcode 12.5
Related
The title may be confusing, so I will explain with an example:
There are 3 view controllers in the navigation stack: FirstViewController, SecondViewController and ThirdViewController.
FirstViewController supports landscape interface orientation only while SecondViewController and ThirdViewController support portrait only.
When FirstViewController shows up, SecondViewController will be pushed into the stack after 2 seconds, and we can see a rotation animation from landscape to portrait.
Then I continue to push a ThirdViewController instance and pop SecondViewController at the same time by assigning a view controller array to navigation controller.
The problem is in Step 4: For iOS 16, there is also a rotation animation for ThirdViewController, just like when SecondViewController is pushed into the stack.
class CustomNavigationController: UINavigationController {
override var shouldAutorotate: Bool {
return topViewController?.shouldAutorotate ?? false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return topViewController?.supportedInterfaceOrientations ?? .portrait
}
}
class FirstViewController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeRight
}
override func viewDidLoad() {
super.viewDidLoad()
title = "First"
view.backgroundColor = .green
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let vc = SecondViewController()
self.navigationController?.pushViewController(vc, animated:true)
}
}
}
class SecondViewController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Second"
view.backgroundColor = .yellow
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
let vc = ThirdViewController()
var viewControllers = self.navigationController?.viewControllers ?? []
viewControllers.removeLast()
viewControllers.append(vc)
// setting animated to false doesn't help
self.navigationController?.setViewControllers(viewControllers, animated: true)
}
}
}
class ThirdViewController: UIViewController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Third"
view.backgroundColor = .red
}
}
Is there any way to solve this weird rotation animation on iOS 16?
I have three controllers and one swiftUI view, below is the flow
A Controller(RootController) -> B controller - > C controller -> SwiftUI view
I am doing some operation on the SwiftUI view based on the operation need to decide to whether pop back to C controller or B. Currently I am able pop back to C Controller and root controller, not sure how to jump to specific (in my case B controller)controller in SwiftUI
.navigationBarItems(leading:Button(action: {
self.presentationMode.wrappedValue.dismiss()
})
I mean to say instead of this line self.presentationMode.wrappedValue.dismiss() is there any method to pass view controller to pop back
You can make an extension for UINavigationController
extension UINavigationController {
func popToViewController(ofClass: AnyClass, animated: Bool = true) {
if let vc = viewControllers.last(where: { $0.isKind(of: ofClass) }) {
popToViewController(vc, animated: animated)
}
}
}
and use it like this
func goBack() {
navigationController?.popToViewController(ofClass: UIHostingController<SomeSwinftUIView>.self) // if this is a swuftUI view
}
or
func goBack() {
navigationController?.popToViewController(ofClass: SomeViewController.self)
}
#OhStack just import UIKit (Personally i'm using a router/coordinator for navigation, and there I include UIKit)
import UIKit
import SwiftUI
struct SwiftUIViewCoordinator {
let navController: UINavigationController?
func startView(navController: UINavigationController) {
self.navController = navController
let swiftUIView = SwifUIViewView(router: self)
let hostingView = UIHostingController(rootView: swiftUIView)
navigationController?.pushViewController(hostingView, animated: true)
}
func goBackToSomeView() {
navigationController?.popToViewController(ofClass:
UIHostingController<SomeSwinftUIView>.self)
}
}
i have a navigation controller which navigates to a view controller, which navigates to uihostingcontroller. How would i push to another view controller from swift ui
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
self.window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
let navigationView = UINavigationController(rootViewController: PreviewViewVideoController())
navigationView.isToolbarHidden = true
self.window?.rootViewController = navigationView
self.window?.makeKeyAndVisible()
}
in preview video controller
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch cards[indexPath.row] {
case .trans_human:
let controller = UIHostingControllerCustom(rootView: FilmOverviewView(overview: TransHumanOverview()))
self.navigationController?.pushViewController(controller, animated: true)
controller.navigationItem.title = cards[indexPath.row].rawValue
}
}
in filmOverviewView
struct FilmOverviewView: View {
var filmOverview: FilmOverview!
var imageResource: String!
init(overview: FilmOverview) {
filmOverview = overview
}
var body: some View {
ScrollView(Axis.Set.vertical, showsIndicators: false) {
VStack {
Image(filmOverview.imageResource)
.resizable()
.frame(width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height / 2)
.overlay(StartButtonOverlay(), alignment: .bottom)
Button(action: {})
}
}
How would i navigate from a swiftui button view action to a new view controller using existing navigation stack
I would inject UINavigationController into SwiftUI stack via environment values
struct NavigationControllerKey: EnvironmentKey {
static let defaultValue: UINavigationController? = nil
}
extension EnvironmentValues {
var navigationController: NavigationControllerKey.Value {
get {
return self[NavigationControllerKey.self]
}
set {
self[NavigationControllerKey.self] = newValue
}
}
}
then in table view delegate inject it
let controller = UIHostingControllerCustom(rootView:
FilmOverviewView(overview: TransHumanOverview()
.environment(\.navigationController, self.navigationController))
and now in FilmOverviewView we can use it
struct FilmOverviewView: View {
#Environment(\.navigationController) var navigationController
// ... other code
var body: some View {
ScrollView(Axis.Set.vertical, showsIndicators: false) {
VStack {
// ... other code
Button("Demo") {
let controller = UIHostingControllerCustom(rootView:
SomeOtherView() // inject if needed again
.environment(\.navigationController, self.navigationController)))
self.navigationController?.pushViewController(controller, animated: true)
}
}
}
Update: updated custom hosting controller
class UIHostingControllerCustom<V: View>: UIHostingController<V> {
override func viewWillAppear(_ animated: Bool) {
navigationController?.setNavigationBarHidden(true, animated: false)
}
}
I've been testing SwiftUI to see how far I can go with it. Now, I want to find out whether or not I can pass a string from SwiftUI View directly to UIViewController. And I want to display this string with UILabel. UILabel is all I have on my storyoboard.
The following is my view controller (UIViewController).
import UIKit
import SwiftUI
class HomeViewController: UIViewController {
var message = String()
#IBOutlet weak var messageLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
messageLabel.text = message
}
}
struct HomeViewControllerRepresentation: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<HomeViewControllerRepresentation>) -> HomeViewController {
UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "homeViewController") as! HomeViewController
}
func updateUIViewController(_ uiViewController: HomeViewController, context: UIViewControllerRepresentableContext<HomeViewControllerRepresentation>) {
}
}
My SwiftUI View is the following.
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: HomeViewControllerRepresentation(message: "GGG")) { // Error
Text("Tap me")
}
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
I hoped that I could just pass a string with HomeViewControllerRepresentation. But it won't work. Is there a simple way of passing data from SwiftUI View to UIViewController? Thanks.
UIViewControllerRepresentable is not a proxy, but wrapper, so everything should be transferred manually, like
struct HomeViewControllerRepresentation: UIViewControllerRepresentable {
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<HomeViewControllerRepresentation>) -> HomeViewController {
let controller = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "homeViewController") as! HomeViewController
controller.message = self.message
return controller
}
func updateUIViewController(_ uiViewController: HomeViewController, context: UIViewControllerRepresentableContext<HomeViewControllerRepresentation>) {
}
}
I have an iMessage extension and I'm having some issues with the top layout guide. I have an MSMessagesAppViewController that handles changes between presentation styles. In my extension I have a button. When it is clicked I transition to expanded presentation style and then present a view controller modally. Here's the problem: my UI in the second VC is getting hidden behind the top navigation bar. I thought this was strange as I set my constraints to the top layout guide. So I dug through my code and started debugging the top layout guide. I noticed that after I transition to expanded presentation style, topLayoutGuide.length = 86. That's how it should be. But when I present the second view controller modally, the top layout guide is reset to 0. Why isn't it 86 as it should be? Here is my code:
In my main viewController:
#IBAction func addStickerButtonPressed(_ sender: AnyObject) {
shouldPerformCreateSegue = true
theSender = sender
requestPresentationStyle(.expanded)
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
if presentationStyle == .expanded {
if shouldPerformCreateSegue == true {
shouldPerformCreateSegue = false
performSegue(withIdentifier: "CreateStickerSegue", sender: theSender)//here is where I present the new viewController
} else {
searchBar.becomeFirstResponder()
searchBar.placeholder = nil
searchBar.showsCancelButton = true
searchBar.tintColor = UIColor.white
}
} else {
searchBar.showsCancelButton = false
}
print(topLayoutGuide.length) //This prints out 86
}
In the other modally presented view controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.addConstraint(navBar.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor))
print(topLayoutGuide.length) //This prints out 0
}
As a workaround I use UIPresentationController, which shifts the modal view controller by topLayoutGuide.length points:
class MyViewController: MSMessagesAppViewController {
private func presentModalViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.modalPresentationStyle = .custom
imagePicker.transitioningDelegate = self
present(imagePicker, animated: true, completion: nil)
}
}
// MARK: - UIViewControllerTransitioningDelegate
extension MyViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let vc = PresentationController(presentedViewController: presented, presenting: presenting)
// I really don't want to hardcode the value of topLayoutGuideLength here, but when the extension is in compact mode, topLayoutGuide.length returns 172.0.
vc.topLayoutGuideLength = topLayoutGuide.length > 100 ? 86.0 : topLayoutGuide.length
return vc
}
}
class PresentationController: UIPresentationController {
var topLayoutGuideLength: CGFloat = 0.0
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView else {
return super.frameOfPresentedViewInContainerView
}
return CGRect(x: 0, y: topLayoutGuideLength, width: containerView.bounds.width, height: containerView.bounds.height - topLayoutGuideLength)
}
}
The only problem is when you're calling presentModalViewController from compact mode, topLayoutGuide.length is 172.0 for unknown reason. So I had to hardcode a value for that case.
I believe this was known bug on previous iOS 10 beta. I had same issue and top and bottom layout guide works as I expect after I upgraded iOS version to latest.
I used a slightly varied version of Andrey's
class MyViewController: MSMessagesAppViewController {
private func presentModalViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.modalPresentationStyle = .custom
imagePicker.transitioningDelegate = self
present(
imagePicker,
animated: true,
completion: nil
)
}
}
extension MyViewController: UIViewControllerTransitioningDelegate {
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
let vc = PresentationController(
presentedViewController: presented,
presenting: presenting
)
vc.framePresented = modalBoundaries.frame
return vc
}
}
class PresentationController: UIPresentationController {
var framePresented = CGRect.zero
override var frameOfPresentedViewInContainerView: CGRect {
return framePresented
}
}
modalBoundaries being a dummy UIView constrained (via XIB in my case) to respect any TopLayoutGuide length.