Lock device orientation for SwiftUI UIViewControllerRepresentable - swift

I have a UIViewController that I need to wrap and use as a SwiftUI view. I have implemented the methods needed to lock device orientation for that view controller. When I use the UIViewController in an UIKit context, the orientation could be locked.
class ContentViewController : UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.red
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return false
}
}
When I wrap it as a SwiftUI view with UIViewControllerRepresentable, the orientation could not be locked. The
struct ContentWrappedView : UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ContentViewController {
return ContentViewController.init()
}
func updateUIViewController(_ uiViewController: ContentViewController, context: Context) {
}
}
Is this expected behavior? Is there a way around it?
I'll be using it in a framework and so, implementing it with AppDelegate doesn't work for me. Thanks in advance.

Related

Connecting UIKit with SwiftUI, why did I lose my NavBar?

So when I use the old UIKit Lifecycle with SceneDelegate and AppDelegate I get the desired result. But when I use the new App life cycle I can't figure out to get the NavBar back..
import SwiftUI
struct ContentView: View {
var body: some View {
DiffableContainer()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
DiffableContainer()
.ignoresSafeArea()
}
}
struct DiffableContainer: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return UINavigationController(rootViewController: DiffableTableVC())
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
class DiffableTableVC: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
tableView.backgroundColor = .green
navigationItem.title = "Test"
title = "Hello"
}
}
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("This is set up and running")
return true
}
}
Here is the lifecycle that I am using now:
#main
struct ContactsApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

Using TabbarViewController the preferredStatusBarStyle not called

I have a TabbarViewController with different views, the problem is:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
not called in any the of the views.
I have tried to add:
extension UITabBarController {
open override var childForStatusBarStyle: UIViewController? {
return selectedViewController?.childForStatusBarStyle ?? selectedViewController
}
}
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return topViewController?.childForStatusBarStyle ?? topViewController
}
}
Nothing change, should I call this extension method in some where else?
You could try to add in to Info.plist expect adding some codes.
You can basically add this into your Info.plist
I had to create a class of UInavigation Controller then add this methods to the class and it start to work.
override open var childViewControllerForStatusBarStyle: UIViewController? {
return self.topViewController
}
override open var preferredStatusBarStyle : UIStatusBarStyle {
return topViewController?.preferredStatusBarStyle ?? .default
}

Set property on ViewController in UIViewControllerRepresentable

How can I change a property on a UIViewController presented via. a UIViewControllerRepresentable ?
Sample code of how I would expect it to work, however it doesn't. How can I make it work?
(color is just a example, please don't focus on that)
class MyViewController: UIViewController {
var color: UIColor? = nil {
didSet {
guard isViewLoaded else { return }
view.layer.backgroundColor = color?.cgColor
}
}
override func viewDidLoad() {
view.layer.backgroundColor = color?.cgColor
}
}
struct MyView: UIViewControllerRepresentable {
#State private var color: UIColor?
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
let viewController = MyViewController()
viewController.color = color // always nil?
return viewController
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
uiViewController.color = color // always nil?
}
}
extension MyView {
func color(_ color: UIColor) -> MyView {
self.color = color // does nothing?
return self
}
}
struct ContentView: View {
var body: some View {
MyView()
.color(.magenta)
}
}
Here is possible approach (if you expect that color can be modified externally, as it is seen). Tested with Xcode 11.4 / iOS 13.4
struct MyView: UIViewControllerRepresentable {
#Binding var color: UIColor?
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
let viewController = MyViewController()
viewController.color = color // always nil?
return viewController
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
uiViewController.color = color // always nil?
}
}
struct ContentView: View {
#State private var color: UIColor? = .magenta
var body: some View {
MyView(color: $color)
// MyView(color: .constant(.magenta)) // alternate usage
}
}
Another solution based on idea from Asperi's answer:
struct MyView: UIViewControllerRepresentable {
private class State: ObservableObject {
var color: UIColor?
}
#Binding private var state: State
init() {
_state = .constant(State())
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
return MyViewController()
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
uiViewController.color = state.color
}
}
extension MyView {
func color(_ color: UIColor) -> MyView {
self.state.color = color
return self
}
}
Or a even simpler version, we just use #Binding for the wrapped ViewController directly
struct MyView: UIViewControllerRepresentable {
#Binding private var viewController: MyViewController
init() {
_viewController = .constant(MyViewController())
}
func makeUIViewController(context: UIViewControllerRepresentableContext<MyView>) -> MyViewController {
return viewController
}
func updateUIViewController(_ uiViewController: MyViewController,
context: UIViewControllerRepresentableContext<MyView>) {
}
}
extension MyView {
func color(_ color: UIColor) -> MyView {
self.viewController.color = color
return self
}
}

View content doesn't appear in UIScrollView

I have this...
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.addSubview(UIHostingController(rootView: preview).view)
return control
}
If I put my preview view outside of my custom scroll view then I can see it. But when I add it as a subview of a UIScrollView then I can't see anything.
Have I added it correctly?
Here is the complete code for the scroll view, which I got from here.
import SwiftUI
import Foundation
struct LegacyScrollView : UIViewRepresentable {
var preview: AnyView
init(preview: AnyView) {
self.preview = preview
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
let control = UIScrollView()
control.addSubview(UIHostingController(rootView: preview).view)
return control
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
}
class Coordinator: NSObject {
var control: LegacyScrollView
init(_ control: LegacyScrollView) {
self.control = control
}
#objc func handleRefreshControl(sender: UIRefreshControl) {
sender.endRefreshing()
}
}
}

