Reputation: 2282
I'm trying to present a UIActivityViewController
(share sheet) from a SwiftUI View. I created a view called ShareSheet
conformed to UIViewControllerRepresentable
to configure the UIActivityViewController
, but it's turning out to be not as trivial to actually present this.
struct ShareSheet: UIViewControllerRepresentable {
typealias UIViewControllerType = UIActivityViewController
var sharing: [Any]
func makeUIViewController(context: UIViewControllerRepresentableContext<ShareSheet>) -> UIActivityViewController {
UIActivityViewController(activityItems: sharing, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ShareSheet>) {
}
}
Doing so naively via .sheet
leads to the following.
.sheet(isPresented: $showShareSheet) {
ShareSheet(sharing: [URL(string: "https://example.com")!])
}
Is there a way to present this like it's usually presented? As in covering half the screen?
Upvotes: 24
Views: 8306
Reputation: 861
Starting from iOS 16.0 there is view modifier .presentationDetents
to accomplish it. Here is example:
struct ContentView: View {
@State private var showShareSheet = false
var body: some View {
Button("Share) {
showShareSheet = true
}
.sheet(isPresented: $showShareSheet) {
ShareSheet(sharing: [URL(string: "https://example.com")!])
.presentationDetents([.medium])
}
}
}
The key is .medium
value for the .presentationDetents
view modifier.
Upvotes: 1
Reputation: 607
Extension to get the top presented UIViewController:
import UIKit
extension UIApplication {
// MARK: No shame!
static func TopPresentedViewController() -> UIViewController? {
guard let rootViewController = UIApplication.shared
.connectedScenes.lazy
.compactMap({ $0.activationState == .foregroundActive ? ($0 as? UIWindowScene) : nil })
.first(where: { $0.keyWindow != nil })?
.keyWindow?
.rootViewController
else {
return nil
}
var topController = rootViewController
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
}
Then use it to present your UIActivityViewController:
UIApplication.TopPresentedViewController?.present(activityViewController, animated: true, completion: nil)
Original Answer (deprecated code):
It's not pretty but you can call it directly like this (considering your app has only 1 window):
UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true, completion: nil)
And if you get some warning blablabla:
Warning: Attempt to present ... which is already presenting ...
you can do something like this to get the top most view controller and call present on it.
Upvotes: 4
Reputation: 4971
In iOS 14, Swift 5, Xcode 12.5 at least, I was able to accomplish this fairly easily by simply wrapping the UIActivityViewController
in another view controller. It doesn't require inspecting the view hierarchy or using any 3rd party libraries. The only hackish part is asynchronously presenting the view controller, which might not even be necessary. Someone with more SwiftUI experience might be able to offer suggestions for improvement.
import Foundation
import SwiftUI
import UIKit
struct ActivityViewController: UIViewControllerRepresentable {
@Binding var shareURL: URL?
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> some UIViewController {
let containerViewController = UIViewController()
return containerViewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
guard let shareURL = shareURL, context.coordinator.presented == false else { return }
context.coordinator.presented = true
let activityViewController = UIActivityViewController(activityItems: [shareURL], applicationActivities: nil)
activityViewController.completionWithItemsHandler = { activity, completed, returnedItems, activityError in
self.shareURL = nil
context.coordinator.presented = false
if completed {
// ...
} else {
// ...
}
}
// Executing this asynchronously might not be necessary but some of my tests
// failed because the view wasn't yet in the view hierarchy on the first pass of updateUIViewController
//
// There might be a better way to test for that condition in the guard statement and execute this
// synchronously if we can be be sure updateUIViewController is invoked at least once after the view is added
DispatchQueue.main.asyncAfter(deadline: .now()) {
uiViewController.present(activityViewController, animated: true)
}
}
class Coordinator: NSObject {
let parent: ActivityViewController
var presented: Bool = false
init(_ parent: ActivityViewController) {
self.parent = parent
}
}
}
struct ContentView: View {
@State var shareURL: URL? = nil
var body: some View {
ZStack {
Button(action: { shareURL = URL(string: "https://apple.com") }) {
Text("Share")
.foregroundColor(.white)
.padding()
}
.background(Color.blue)
if shareURL != nil {
ActivityViewController(shareURL: $shareURL)
}
}
.frame(width: 375, height: 812)
}
}
Upvotes: 5
Reputation: 2391
Hope this will help you,
struct ShareSheetView: View {
var body: some View {
Button(action: actionSheet) {
Image(systemName: "square.and.arrow.up")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 36, height: 36)
}
}
func actionSheet() {
guard let data = URL(string: "https://www.apple.com") else { return }
let av = UIActivityViewController(activityItems: [data], applicationActivities: nil)
UIApplication.shared.windows.first?.rootViewController?.present(av, animated: true, completion: nil)
}
}
Upvotes: 10
Reputation: 27214
There's a UIModalPresentationStyle
which can be used to display certain presentations:
case pageSheet
A presentation style that partially covers the underlying content.
The way you apply the presentation style:
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
let v = UIActivityViewController(activityItems: sharing, applicationActivities: nil)
v.modalPresentationStyle = .pageSheet
return v
}
A list of the Presentations can be found here:
https://developer.apple.com/documentation/uikit/uimodalpresentationstyle
I haven't yet tested them all myself so I apologise in advance if this didn't end up working like you expected it to.
Alternatively you can have a look at this answer where they mention a third-party library, which will allow you to create a half modal in the way that it's usually presented.
Upvotes: 2