What is the best way open UIKit ViewController from SwiftUI - swiftui

I am not getting the proper solution, How to open UIKit ViewController(With Navigation Controller) from SwiftUI Button clicked.

You can do it by using the UIViewControllerRepresentable protocol which is used to manage your UIKit ViewController in the SwiftUI.
Here is the code to show your ViewController from SwiftUI View interface.
SwiftUI View
struct ContentView: View {
#State var isOpenView = false
var body: some View {
NavigationView {
VStack {
//show your view controller
NavigationLink(destination: TestControllerView(), isActive: $isOpenView) {
EmptyView()
}
Button(action: {
self.isOpenView = true
}){
Text("Tap Me")
}
}
}
}
}
Now wrap your ViewController into UIViewControllerRepresentable
struct TestControllerView : UIViewControllerRepresentable {
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeUIViewController(context: Context) -> some UIViewController {
guard let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "TestViewController") as? TestViewController else {
fatalError("ViewController not implemented in storyboard")
}
return viewController
}
}
Here is your ViewController.Swift File code
import UIKit
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func btnBackClicked(_ sender : UIButton) {
self.dismiss(animated: true, completion: nil)
}
}

First, wrap your UIViewController in a UIViewControllerRepresantable like so.
Then present that View using .fullScreenCover

Related

How to make UIViewControllerRepresentable ViewController reusable in many SwiftUI views?

I have managed to create a UIKit UIAlertController and present it to my SwiftUI View. I had to create a UIKit UIAlertController so i can be able to change the colors of the cancel button and submit button as well as make the submit button bold. It worked but now i need to use the UIAlertController in other SwiftUI Views so i have to make it reusable.
How can i make UIViewControllerRepresentable ViewController reusable in many SwiftUI views without having to copy and past the code below? I am thinking of using a class and just instantiate the class and use the UIKit UIAlertController. Any help or small example appreciated.
This is the UIViewControllerRepresentable struct in my SwiftUI view. The code below has to be reusable without having to add it in each SwiftUI view.
struct UIAlertViewPopup: UIViewControllerRepresentable {
#Binding var show: Bool
let viewModel: SettingsViewModel
var title: String
var message: String
func makeUIViewController(context: UIViewControllerRepresentableContext<UIAlertViewPopup>) -> some UIViewController {
return UIViewController()
}
//update UIKit from SwiftUI
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<UIAlertViewPopup>) {
if (self.show) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { (action) in
self.show = false
}
let submitAction = UIAlertAction(title: "Edit profile", style: .default) { (action) in
viewModel(perform: .editProfile)
self.show = false
}
submitAction.setValue(UIColor.accentBlue, forKey: "titleTextColor")
cancelAction.setValue(UIColor.accentPrimary, forKey: "titleTextColor")
alert.addAction(cancelAction)
alert.addAction(submitAction)
alert.preferredAction = submitAction
DispatchQueue.main.async {
uiViewController.present(alert, animated: true, completion: {
self.show = false
})
}
}
}
//update SwiftUI from UIKit through delegates
func makeCoordinator() -> UIAlertViewPopup.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIAlertViewDelegate {
var control: UIAlertViewPopup
init(_ control: UIAlertViewPopup) {
self.control = control
}
}
}
I use it in my SwiftUI view like this
#State var isAlertVisible: Bool = false
Button {
isAlertVisible = true
}
then on the .background modifier
.background(UIAlertViewPopup(show: $viewModel.isAlertVisible, viewModel: viewModel, title: "", message: "hello from UIKit alert"))

Make UIKit ContextMenu preview provider frame fit a SwiftUI view inside a UIHostingControler