Hide navigation bar without losing swipe back gesture in SwiftUI

In SwiftUI, whenever the navigation bar is hidden, the swipe to go back gesture is disabled as well.
Is there any way to hide the navigation bar while preserving the swipe back gesture in SwiftUI? I've already had a custom "Back" button, but still need the gesture.
I've seen some solutions for UIKit, but still don't know how to do it in SwiftUI
Here is the code to try yourself:
import SwiftUI
struct RootView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View{
Text("As you can see, swipe to go back will not work")
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Any suggestions or solutions are greatly appreciated
This should work by just extending UINavigationController.
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return viewControllers.count > 1
}
}
It is even easier than what Nick Bellucci answered.
Here is the simplest working solution:
extension UINavigationController {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = nil
}
}
When using the UINavigationController extension you might encounter a bug that blocks your navigation after you start swiping the screen and let it go without navigating back. Adding .navigationViewStyle(StackNavigationViewStyle()) to NavigationView does fix this issue.
If you need different view styles based on device, this extension helps:
extension View {
public func currentDeviceNavigationViewStyle() -> AnyView {
if UIDevice.current.userInterfaceIdiom == .pad {
return AnyView(self.navigationViewStyle(DefaultNavigationViewStyle()))
} else {
return AnyView(self.navigationViewStyle(StackNavigationViewStyle()))
}
}
}
I looked around documentation and other sources about this issue and found nothing. There are only a few solutions, based on using UIKit and UIViewControllerRepresentable. I tried to combine solutions from this question and I saved swipe back gesture even while replacing back button with other view. The code is still dirty a little, but I think that is the start point to go further (totally hide navigation bar, for example). So, here is how ContentView looks like:
import SwiftUI
struct ContentView: View {
var body: some View {
SwipeBackNavController {
SwipeBackNavigationLink(destination: DetailViewWithCustomBackButton()) {
Text("Main view")
}
.navigationBarTitle("Standard SwiftUI nav view")
}
.edgesIgnoringSafeArea(.top)
}
}
// MARK: detail view with custom back button
struct DetailViewWithCustomBackButton: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
Text("detail")
.navigationBarItems(leading: Button(action: {
self.dismissView()
}) {
HStack {
Image(systemName: "return")
Text("Back")
}
})
.navigationBarTitle("Detailed view")
}
private func dismissView() {
presentationMode.wrappedValue.dismiss()
}
}
Here is realization of SwipeBackNavController and SwipeBackNavigationLink which mimic NavigationView and NavigationLink. They are just wrappers for SwipeNavigationController's work. The last one is a subclass of UINavigationController, which can be customized for your needs:
import UIKit
import SwiftUI
struct SwipeBackNavController<Content: View>: UIViewControllerRepresentable {
let content: Content
public init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
func makeUIViewController(context: Context) -> SwipeNavigationController {
let hostingController = UIHostingController(rootView: content)
let swipeBackNavController = SwipeNavigationController(rootViewController: hostingController)
return swipeBackNavController
}
func updateUIViewController(_ pageViewController: SwipeNavigationController, context: Context) {
}
}
struct SwipeBackNavigationLink<Destination: View, Label:View>: View {
var destination: Destination
var label: () -> Label
public init(destination: Destination, #ViewBuilder label: #escaping () -> Label) {
self.destination = destination
self.label = label
}
var body: some View {
Button(action: {
guard let window = UIApplication.shared.windows.first else { return }
guard let swipeBackNavController = window.rootViewController?.children.first as? SwipeNavigationController else { return }
swipeBackNavController.pushSwipeBackView(DetailViewWithCustomBackButton())
}, label: label)
}
}
final class SwipeNavigationController: UINavigationController {
// MARK: - Lifecycle
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
}
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
delegate = self
}
override func viewDidLoad() {
super.viewDidLoad()
// This needs to be in here, not in init
interactivePopGestureRecognizer?.delegate = self
}
deinit {
delegate = nil
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
duringPushAnimation = true
setNavigationBarHidden(true, animated: false)
super.pushViewController(viewController, animated: animated)
}
var duringPushAnimation = false
// MARK: - Custom Functions
func pushSwipeBackView<Content>(_ content: Content) where Content: View {
let hostingController = SwipeBackHostingController(rootView: content)
self.delegate = hostingController
self.pushViewController(hostingController, animated: true)
}
}
// MARK: - UINavigationControllerDelegate
extension SwipeNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
}
// MARK: - UIGestureRecognizerDelegate
extension SwipeNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard gestureRecognizer == interactivePopGestureRecognizer else {
return true // default value
}
// Disable pop gesture in two situations:
// 1) when the pop animation is in progress
// 2) when user swipes quickly a couple of times and animations don't have time to be performed
let result = viewControllers.count > 1 && duringPushAnimation == false
return result
}
}
// MARK: Hosting controller
class SwipeBackHostingController<Content: View>: UIHostingController<Content>, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.duringPushAnimation = false
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
guard let swipeNavigationController = navigationController as? SwipeNavigationController else { return }
swipeNavigationController.delegate = nil
}
}
This realization provides to save custom back button and swipe back gesture for now. I still don't like some moments, like how SwipeBackNavigationLink pushes view, so later I'll try to continue research.