Share Menu Screen Position on Mac(Catalyst) - swift

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?

Related

SwiftUI: Display file url from Array after user Picks file using DocumentPicker

I followed a tutorial on getting Url from a Document a user chooses and be able to display it on the View. My problem now is I want to add those Url's into an array. Then get the items from the array and print them onto the View. The way it works is the User presses a button and a sheet pops up with the files app. There the user is able to choose a document. After the user chooses the document the Url is printed on the View. To print the Url is use this
//if documentUrl has an Url show it on the view
If let url= documentUrl{
Text(url.absoluteString)
}
Issue with this is that when I do the same thing the
If let url= documentUrl
Is ran before the Url is even added to the array and the app crashes
Here is the full code
//Add the Urls to the array
class Article: ObservableObject{
var myArray:[String] = []
}
struct ContentView: View {
#State private var showDocumentPicker = false
#State private var documentUrl:URL?
#State var myString:URL?
#ObservedObject var userData:Article
// Func for onDismiss from the Sheet
func upload() {
// add the Url to the Array
DispatchQueue.main.async{
userData.myArray.append(documentUrl!.absoluteString)
}
}
var body: some View {
VStack{
//If have Url reflect that on the View
if let url = documentUrl{
//Works
Text(url.absoluteString)
//doesntwork
Text(userData.myArray[0])
}
}
Button(action:{showDocumentPicker.toggle()},
label: {
Text("Select your file")
})
.sheet(isPresented: $showDocumentPicker, onDismiss: upload )
{
DocumentPicker(url: $documentUrl)
}
}
}
The main thing I want to do the just display the ulrs into the view after the user chooses the document or after the sheet disappears. So if the user chooses 1 Url only one is printed. If another one is chosen after then 2 are show etc.
This is the documentPicker code used to choose a document
struct DocumentPicker : UIViewControllerRepresentable{
#Binding var url : URL?
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
//initialize a UI Document Picker
let viewController = UIDocumentPickerViewController(forOpeningContentTypes: [.epub])
viewController.delegate = context.coordinator
print("1")
return viewController
}
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {
print("Swift just updated ")
print("2")
}
}
extension DocumentPicker{
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator:NSObject, UIDocumentPickerDelegate{
let parent: DocumentPicker
init(_ documentPicker: DocumentPicker){
self.parent = documentPicker
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else{return}
parent.url = url
print("3")
}
}
}
Not sure if maybe I'm not approaching this the correct way? I looked at different tutorial but couldn't find anything.
Use .fileImporter presentation modifire (above ios 14)
.fileImporter(isPresented: $showDocumentPicker,
allowedContentTypes: [.image],
allowsMultipleSelection: true)
{ result in
// processing results Result<[URL], Error>
}
An observable object doesn't have a change trigger. To inform that the observable object has changed use one of the following examples:
class Article: ObservableObject {
#Published var myArray:[String] = []
}
or
class Article: ObservableObject {
private(set) var myArray:[String] = [] {
willSet {
objectWillChange.send()
}
}
func addUrl(url: String) {
myArray.append(url)
}
}
official documentation: https://developer.apple.com/documentation/combine/observableobject

Present embedded SwiftUI Subview in Full Screen, UIHostingController

