Dismiss SwiftUI View Embedded in UINavigationController - swift

I am experiencing an issue when deep linking into a certain SwiftUI view from a link. the openTakeVC is what is called when deep linked. Currently it was to be embedded in a UINavigationController in order to work, if I try just presenting the UIHostingController I get a crash with this error:
Thread 1: "Application tried to present modally a view controller <_TtGC7SwiftUI19UIHostingControllerV8uSTADIUM8TakeView_: 0x14680a000> that has a parent view controller <UINavigationController: 0x1461af000>."
The dismiss functionality works perfectly fine if not embedded in a UINavigationController but I am only able to deep link that view using a UINavigationController.
Is there a fix for this error or a way to dismiss a UIHostingController embedded in a UINavigationController?
func openTakeVC(take: TakeOBJ) {
DispatchQueue.main.async {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
if let _ = appDelegate.window?.rootViewController as? BannedViewController { return }
//let vc = TakeSingleViewController(nibName: "TakeSingleView", bundle: nil, take: take)
let vc = UIHostingController(rootView: TakeView(take: take))
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
nav.setNavigationBarHidden(true, animated: false)
appDelegate.window?.rootViewController?.present(vc, animated: true, completion: nil)
UserDefaults.removeURLToContinue()
}
}
in TakeView
#Environment(\.presentationMode) var presentationMode
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "xmark")
}
}

What about creating the hostingcontroller as a separate vc and dismissing it from there?
TakeSingleVC:
final class TakeSingleVC: UIViewController {
var viewModel: TakeViewModel
var subscriptions = Set<AnyCancellable>()
init(viewModel: TakeViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let childView = UIHostingController(rootView: TakeView(viewModel: viewModel))
addChild(childView)
childView.view.frame = view.bounds
view.addSubview(childView.view)
childView.didMove(toParent: self)
viewModel.dismissSheet
.sink { isDismissed in
if isDismissed {
childView.dismiss(animated: true)
}
}.store(in: &subscriptions)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Dismissing in TakeView
viewModel.dismissSheet.send(true)
TakeViewModel:
final class TakeViewModel: ObservableObject {
// UIKit
var dismissSheet = CurrentValueSubject<Bool, Never>(false)
}
and then change your presentation to
let vc = TakeSingleVC(viewModel: viewModel)
let nav = UINavigationController(rootViewController: vc)

Related

Making View conform to UINavigationControllerDelegate and UIImagePickerControllerDelegate

I'm trying to follow this tutorial https://www.hackingwithswift.com/example-code/uikit/how-to-take-a-photo-using-the-camera-and-uiimagepickercontroller to use built-in camera in my SwiftUI App.
How do I make my main View conform to both UINavigationControllerDelegate and UIImagePickerControllerDelegate (as stated in the tutorial). Where should I put it:
struct ContentView: View {
var body: some View {
Button (action: {
vc.sourceType = .camera
vc.allowsEditing = true
vc.delegate = self
present(vc, animated: true)
}) {
Text("Start Camera")
}
}
}

Passing data from UIViewControllerRepresentable to UIViewController

I'm trying to pass data from a SwiftUI struct (a first name and a last name) and can't seem to update the variables in my UIViewController with the data in my UIViewControllerRepresentable.
I've checked and confirmed that the data I'm trying to pass in from my SwiftUI view is correct. What do I need to do/change to update the firstName and lastName variables in my UIViewController?
import UIKit
import PDFKit
import SwiftUI
class PDFPreviewViewController: UIViewController {
public var documentData: Data?
var firstName: String = "firstName did not load"
var lastName: String = "lastName did not load"
#IBOutlet weak var pdfView: PDFView!
override func viewDidLoad() {
super.viewDidLoad()
let pdfCreator = PDFCreator(firstName: firstName, lastName: lastName, format: "test format")
documentData = pdfCreator.createReleasePDF()
if let data = documentData {
pdfView.document = PDFDocument(data: data)
pdfView.autoScales = true
}
}
}
struct PDFPreviewController: UIViewControllerRepresentable {
var release: ModelRelease
let vc = PDFPreviewViewController()
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> UIViewController {
let storyboard = UIStoryboard(name: "Preview", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier: "Preview")
vc.firstName = release.firstName
vc.lastName = release.lastName
return controller
}
func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<PDFPreviewController>) {
}
}
struct PDFPreviewControllerWrapper: View {
#Environment(\.presentationMode) var presentationMode
var release: ModelRelease
var body: some View {
NavigationView {
PDFPreviewController(release: release)
.navigationBarTitle(Text("Preview"))
.navigationBarItems(trailing: Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Done")
.fontWeight(.bold)
}
)
}
}
}
Creating view controller representable you set parameters to one controller but return another. Probably you meant the following:
//let vc = PDFPreviewViewController() // don't think it is needed at all
func makeUIViewController(context: UIViewControllerRepresentableContext<PDFPreviewController>) -> UIViewController {
let storyboard = UIStoryboard(name: "Preview", bundle: Bundle.main)
let controller = storyboard.instantiateViewController(identifier:
"Preview") as! PDFPreviewViewController
controller.firstName = release.firstName // << here !
controller.lastName = release.lastName // << and here !
return controller
}