I am learning Swift & SwiftUI as a hobby with no UIKit background, so I am not sure if this is currently possible. I would really like to use UIKit's context menus with SwiftUI (e.g. to implement submenus, action attributes and maybe custom preview providers).
My original idea was to create a LegacyContextMenuView with UIViewControllerRepresentable. Then I'd use a UIHostingController to add a SwiftUI View as a child of a UIViewController ContainerViewController to which I'd add a UIContextMenuInteraction.
My current solution kinda works but when the context menu is activated the preview frame of the 'ContainerViewController' view does not fit the size of UIHostingController view. I am not familiar with UIKit's layout system so I'd like to know:
Is it possible to add such constrains while the preview is activated?
Is it possible to preserve the clipShape of the underlying SwiftUI view inside the preview provider?
The code:
// MARK: - Describes a UIKit Context Menu
struct LegacyContextMenu {
let title: String
let actions: [UIAction]
var actionProvider: UIContextMenuActionProvider {
{ _ in
UIMenu(title: title, children: actions)
}
}
init(actions: [UIAction], title: String = "") {
self.actions = actions
self.title = title
}
}
// MARK: - A View that brings UIKit context menus into the SwiftUI world
struct LegacyContextMenuView<Content: View>: UIViewControllerRepresentable {
let content: Content
let menu: LegacyContextMenu
func makeUIViewController(context: Context) -> UIViewController {
let controller = ContainerViewController(rootView: content)
let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator)
controller.view.addInteraction(menuInteraction)
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
class Coordinator: NSObject, UIContextMenuInteractionDelegate {
let parent: LegacyContextMenuView
init(parent: LegacyContextMenuView) { self.parent = parent }
func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration?
{
// previewProvider nil = using the default UIViewController: ContainerViewController
UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: parent.menu.actionProvider)
}
}
class ContainerViewController: UIViewController {
let hostingController: UIHostingController<Content>
init(rootView: Content) {
self.hostingController = UIHostingController(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
setupHostingController()
setupContraints()
// Additional setup required?
}
func setupHostingController() {
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
}
// Not familiar with UIKit's layout system so unsure if this is the best approach
func setupContraints() {
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
hostingController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
hostingController.view.rightAnchor.constraint(equalTo: view.rightAnchor)
])
}
}
}
// MARK: - Simulate SwiftUI syntax
extension View {
func contextMenu(_ legacyContextMenu: LegacyContextMenu) -> some View {
self.modifier(LegacyContextViewModifier(menu: legacyContextMenu))
}
}
struct LegacyContextViewModifier: ViewModifier {
let menu: LegacyContextMenu
func body(content: Content) -> some View {
LegacyContextMenuView(content: content, menu: menu)
}
}
Then to test, I use this:
// MARK - A sample view with custom content shape and a dynamic frame
struct SampleView: View {
#State private var isLarge = false
let viewClipShape = RoundedRectangle(cornerRadius: 50.0)
var body: some View {
ZStack {
Color.blue
Text(isLarge ? "Large" : "Small")
.foregroundColor(.white)
.font(.largeTitle)
}
.onTapGesture { isLarge.toggle() }
.clipShape(viewClipShape)
.contentShape(viewClipShape)
.frame(height: isLarge ? 250 : 150)
.animation(.easeInOut, value: isLarge)
}
}
struct ContentView: View {
var body: some View {
SampleView()
.contextMenu(LegacyContextMenu(actions: [sampleAction], title: "My Menu"))
.padding(.horizontal)
}
let sampleAction = UIAction(
title: "Remove",
image: UIImage(systemName: "trash.fill"),
identifier: nil,
attributes: UIMenuElement.Attributes.destructive,
handler: { _ in print("Pressed 'Remove'") })
}
While long pressing, the context menu scaling animation respects the content shape of SampleView for both small and large sizes, but the preview pops out like this:
you must set preferredContentSize of ViewController to fit content size u want

FSCalendar integration in SwiftUI