I work on a Storyboard Swift App want to present a SwiftUI Subview in Full-Screen. I embedded the SwiftUI View using UIHostingController, like this:
let contentView = UIHostingController(rootView: ContentView())
override func viewDidLoad() {
super.viewDidLoad()
addChild(contentView)
view.addSubview(contentView.view)
}
And this is my SwiftUI ContentView() Subview:
import SwiftUI
import UIKit
struct ContentView: View {
var body: some View {
Text("").fullScreenCover(isPresented: /*#START_MENU_TOKEN#*/.constant(true)/*#END_MENU_TOKEN#*/, content: {
FullScreenView.init()
})
}
}
struct FullScreenView: View{
var body: some View {
NavigationView{
MasterView()
}.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
struct MasterView: View {
var body: some View {
Form {
Section(header: Text("HEADER")) {
Section {
NavigationLink(destination: UIKitView()) { Text("NAVLINK TEXT") }
}
}
}
.navigationBarTitle("NAVBAR TEXT")
}
}
struct UIKitView: UIViewControllerRepresentable {
typealias UIViewControllerType = SwipeViewController
func makeUIViewController(context: Context) -> SwipeViewController {
let sb = UIStoryboard(name: "Storyboard", bundle: nil)
let viewController = sb.instantiateViewController(identifier: "vc") as! SwipeViewController
return viewController
}
func updateUIViewController(_ uiViewController: SwipeViewController, context: Context) {
}
}
Is there a better way to do it? As you can see, I use an empty text field to add the .fullScreenCover :/
Thanks for any help!!

SwiftUI using NSSharingServicePicker in MacOS

I am trying to use a Share function inside my MacOS app in SwiftUI. I am having a URL to a file, which I want to share. It can be images/ documents and much more.
I found NSSharingServicePicker for MacOS and would like to use it. However, I am struggeling to use it in SwiftUI.
Following the documentation, I am creating it like this:
let shareItems = [...]
let sharingPicker : NSSharingServicePicker = NSSharingServicePicker.init(items: shareItems as [Any])
sharingPicker.show(relativeTo: NSZeroRect, of:shareView, preferredEdge: .minY)
My problem is in that show() method. I need to set a NSRect, where I can use NSZeroRect.. but I am struggeling with of: parameter. It requires a NSView. How can I convert my current view as NSView and use it that way. Or can I use my Button as NSView(). I am struggling with that approach.
Another option would be to use a NSViewRepresentable. But should I just create a NSView and use it for that method.
Here is minimal working demo example
struct SharingsPicker: NSViewRepresentable {
#Binding var isPresented: Bool
var sharingItems: [Any] = []
func makeNSView(context: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
let picker = NSSharingServicePicker(items: sharingItems)
picker.delegate = context.coordinator
// !! MUST BE CALLED IN ASYNC, otherwise blocks update
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(owner: self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
let owner: SharingsPicker
init(owner: SharingsPicker) {
self.owner = owner
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) {
// do here whatever more needed here with selected service
sharingServicePicker.delegate = nil // << cleanup
self.owner.isPresented = false // << dismiss
}
}
}
Demo of usage:
struct TestSharingService: View {
#State private var showPicker = false
var body: some View {
Button("Share") {
self.showPicker = true
}
.background(SharingsPicker(isPresented: $showPicker, sharingItems: ["Message"]))
}
}
Another option without using NSViewRepresentable is:
extension NSSharingService {
static func submenu(text: String) -> some View {
return Menu(
content: {
ForEach(items, id: \.title) { item in
Button(action: { item.perform(withItems: [text]) }) {
Image(nsImage: item.image)
Text(item.title)
}
}
},
label: {
Image(systemName: "square.and.arrow.up")
}
)
}
}
You lose things like the "more" menu item or recent recipients. But in my opinion it's more than enough, simple and pure SwiftUI.

LinkPresentation views not fully loading in SwiftUI

I'm using code I found from a great article here that demonstrates how to use the LinkPresentation framework in SwiftUI.
However I'm having a small problem that I can't find solution to - the link previews loads their metadata but don't refresh the view once fully loaded unless I do something which forces the view to refresh, like rotating the phone.
They load as much as this:
Then look like this after rotating:
I'd like the views to fully refresh once the metadata is loaded. I feel like I probably need to add some binding in somewhere but I don't know where. Can anyone help at all?
Here's the UIViewRepresentable
import SwiftUI
import LinkPresentation
struct URLPreview : UIViewRepresentable {
var previewURL:URL
func makeUIView(context: Context) -> LPLinkView {
LPLinkView(url: previewURL)
}
func updateUIView(_ view: LPLinkView, context: Context) {
// New instance for each update
let provider = LPMetadataProvider()
provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
if let md = metadata {
DispatchQueue.main.async {
view.metadata = md
view.sizeToFit()
}
}
}
}
}
and here's how it's called:
struct Content: View {
var body: some View {
URLPreview(previewURL: URL(string: "www.apple.com")!)
}
}
Triggering a redraw is what you need. Not a fan of this, but you can try Binding a State CGSize and set frame to width/height.
struct URLPreview : UIViewRepresentable {
var previewURL:URL
//Add binding
#Binding var metaSize: CGSize
func makeUIView(context: Context) -> LPLinkView {
LPLinkView(url: previewURL)
}
func updateUIView(_ view: LPLinkView, context: Context) {
// New instance for each update
let provider = LPMetadataProvider()
provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
if let md = metadata {
DispatchQueue.main.async {
view.metadata = md
view.sizeToFit()
//Set binding after resize
self.metaSize = view.frame.size
}
}
}
}
}
struct ContentView: View {
//can default original state
#State var metaSize: CGSize = CGSize()
var body: some View {
URLPreview(previewURL: URL(string: "www.apple.com")!, metaSize: $metaSize)
.frame(width: metaSize.width, height: metaSize.height)
}
}
UPDATE
NSPratik is right, the solution is not really viable for Lists. So an amended solution is actually just to use a simple Bool State to toggle the Views generated by a list:
struct ContentView: View {
//can default original state
#State var togglePreview = false
let urls: [String] = ["https://medium.com","https://apple.com","https://yahoo.com","https://stackoverflow.com"]
var body: some View {
List(urls, id: \.self) { url in
URLPreview(previewURL: URL(string: url)!, togglePreview: self.$togglePreview)
.aspectRatio(contentMode: .fit)
.padding()
}
}
}
struct URLPreview : UIViewRepresentable {
var previewURL:URL
//Add binding
#Binding var togglePreview: Bool
func makeUIView(context: Context) -> LPLinkView {
let view = LPLinkView(url: previewURL)
let provider = LPMetadataProvider()
provider.startFetchingMetadata(for: previewURL) { (metadata, error) in
if let md = metadata {
DispatchQueue.main.async {
view.metadata = md
view.sizeToFit()
self.togglePreview.toggle()
}
}
}
return view
}
func updateUIView(_ uiView: LPLinkView, context: UIViewRepresentableContext<URLPreview>) {
}
}
We simply use togglePreview as our trigger, pass it to a Binding var in the UIView, and then setup our List. Even if this triggers all the Views in the List, there won't be any animation to reflect the resize of fully loaded LinkViews.
Using LPLinkViews in a List causes huge memory leaks. Your best bet is to use a VStack embedded inside a ScrollView.
ScrollView {
VStack {
ForEach(links, id: \.self) { link in
if let url = URL(string: link) {
LinkRow(url: url)
}
}
}
.padding()
}
This will make the LPLinkViews resize themselves as they load.
I have done this in an app and it has significant improvement over using a List. However a little caveat, if the user stars scrolling up and down as soon as the view comes on screen while the previews are still loading, it might causes crashes at random. Unfortunately I haven't been able to find a solution for that yet. I think all these crashes happen because the LPMetadataProvider requires you to be called on the main thread and obviously that doesn't play well with smooth scrolling.

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