using TabBarController in MVVM-C, coordinator for Tab Bar

I am using MVVM with coordinator pattern for navigation. Navigation is made with pushViewController. Tab Bar Controller is created in TabBarCoordinator class.
class TabBarCoordinator: NSObject, BaseCoordinator {
var rootViewController: UIViewController {
return tabBarController
}
var tabBarController: UITabBarController
var window: UIWindow
private let appDependency: AppDependency
var navigationController: NavigationController
private let navigationObserver: NavigationObserver
var childCoordinators: [BaseCoordinator] = []
var onDidFinish: (() -> Void)?
var topController: UIViewController {
if let lastChild = topCoordinator {
return lastChild.topController
}
var controller: UIViewController = navigationController
while let presentedController = controller.presentedViewController {
controller = presentedController
}
return controller
}
func start() {
let homeCoordinator = HomeCoordinator(navigationObserver: navigationObserver,
navigationController: navigationController,
appDependency: appDependency)
add(child: homeCoordinator)
homeCoordinator.start()
let ordersListCoordinator = OrdersListCoordinator(navigationController: navigationController,
navigationObserver: navigationObserver,
appDependency: appDependency,
filterStatus: nil)
add(child: ordersListCoordinator)
ordersListCoordinator.start()
tabBarController.navigationController?.navigationBar.isTranslucent = true
let homeVC = homeCoordinator.topController
homeVC.tabBarItem = UITabBarItem(title: "home", image: nil, selectedImage: nil)
let ordersListVC = ordersListCoordinator.topController
ordersListVC.tabBarItem = UITabBarItem(title: "orders", image: nil, selectedImage: nil)
let controllers = [homeVC, ordersListVC]
tabBarController.viewControllers = controllers.map { UINavigationController(rootViewController: $0) }
window.rootViewController = tabBarController
window.makeKeyAndVisible()
}
required init(window: UIWindow,
navigationObserver: NavigationObserver,
navigationController: NavigationController,
appDependency: AppDependency
) {
tabBarController = UITabBarController()
self.window = window
self.navigationController = navigationController
self.navigationObserver = navigationObserver
self.appDependency = appDependency
}
}
Here the code of func start() for HomeCoordinator class:
func start() {
guard let user = appDependency.userDataStore.authorizedUserInfo?.user else { return }
let viewModel = HomeViewModel(user: user)
viewModel.delegate = self
let homeViewController = HomeViewController(viewModel: viewModel)
viewController = homeViewController
navigationController.viewControllers = [homeViewController]
}
Tab bar Screenshot
Problems with tab bar:
isTranslucent doesn't hide navBar from TabbarController
the screen is black
views of tabBarItems are not shown, but you can still tap them and screens would change
the second vc is just black screen
Any ideas what's wrong with my TabBarCoordinator class? Thank you in advance

SwiftUI exporting or sharing files