I'm currently trying to add a calendar interface in my app where when you click on a day at the bottom it will show details about events on that day. Currently I am using FSCalendar as my calendar.
I realised that this library is for UIKit and I would have to wrap it with representable protocol to get it working in SwiftUI.
I've been watching youtube and looking up guides on integrating UIKit in SwiftUI to help me do this. This is what I have currently:
import SwiftUI
import FSCalendar
CalendarModule.swift:
class CalendarModule: UIViewController, FSCalendarDelegate {
var calendar = FSCalendar()
override func viewDidLoad() {
super.viewDidLoad()
calendar.delegate = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
initCalendar()
view.addSubview(calendar)
}
private func initCalendar() {
calendar.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.width)
calendar.appearance.todayColor = UIColor.systemGreen
calendar.appearance.selectionColor = UIColor.systemBlue
}
}
struct CalendarModuleViewController: UIViewControllerRepresentable {
typealias UIViewControllerType = UIViewController
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarModuleViewController>) -> UIViewController {
let viewController = CalendarModule()
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<CalendarModuleViewController>) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
final class Coordinator: NSObject, FSCalendarDelegate {
private var parent: CalendarModuleViewController
init (_ parent: CalendarModuleViewController) {
self.parent = parent
}
}
}
struct CalendarModuleView: View {
var body: some View {
CalendarModuleViewController()
}
}
CalendarView.swift - Displaying calendar in top half and the details in bottom half:
import SwiftUI
struct CalendarView: View {
var body: some View {
NavigationView {
VStack{
VStack {
Spacer()
CalendarModuleView()
Spacer()
}
VStack {
Spacer()
Text("Details")
Spacer()
}
}.navigationBarTitle(Text("Calendar"), displayMode: .inline)
.navigationBarItems(trailing:
NavigationLink(destination: CreateEventView().environmentObject(Event())) {
Image(systemName: "plus").imageScale(.large)
}.buttonStyle(DefaultButtonStyle())
)
}
}
}
struct CalendarView_Previews: PreviewProvider {
static var previews: some View {
CalendarView()
}
}
ContentView - Just displaying my CalendarView:
import SwiftUI
struct ContentView: View {
var body: some View {
CalendarView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The calendar looks like this so far:
The calendar module itself works but I got stuck in writing the Coordinator to handle the delegates. Specifically the didSelectDate one..
When I start typing didSelectDate in the Coordinator class and look through the suggestions I only get
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
<#code#>
}
Am I wrapping the view controller wrong?
Or should I be making my own UIView and UIViewController for FSCalendar then create UIViewRepresentable and UIViewControllerRepresentable to use in SwiftUI?
This is an old question, but still.
I think the problem is that you assign CalendarModule as a delegate and not the Coordinator
you need to write the following code:
func makeUIViewController(context: UIViewControllerRepresentableContext<CalendarModuleViewController>) -> UIViewController {
let viewController = CalendarModule()
viewController.calendar.delegate = context.coordinator // ->> Add this
return viewController
}
After that, you should be able to implement delegate methods in the Coordinator

Custom UIViewController with UIViewcontrollerRepresentable that has a UITextView that crashes or is nil when called in SwiftUI

I have made a custom UIViewController called ViewControllerA and want to be able to use it so I made a UIViewControllerRepresentable called ViewControllerARepresentable as shown below, the problem though is that when I call ViewControllerARepresentable in my SwiftUI view and pass a value for stringToUpdateTextView, the ViewControllerA says the htmlTextView(UITextView) in ViewControllerA is nil and I'm not sure why.
ViewControllerARepresentable(stringToUpdateTextView: "<html>Send some Html Text as string here</html>")
ViewControllerARepresentable
public struct ViewControllerARepresentable: UIViewControllerRepresentable {
var stringToUpdateTextView: String
public func makeUIViewController(context: Context) -> ViewControllerA {
let viewcontrollerA = ViewControllerA(testString: testingString)
return viewcontrollerA
}
public func updateUIViewController(_ uiViewController: ViewControllerA, context: Context) {}
}
ViewControllerA
open class ViewControllerA: UIViewController {
public var stringToUpdateTextView: String
override open func viewDidLoad() {
super.viewDidLoad()
htmlTextView.text = stringToUpdateTextView
}
#IBOutlet weak var htmlTextView: UITextView!
public init(testString: String) {
self.testString = testString
super.init(nibName: nil, bundle: nil)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Crash occurs at htmlTextView.text = stringToUpdateTextView saying that htmlTextView.text is nil even though its an IBOutlet.
Any Change made to the htmlTextView like background color ,etc, also causes a crash if called in viewDidAppear or viewDidLoad
When instantiating your view controller in makeUIViewController, the outlets haven't been initialised yet.
The following code loads your view controller from the storyboard, and updates the properties in updateUIViewController:
ViewController.swift
import UIKit
import SwiftUI
class ViewController: UIViewController {
#IBOutlet weak var htmlTextView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
struct ViewControllerWrapper: UIViewControllerRepresentable {
typealias UIViewControllerType = ViewController
#Binding var text: String
func makeUIViewController(context: Context) -> ViewController {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let viewController = storyboard.instantiateViewController(
identifier: "ViewController") as? ViewController else {
fatalError("Cannot load from storyboard")
}
return viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
uiViewController.htmlTextView.text = text
}
}
struct ViewControllerPreview: PreviewProvider {
static var previews: some View {
ViewControllerWrapper(text: .constant("hello world!"))
}
}
SwiftUIView.swift
struct SwiftUIView: View {
#State var text = "Text"
var body: some View {
HStack {
TextField("Text:", text: $text)
ViewControllerWrapper(text: $text)
}
}
}

Disable swipe-back for a NavigationLink SwiftUI

How can I disable the swipe-back gesture in SwiftUI? The child view should only be dismissed with a back-button.
By hiding the back-button in the navigation bar, the swipe-back gesture is disabled. You can set a custom back-button with .navigationBarItems()
struct ContentView: View {
var body: some View {
NavigationView{
List{
NavigationLink(destination: Text("You can swipe back")){
Text("Child 1")
}
NavigationLink(destination: ChildView()){
Text("Child 2")
}
}
}
}
}
struct ChildView: View{
#Environment(\.presentationMode) var presentationMode
var body:some View{
Text("You cannot swipe back")
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: Button("Back"){self.presentationMode.wrappedValue.dismiss()})
}
}
I use Introspect library then I just do:
import SwiftUI
import Introspect
struct ContentView: View {
var body: some View {
Text("A view that cannot be swiped back")
.introspectNavigationController { navigationController in
navigationController.interactivePopGestureRecognizer?.isEnabled = false
}
}
}
Only complete removal of the gesture recognizer worked for me.
I wrapped it up into a single modifier (to be added to the detail view).
struct ContentView: View {
var body: some View {
VStack {
...
)
.disableSwipeBack()
}
}
DisableSwipeBack.swift
import Foundation
import SwiftUI
extension View {
func disableSwipeBack() -> some View {
self.background(
DisableSwipeBackView()
)
}
}
struct DisableSwipeBackView: UIViewControllerRepresentable {
typealias UIViewControllerType = DisableSwipeBackViewController
func makeUIViewController(context: Context) -> UIViewControllerType {
UIViewControllerType()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
class DisableSwipeBackViewController: UIViewController {
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent?.parent,
let navigationController = parent.navigationController,
let interactivePopGestureRecognizer = navigationController.interactivePopGestureRecognizer {
navigationController.view.removeGestureRecognizer(interactivePopGestureRecognizer)
}
}
}
You can resolve the navigation controller without third party by using a UIViewControllerRepresentable in the SwiftUI hierarchy, then access the parent of its parent.
Adding this extension worked for me (disables swipe back everywhere, and another way of disabling the gesture recognizer):
extension UINavigationController: UIGestureRecognizerDelegate {
override open func viewDidLoad() {
super.viewDidLoad()
interactivePopGestureRecognizer?.delegate = self
}
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
}
This answer shows how to configure your navigation controller in SwiftUI (In short, use UIViewControllerRepresentable to gain access to the UINavigationController). And this answer shows how to disable the swipe gesture. Combining them, we can do something like:
Text("Hello")
.background(NavigationConfigurator { nc in
nc.interactivePopGestureRecognizer?.isEnabled = false
})
This way you can continue to use the built in back button functionality.
Setting navigationBarBackButtonHidden to true will lose the beautiful animation when you have set the navigationTitle.
So I tried another answer
navigationController.interactivePopGestureRecognizer?.isEnabled = false
But It's not working for me.
After trying the following code works fine
NavigationLink(destination: CustomView()).introspectNavigationController {navController in
navController.view.gestureRecognizers = []
}
preview
The following more replicates the existing iOS chevron image.
For the accepted answer.
That is replace the "back" with image chevron.
.navigationBarItems(leading: Button("Back"){self.presentationMode.wrappedValue.dismiss()})
With
Button(action: {self.presentationMode.wrappedValue.dismiss()}){Image(systemName: "chevron.left").foregroundColor(Color.blue).font(Font.system(size:23, design: .serif)).padding(.leading,-6)}