Making View conform to UINavigationControllerDelegate and UIImagePickerControllerDelegate - swift

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")
}
}
}

Related

Dismiss SwiftUI View Embedded in UINavigationController

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)

Share Menu Screen Position on Mac(Catalyst)

I created a ShareButton like this:
struct ShareButton<Content: View>: View {
var items: [Any]
var content: () -> Content
var body: some View {
Button {
let avc = UIActivityViewController(activityItems: items, applicationActivities: [])
avc.popoverPresentationController?.sourceView = UIHostingController(rootView: self).view
UIApplication.shared.windows.filter({$0.isKeyWindow}).first?.rootViewController?.present(avc, animated: false)
} label: {
content()
}
}
}
this can be used like this:
ShareButton(items: [someURL]){
Label("share", systemImage:"square.and.arrow.up")
}
However, on Mac(Catalyst) and iPad the share-menu-popup appears in the wrong place.
It seems that UIHostingController(rootView: self).view returns the wrong reference view. Does anybody know how to fix this?

Implement delegates within SwiftUI Views

I am trying to implement a functionality that requires a delegate method (like NSUserActivity). Therefore I need a UIViewController that conforms to NSUserActivityDelegate (or similar other delegates), handles and hold all the required information. My problem is that I am using SwiftUI for my interface and therefore I am not using UIViewControllers. So how can I implement this functionality and still use SwiftUI for the UI. What I tried: view1 is just a normal SwiftUI View that can present (via NavigationLink) view2 which is the view where in want to implement this functionality. So I tried instead of linking view1 and view2, linking view1 to a UIViewControllerRepresentable which then handles the implementation of this functionality and adds UIHostingController(rootView: view2) as a child view controller.
struct view1: View {
var body: some View {
NavigationLink(destination: VCRepresentable()) {
Text("Some Label")
}
}
}
struct view2: View {
var body: some View {
Text("Hello World!")
}
}
struct VCRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return implementationVC()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
class implementationVC: UIViewController, SomeDelegate for functionality {
// does implementation stuff in delegate methods
...
override func viewDidLoad() {
super.viewDidLoad()
attachChild(UIHostingController(rootView: view2()))
}
private func attachChild(_ viewController: UIViewController) {
addChild(viewController)
if let subview = viewController.view {
subview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(subview)
subview.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
subview.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
subview.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
subview.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
viewController.didMove(toParent: self)
}
}
I am having trouble with transferring the data between my VC and my view2. So I'm wondering if there is a better way to implement such a functionality within a SwiftUI View.
You need to create a view that conforms to UIViewControllerRepresentable and has a Coordinator that handles all of the delegate functionality.
For example, with your example view controller and delegates:
struct SomeDelegateObserver: UIViewControllerRepresentable {
let vc = SomeViewController()
var foo: (Data) -> Void
func makeUIViewController(context: Context) -> SomeViewController {
return vc
}
func updateUIViewController(_ uiViewController: SomeViewController, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(vc: vc, foo: foo)
}
class Coordinator: NSObject, SomeDelegate {
var foo: (Data) -> Void
init(vc: SomeViewController, foo: #escaping (Data) -> Void) {
self.foo = foo
super.init()
vc.delegate = self
}
func someDelegateFunction(data: Data) {
foo(data)
}
}
}
Usage:
struct ContentView: View {
var dataModel: DataModel
var body: some View {
NavigationLink(destination: CustomView(numberFromPreviousView: 10)) {
Text("Go to VCRepresentable")
}
}
}
struct CustomView: View {
#State var instanceData1: String = ""
#State var instanceData2: Data?
var numberFromPreviousView: Int // example of data passed from the previous view to this view, the one that can react to the delegate's functions
var body: some View {
ZStack {
SomeDelegateObserver { data in
print("Some delegate function was executed.")
self.instanceData1 = "Executed!"
self.instanceData2 = data
}
VStack {
Text("This is the UI")
Text("That, in UIKit, you would have in the UIViewController")
Text("That conforms to whatever delegate")
Text("SomeDelegateObserver is observing.")
Spacer()
Text(instanceData1)
}
}
}
}
Note: I renamed VCRepresentable to SomeDelegateObserver to be more indicative of what it does: Its sole purpose is to wait for delegate functions to execute and then run the closures (i.e foo in this example) you provide it. You can use this pattern to create as many functions as you need to "observe" whatever delegate functions you care about, and then execute code that can update the UI, your data model, etc. In my example, when SomeDelegate fires someDelegateFunction(data:), the view will display "Excuted" and update the data instance variable.

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
}
}