I'm wondering if there is a good way export or share a file through SwiftUI. There doesn't seem to be a way to wrap a UIActivityViewController and present it directly. I've used the UIViewControllerRepresentable to wrap a UIActivityViewController, and it crashes if I, say, present it in a SwiftUI Modal.
I was able to create a generic UIViewController and then from there call a method that presents the UIActivityViewController, but that's a lot of wrapping.
And if we want to share from the Mac using SwiftUI, is there a way to wrap NSSharingServicePicker?
Anyway, if anyone has an example of how they're doing this, it would be much appreciated.
You can define this function anywhere (preferably in the global scope):
#discardableResult
func share(
items: [Any],
excludedActivityTypes: [UIActivity.ActivityType]? = nil
) -> Bool {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return false
}
let vc = UIActivityViewController(
activityItems: items,
applicationActivities: nil
)
vc.excludedActivityTypes = excludedActivityTypes
vc.popoverPresentationController?.sourceView = source.view
source.present(vc, animated: true)
return true
}
You can use this function in a button action, or anywhere else needed:
Button(action: {
share(items: ["This is some text"])
}) {
Text("Share")
}
We can call the UIActivityViewController directly from the View (SwiftUI) without using UIViewControllerRepresentable.
import SwiftUI
enum Coordinator {
static func topViewController(_ viewController: UIViewController? = nil) -> UIViewController? {
let vc = viewController ?? UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController
if let navigationController = vc as? UINavigationController {
return topViewController(navigationController.topViewController)
} else if let tabBarController = vc as? UITabBarController {
return tabBarController.presentedViewController != nil ? topViewController(tabBarController.presentedViewController) : topViewController(tabBarController.selectedViewController)
} else if let presentedViewController = vc?.presentedViewController {
return topViewController(presentedViewController)
}
return vc
}
}
struct ActivityView: View {
var body: some View {
Button(action: {
self.shareApp()
}) {
Text("Share")
}
}
}
extension ActivityView {
func shareApp() {
let textToShare = "something..."
let activityViewController = UIActivityViewController(activityItems: [textToShare], applicationActivities: nil)
let viewController = Coordinator.topViewController()
activityViewController.popoverPresentationController?.sourceView = viewController?.view
viewController?.present(activityViewController, animated: true, completion: nil)
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityView()
}
}
And this is a preview:
Hoping to help someone!
EDIT: Removed all code and references to UIButton.
Thanks to #Matteo_Pacini for his answer to this question for showing us this technique. As with his answer (and comment), (1) this is rough around the edges and (2) I'm not sure this is how Apple wants us to use UIViewControllerRepresentable and I really hope they provide a better SwiftUI ("SwiftierUI"?) replacement in a future beta.
I put in a lot of work in UIKit because I want this to look good on an iPad, where a sourceView is needed for the popover. The real trick is to display a (SwiftUI) View that gets the UIActivityViewController in the view hierarchy and trigger present from UIKit.
My needs were to present a single image to share, so things are targeted in that direction. Let's say you have an image, stored as a #State variable - in my example the image is called vermont.jpg and yes, things are hard-coded for that.
First, create a UIKit class of type `UIViewController to present the share popover:
class ActivityViewController : UIViewController {
var uiImage:UIImage!
#objc func shareImage() {
let vc = UIActivityViewController(activityItems: [uiImage!], applicationActivities: [])
vc.excludedActivityTypes = [
UIActivity.ActivityType.postToWeibo,
UIActivity.ActivityType.assignToContact,
UIActivity.ActivityType.addToReadingList,
UIActivity.ActivityType.postToVimeo,
UIActivity.ActivityType.postToTencentWeibo
]
present(vc,
animated: true,
completion: nil)
vc.popoverPresentationController?.sourceView = self.view
}
}
The main things are;
You need a "wrapper" UIViewController to be able to present things.
You need var uiImage:UIImage! to set the activityItems.
Next up, wrap this into a UIViewControllerRepresentable:
struct SwiftUIActivityViewController : UIViewControllerRepresentable {
let activityViewController = ActivityViewController()
func makeUIViewController(context: Context) -> ActivityViewController {
activityViewController
}
func updateUIViewController(_ uiViewController: ActivityViewController, context: Context) {
//
}
func shareImage(uiImage: UIImage) {
activityViewController.uiImage = uiImage
activityViewController.shareImage()
}
}
The only two things of note are:
Instantiating ActivityViewController to return it up to ContentView
Creating shareImage(uiImage:UIImage) to call it.
Finally, you have ContentView:
struct ContentView : View {
let activityViewController = SwiftUIActivityViewController()
#State var uiImage = UIImage(named: "vermont.jpg")
var body: some View {
VStack {
Button(action: {
self.activityViewController.shareImage(uiImage: self.uiImage!)
}) {
ZStack {
Image(systemName:"square.and.arrow.up").renderingMode(.original).font(Font.title.weight(.regular))
activityViewController
}
}.frame(width: 60, height: 60).border(Color.black, width: 2, cornerRadius: 2)
Divider()
Image(uiImage: uiImage!)
}
}
}
Note that there's some hard-coding and (ugh) force-unwrapping of uiImage, along with an unnecessary use of #State. These are there because I plan to use `UIImagePickerController next to tie this all together.
The things of note here:
Instantiating SwiftUIActivityViewController, and using shareImage as the Button action.
Using it to also be button display. Don't forget, even a UIViewControllerRepresentable is really just considered a SwiftUI View!
Change the name of the image to one you have in your project, and this should work. You'll get a centered 60x60 button with the image below it.
Most of the solutions here forget to populate the share sheet on the iPad.
So, if you intend to have an application not crashing on this device, you can use
this method where popoverController is used and add your desired activityItems as a parameter.
import SwiftUI
/// Share button to populate on any SwiftUI view.
///
struct ShareButton: View {
/// Your items you want to share to the world.
///
let itemsToShare = ["https://itunes.apple.com/app/id1234"]
var body: some View {
Button(action: { showShareSheet(with: itemsToShare) }) {
Image(systemName: "square.and.arrow.up")
.font(.title2)
.foregroundColor(.blue)
}
}
}
extension View {
/// Show the classic Apple share sheet on iPhone and iPad.
///
func showShareSheet(with activityItems: [Any]) {
guard let source = UIApplication.shared.windows.last?.rootViewController else {
return
}
let activityVC = UIActivityViewController(
activityItems: activityItems,
applicationActivities: nil)
if let popoverController = activityVC.popoverPresentationController {
popoverController.sourceView = source.view
popoverController.sourceRect = CGRect(x: source.view.bounds.midX,
y: source.view.bounds.midY,
width: .zero, height: .zero)
popoverController.permittedArrowDirections = []
}
source.present(activityVC, animated: true)
}
}
Take a look at AlanQuatermain -s SwiftUIShareSheetDemo
In a nutshell it looks like this:
#State private var showShareSheet = false
#State public var sharedItems : [Any] = []
Button(action: {
self.sharedItems = [UIImage(systemName: "house")!]
self.showShareSheet = true
}) {
Text("Share")
}.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: self.sharedItems)
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
// nothing to do here
}
}

