Present UINavigationController from SwiftUI - swift

I have a UIViewController that I want presented from my SwiftUI view, the issue is that I need the UIViewController wrapped in a UINavigationController, how can I present that wrapped VC from my SwiftUI View?
This is what I've tried, it works for presenting the view controller but I do not know how to wrap it in a navigation controller:
struct ComposeTakeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> ComposeTakeVC {
return ComposeTakeVC(nibName: "ComposeTakeVC", bundle: nil)
}
func updateUIViewController(_ uiViewController: ComposeTakeVC, context: Context) {
}
}
In the SwiftUI view:
.fullScreenCover(isPresented: $showComposeTake, content: {
ComposeTakeView()
})
I've also tried creating a new SwiftUI view with NavigationView and tool bar, but this does not show the tool bar button:
struct ComposeTakeNavView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
ComposeTakeView()
}
.toolbar {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
.foregroundColor(.black)
}
}
}
}

We can wrap it right inside representable and return just as base class, like
func makeUIViewController(context: Context) -> UIViewController {
let controller = ComposeTakeVC(nibName: "ComposeTakeVC", bundle: nil)
return UINavigationController(rootViewController: controller)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}

Related

SwiftUI Button on top of a MKMapView does not get triggered

I have a button on top of a MKMapView. But the button does not get triggered when it's tapped on. Do you know what's missing?
MapView.swift
import SwiftUI
import UIKit
import MapKit
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView {
let mkMapView = MKMapView()
return mkMapView
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, MKMapViewDelegate { }
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
MapView()
Button(action: {
print("Tapped")
}) {
Image(systemName: "lock.fill")
}
}
}
}
Give it just a bit more internal space to be better recognizable. Here is fixed & tested variant (Xcode 12 / iOS 14):
struct TestButtonWithMap: View {
#State private var locked = true
var body: some View {
ZStack {
MapView()
Button(action: {
print("Tapped")
self.locked.toggle()
}) {
Image(systemName: locked ? "lock.fill" : "lock.open")
.padding() // << here !!
}
}
}
}

Is possible to combine together in same View SwiftUIView and UIViewControllerRepresentable?

I am wondering if it's possible to combine a View and a UIViewControllerRepresentable in a same view.
I tried:
//Here I declare MyViewController:
class MyViewController: UIViewController {
override viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
}
}
struct MyViewControllerIntegrate: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<MyViewControllerIntegrate>) -> MyViewController {
return MyViewController()
}
func updateUIViewController(_ uiViewController: MyViewController, context: UIViewControllerRepresentableContext<MyViewControllerIntegrate>) {
}
}
struct MyView: View {
var body: some View {
Text("Hello StackOverflow!")
}
}
struct ContentView: View {
var body: some View {
MyView()
MyViewController()
.frame(height: 400)
}
}
Xcode shows me an error message:
Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
Any hints? Thank you
Put them in Group (or in some stack, eg. VStack)
struct ContentView: View {
var body: some View {
Group {
MyView()
MyViewController()
.frame(height: 400)
}
}
}

Fit content mode for a custom view in SwiftUI

How can I set content mode as fit for a custom UIImageView wrapped in SwiftU?
struct CustomView: UIViewRepresentable {
func makeUIView(context: Context) -> UIImageView {
CustomUIImage()
}
func updateUIView(_ uiView: UIImageView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
CustomView()
}
}
In the code above, CustomUIImage is a subclass of UIImageView from UIKit. It's wrapped in UIViewRepresentable to integrate it with the SwiftUI framework. Calling CustomView().aspectRatio(contentMode: .fit) didn't work in this case
Here is possible approach
struct CustomView: UIViewRepresentable {
private let imageView = CustomUIImage()
init(contentMode: UIView.ContentMode = .center) {
imageView.contentMode = contentMode
}
func makeUIView(context: Context) -> UIImageView {
imageView
}
func updateUIView(_ uiView: UIImageView, context: Context) {
}
}
struct ContentView: View {
var body: some View {
CustomView(contentMode: .scaleAspectFit)
}
}

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.

Does somebody have all ready implemented searchbar on tvos with swiftui?

Does somebody have all ready implemented a search bar using Apple component like UISearchBar with swiftui on tvos ?
I tried this UISearchBar(frame: .zero) but I got this error init(frame:)' is unavailable in tvOS
I only found solutions for ios.
The scheme of initial setup should be like below. Of course, the logic of searching/filtering/showing results is app specific.
import SwiftUI
import TVUIKit
struct SearchView: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<SearchView>) -> UINavigationController {
let controller = UISearchController(searchResultsController: context.coordinator)
controller.searchResultsUpdater = context.coordinator
return UINavigationController(rootViewController: UISearchContainerViewController(searchController: controller))
}
func updateUIViewController(_ uiViewController: UINavigationController, context: UIViewControllerRepresentableContext<SearchView>) {
}
func makeCoordinator() -> SearchView.Coordinator {
Coordinator()
}
typealias UIViewControllerType = UINavigationController
class Coordinator: UIViewController, UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// do here what's needed
}
}
}
struct ContentView: View {
#State private var text: String = ""
var body: some View {
VStack {
SearchView()
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}