LargeTitles UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset

I have implemented large titles in my app with the following code:
if #available(iOS 11.0, *) {
navigationController?.navigationBar.prefersLargeTitles = true
navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
} else {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never
} else {
// Fallback on earlier versions
}
}
self.navigationController?.navigationBar.setNeedsLayout()
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.01, animations: {
self.navigationController?.navigationBar.layoutIfNeeded()
self.view.layoutIfNeeded()
})
}
I am able to successfully toggle between views on a tabbar but when I push a view ontop of the tabbar controller and then pop it off using this code:
_ = self.navigationController?.popViewController(animated: true)
I get this crash when I toggle between views on the tabbar again:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'ERROR: UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset:'
This is not a solution, but a potential thing that you need to investigate in your code. I got this same error message (UIScrollView does not support multiple observers implementing _scrollViewWillEndDraggingWithVelocity:targetContentOffset) and I noticed I was doing something incorrectly.
I got this error message in a SwiftUI app using NavigationView.
The mistake I had made was that ParentView had a Navigation View at the root. Using a NavigationLink I was moving to ChildView, which also had a NavigationView as the root. Here's what it looked like in code:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ParentView()
}
}
}
struct ParentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: ChildView()) {
Text("Parent view")
}
}
.navigationTitle("Parent")
}
}
}
struct ChildView: View {
var body: some View {
List {
ForEach(0 ..< 5) { _ in
Text("Child view")
}
}
.navigationTitle("Child")
}
}
Initially this is what ChildView looked like:
struct ChildView: View {
var body: some View {
NavigationView {
List {
ForEach(0 ..< 5) { _ in
Text("Second screen")
}
}
.navigationTitle("Second")
}
}
}
Notice how I was trying to push a view which itself was embedded in a NavigationView. Removing it as shown in the first snippet, took care of the error messages. You can try looking into that, maybe you are doing the same mistake just in UIKit instead of SwiftUI.
I found the solution. You have to set the first navigation controller to not use large titles.
The point is that now UIScrollView has only one observer (navigationController) implementing _scrollViewWillEndDraggingWithVelocity.
if (#available(iOS 11.0, *)) {
self.navigationController.navigationBar.prefersLargeTitles = FALSE;
self.navigationController.navigationItem.largeTitleDisplayMode = UINavigationItemLargeTitleDisplayModeNever;
}
The problem happened when the tableview was still scrolling when I went to another view. I fixed the problem by setting a bool in the scrollViewDidScroll that disables any scrolling when the segue is started.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if viewIsVisible {
if scrollView.contentOffset.y <= 0 {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .always
} else {
// Fallback on earlier versions
}
} else {
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never
} else {
// Fallback on earlier versions
}
}
self.navigationController?.navigationBar.setNeedsLayout()
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.01, animations: {
self.navigationController?.navigationBar.layoutIfNeeded()
self.view.layoutIfNeeded()
})
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
self.viewIsVisible = false
}
I've the same problem and I fixed it by removing this line from AppDelegate:
UINavigationBar.appearance().prefersLargeTitles = true
and handle prefersLargeTitles inside viewDidLoad in certain UIViewController
I think all of above answers don't really solve the issue and are overcomplicated. I recommend enabling/disabling large titles in each of your UIViewController's subclasses, so they don't use large titles at the same time. Good place to do it is in the viewWillAppear and viewWillDisappear methods
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.largeTitleDisplayMode = .always
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.navigationBar.prefersLargeTitles = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.navigationBar.prefersLargeTitles = false
}