How to show Tab Bar Controller?

I've tried everything to get a tabbar controller onto MainViewController and nothing seems to work.
Just a quick rundown on how app works:
Storyboard entry is AppContainerViewController and if user is logged in then MainViewController appears as it should however I can't get MainVC to become a TabBar controller to display tab bar for user navigation to various pages.
What am I doing wrong?!
appcontainerviewcontroller
class AppContainerViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
AppManager.shared.appContainer = self
AppManager.shared.showApp()
}
}
import UIKit
import Firebase
import FirebaseDatabase
import FBSDKLoginKit
class AppManager {
static let shared = AppManager()
let storyboard = UIStoryboard(name: "Main", bundle: nil)
var appContainer: AppContainerViewController!
private init() {}
func showApp() {
var viewController: UIViewController
if (Auth.auth().currentUser == nil) && (FBSDKAccessToken.current() == nil) {
viewController = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
} else {
viewController = storyboard.instantiateViewController(withIdentifier: "MainViewController")
}
appContainer.present(viewController, animated: true, completion: nil)
}
func logout() {
let loginManager = FBSDKLoginManager()
loginManager.logOut()
try! Auth.auth().signOut()
appContainer.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
main view controller
import UIKit
import Firebase
import FirebaseDatabase
import FBSDKShareKit
class MainViewController: UIViewController {
#IBOutlet weak var name: UILabel!
#IBOutlet weak var email: UILabel!
#IBAction func logoutPressed(_ sender: Any) {
AppManager.shared.logout()
}
#IBAction func fbSharePressed(_ sender: Any) {
let content = FBSDKShareLinkContent()
content.contentURL = URL(string: "https://advice.com")
content.quote = "Hey, I'm one step closer to getting into the college of my dreams with this app. Download it and let's go together!"
let dialog : FBSDKShareDialog = FBSDKShareDialog()
dialog.fromViewController = self
dialog.shareContent = content
dialog.mode = FBSDKShareDialogMode.automatic
dialog.show()
}
func userProfile() {
guard let uid = Auth.auth().currentUser?.uid else { return }
let ref = Database.database().reference()
ref.child("users").child(uid).observeSingleEvent(of: .value, with: { (snapshot) in
guard let dict = snapshot.value as? [String: Any] else { return }
let user = CurrentUserProfile(uid: uid, dictionary: dict)
self.name.text = user.name
self.email.text = user.email
}, withCancel: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
userProfile()
}
}
Egg on my face. My storyboard IDs were wrong and Embedding MainViewController into a TabBarController via the storyboard and then applying MainVC's storyboard ID to the TabBarController did the trick.