diff --git a/README.md b/README.md index aabb2ec7..8e5fbffe 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Environment: - `requestReview` - `Refreshable` – includes pull-to-refresh  - `ScaledMetric` +- `ShareLink` - `StateObject` - `scrollDisabled` - `scrollDismissesKeyboard` diff --git a/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift b/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift index 50a51fd5..b28f77cc 100644 --- a/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift +++ b/Sources/SwiftUIBackports/Internal/Inspect+UIKit.swift @@ -1,13 +1,5 @@ import SwiftUI -#if os(iOS) -internal typealias PlatformView = UIView -internal typealias PlatformViewController = UIViewController -#elseif os(macOS) -internal typealias PlatformView = NSView -internal typealias PlatformViewController = NSViewController -#endif - #if os(iOS) || os(macOS) internal extension PlatformView { func ancestor(ofType type: ViewType.Type) -> ViewType? { diff --git a/Sources/SwiftUIBackports/Internal/OwningController.swift b/Sources/SwiftUIBackports/Internal/OwningController.swift index fb442510..15e1a9c9 100644 --- a/Sources/SwiftUIBackports/Internal/OwningController.swift +++ b/Sources/SwiftUIBackports/Internal/OwningController.swift @@ -22,9 +22,9 @@ import AppKit public extension NSView { var parentController: NSViewController? { - if let responder = self.next as? NSViewController { + if let responder = self.nextResponder as? NSViewController { return responder - } else if let responder = self.next as? NSView { + } else if let responder = self.nextResponder as? NSView { return responder.parentController } else { return nil diff --git a/Sources/SwiftUIBackports/Internal/Platforms.swift b/Sources/SwiftUIBackports/Internal/Platforms.swift new file mode 100644 index 00000000..12dfe7d0 --- /dev/null +++ b/Sources/SwiftUIBackports/Internal/Platforms.swift @@ -0,0 +1,19 @@ +#if os(iOS) + +import UIKit + +public typealias PlatformImage = UIImage + +internal typealias PlatformView = UIView +internal typealias PlatformViewController = UIViewController + +#elseif os(macOS) + +import AppKit + +public typealias PlatformImage = NSImage + +internal typealias PlatformView = NSView +internal typealias PlatformViewController = NSViewController + +#endif diff --git a/Sources/SwiftUIBackports/Internal/String+LocalizationKey.swift b/Sources/SwiftUIBackports/Internal/String+LocalizationKey.swift new file mode 100644 index 00000000..b00bf6f4 --- /dev/null +++ b/Sources/SwiftUIBackports/Internal/String+LocalizationKey.swift @@ -0,0 +1,12 @@ +import SwiftUI + +extension String { + internal init?(_ stringKey: LocalizedStringKey) { + guard let key = Mirror(reflecting: stringKey).children + .first(where: { $0.label == "key" })?.value as? String else { + return nil + } + + self = NSLocalizedString(key, comment: "") + } +} diff --git a/Sources/SwiftUIBackports/Shared/Label/Label.swift b/Sources/SwiftUIBackports/Shared/Label/Label.swift index becce3c9..becdd180 100644 --- a/Sources/SwiftUIBackports/Shared/Label/Label.swift +++ b/Sources/SwiftUIBackports/Shared/Label/Label.swift @@ -97,7 +97,7 @@ extension Backport where Wrapped == Any { } @MainActor public var body: some View { - if #available(iOS 14, *) { + if #available(iOS 14, macOS 11, *) { SwiftUI.Label { config.title } icon: { diff --git a/Sources/SwiftUIBackports/Shared/Label/Styles/DefaultLabelStyle.swift b/Sources/SwiftUIBackports/Shared/Label/Styles/DefaultLabelStyle.swift index ecc5674c..951fd03e 100644 --- a/Sources/SwiftUIBackports/Shared/Label/Styles/DefaultLabelStyle.swift +++ b/Sources/SwiftUIBackports/Shared/Label/Styles/DefaultLabelStyle.swift @@ -19,11 +19,13 @@ extension Backport where Wrapped == Any { IconOnlyLabelStyle().makeBody(configuration: configuration) } else { TitleAndIconLabelStyle().makeBody(configuration: configuration) + #if os(iOS) .inspect { inspector in inspector.ancestor(ofType: UINavigationBar.self) } customize: { _ in isToolbarElement = true } + #endif } } } diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/ActivityView.swift b/Sources/SwiftUIBackports/Shared/ShareLink/ActivityView.swift deleted file mode 100644 index 44121b16..00000000 --- a/Sources/SwiftUIBackports/Shared/ShareLink/ActivityView.swift +++ /dev/null @@ -1,115 +0,0 @@ -import SwiftUI - -internal extension View { - func activitySheet(isPresented: Binding) -> some View { - background(ActivityView(isPresented: isPresented)) - } -} - -private struct ActivityView: View { - @Binding var isPresented: Bool - var body: some View { - Representable(isPresented: $isPresented) - .edgesIgnoringSafeArea(.all) - } -} - -#if os(iOS) -private extension ActivityView { - struct Representable: UIViewRepresentable { - @Binding var isPresented: Bool - - func makeCoordinator() -> Coordinator { - return Coordinator(parent: self) - } - - func makeUIView(context: Context) -> UIView { - return context.coordinator.view - } - - func updateUIView(_ uiView: UIView, context: Context) { - context.coordinator.parent = self - } - } -} - -private extension ActivityView { - final class Coordinator: NSObject, UIAdaptivePresentationControllerDelegate { - var parent: Representable { - didSet { - updateControllerLifecycle( - from: oldValue.isPresented, - to: parent.isPresented - ) - } - } - - init(parent: Representable) { - self.parent = parent - } - - let view = UIView() - private weak var controller: UIActivityViewController? - - private func updateControllerLifecycle(from oldValue: Bool, to newValue: Bool) { - switch (oldValue, newValue) { - case (false, true): - presentController() - case (true, false): - dismissController() - case (true, true): - updateController() - case (false, false): - break - } - } - - private func presentController() { - let controller = UIActivityViewController(activityItems: ["test"], applicationActivities: nil) - controller.presentationController?.delegate = self - controller.popoverPresentationController?.sourceRect = view.bounds - controller.popoverPresentationController?.sourceView = view - controller.completionWithItemsHandler = { [weak self] activityType, success, items, error in - guard let self else { return } - self.resetItemBinding() - self.dismissController() - } - - guard let presenting = view.owningController else { - resetItemBinding() - return - } - - presenting.present(controller, animated: true) - self.controller = controller - } - - private func updateController() { } - - private func dismissController() { - guard let controller = controller else { return } - controller.presentingViewController?.dismiss(animated: true) - } - - private func resetItemBinding() { - parent.isPresented = false - } - - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - resetItemBinding() - } - } -} - -private extension UIView { - var owningController: UIViewController? { - if let responder = self.next as? UIViewController { - return responder - } else if let responder = self.next as? UIView { - return responder.owningController - } else { - return nil - } - } -} -#endif diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/DefaultShareLinkLabel.swift b/Sources/SwiftUIBackports/Shared/ShareLink/DefaultShareLinkLabel.swift index e887637d..66180ff8 100644 --- a/Sources/SwiftUIBackports/Shared/ShareLink/DefaultShareLinkLabel.swift +++ b/Sources/SwiftUIBackports/Shared/ShareLink/DefaultShareLinkLabel.swift @@ -2,6 +2,7 @@ import SwiftUI public struct DefaultShareLinkLabel: View { let text: Text + private static let shareIcon = "square.and.arrow.up" init() { text = .init("Share") @@ -20,10 +21,25 @@ public struct DefaultShareLinkLabel: View { } public var body: some View { - Backport.Label { - text - } icon: { - Image(systemName: "square.and.arrow.up") + if #available(iOS 14, macOS 11, watchOS 7, tvOS 14, *) { + Label { + text + } icon: { + Image(systemName: Self.shareIcon) + } + } else { + Backport.Label { + text + } icon: { + #if os(macOS) + // no icon on earlier macOS versions + if #available(macOS 11, *) { + Image(systemName: Self.shareIcon) + } + #else + Image(systemName: Self.shareIcon) + #endif + } } } } diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label+Preview.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label+Preview.swift new file mode 100644 index 00000000..24283999 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label+Preview.swift @@ -0,0 +1,37 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(_ title: S, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) + where Data.Element: BackportTransferable, Label == DefaultShareLinkLabel + { + self.label = .init(title) + self.data = items + self.subject = subject + self.message = message + self.preview = preview + } + + init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) + where Data.Element: BackportTransferable, Label == DefaultShareLinkLabel + { + self.label = .init(titleKey) + self.data = items + self.subject = subject + self.message = message + self.preview = preview + } + + init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) + where Data.Element: BackportTransferable, Label == DefaultShareLinkLabel + { + self.label = .init(title) + self.data = items + self.subject = subject + self.message = message + self.preview = preview + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label.swift new file mode 100644 index 00000000..2b7acf97 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Label.swift @@ -0,0 +1,61 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(_ title: S, items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { + self.label = .init(title) + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { + self.label = .init(titleKey) + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { + self.label = .init(title) + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(_ title: S, items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { + self.label = .init(title) + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } + + init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { + self.label = .init(titleKey) + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } + + init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { + self.label = .init(title) + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Preview.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Preview.swift new file mode 100644 index 00000000..8413615a --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items+Preview.swift @@ -0,0 +1,25 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) + where Data.Element: BackportTransferable, Label == DefaultShareLinkLabel { + self.label = .init() + self.data = items + self.subject = subject + self.message = message + self.preview = preview + } + + init(items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview, @ViewBuilder label: () -> Label) + where Data.Element: BackportTransferable { + self.label = label() + self.data = items + self.subject = subject + self.message = message + self.preview = preview + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items.swift new file mode 100644 index 00000000..3259bdd2 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Multiple Items/Items.swift @@ -0,0 +1,43 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { + self.label = .init() + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(items: Data, subject: String? = nil, message: String? = nil) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { + self.label = .init() + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } + + init(items: Data, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == String { + self.label = label() + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(items: Data, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) + where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL { + self.label = label() + self.data = items + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/ShareLink.swift b/Sources/SwiftUIBackports/Shared/ShareLink/ShareLink.swift index f7e910dc..4423f90d 100644 --- a/Sources/SwiftUIBackports/Shared/ShareLink/ShareLink.swift +++ b/Sources/SwiftUIBackports/Shared/ShareLink/ShareLink.swift @@ -1,322 +1,67 @@ import SwiftUI +import LinkPresentation -public struct ShareLink: View where Data: RandomAccessCollection, Label: View { - @State private var showSheet: Bool = false +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport where Wrapped == Any { + struct ShareLink: View where Data: RandomAccessCollection, Data.Element: BackportTransferable, Label: View { + @State private var activity: ActivityItem? - let label: Label - let data: Data - let subject: String? - let message: String? - let preview: (Data.Element) -> SharePreview + let label: Label + let data: Data + let subject: String? + let message: String? + let preview: (Data.Element) -> SharePreview + + public var body: some View { + Button { - public var body: some View { - Button { - showSheet = true - } label: { - label + } label: { + label + } + .shareSheet(item: $activity) } - .activitySheet(isPresented: $showSheet) } } -// Sharing an item -public extension ShareLink { - init(item: String, subject: String? = nil, message: String? = nil) - where Data == CollectionOfOne, PreviewImage == Never, PreviewIcon == Never, Label == DefaultShareLinkLabel { - self.label = .init() - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(item: URL, subject: String? = nil, message: String? = nil) - where Data == CollectionOfOne, PreviewImage == Never, PreviewIcon == Never, Label == DefaultShareLinkLabel { - self.label = .init() - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } - - init(item: String, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne { - self.label = label() - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(item: URL, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne { - self.label = label() - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } -} +final class TransferableActivityProvider: UIActivityItemProvider where Data: BackportTransferable { + let title: String? + let subject: String? + let message: String? + let image: Image? + let icon: Icon? + let data: Data -// Sharing an item with a preview -public extension ShareLink { - init(item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) - where Data == CollectionOfOne, Label == DefaultShareLinkLabel { - self.label = .init() - self.data = .init(item) + init(data: Data, title: String?, subject: String?, message: String?, image: Image?, icon: Icon?) { + self.title = title self.subject = subject self.message = message - self.preview = { _ in preview } - } - - init(item: I, subject: String? = nil, message: String? = nil, preview: SharePreview, @ViewBuilder label: () -> Label) - where Data == CollectionOfOne { - self.label = label() - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { _ in preview } - } -} + self.image = image + self.icon = icon + self.data = data -// Sharing an item with a label -public extension ShareLink { - init(_ title: S, item: String, subject: String? = nil, message: String? = nil) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { - self.label = Text(title) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(_ titleKey: LocalizedStringKey, item: String, subject: String? = nil, message: String? = nil) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { - self.label = Text(titleKey) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(_ title: Text, item: String, subject: String? = nil, message: String? = nil) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { - self.label = title - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(_ title: S, item: URL, subject: String? = nil, message: String? = nil) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { - self.label = Text(title) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } - - init(_ titleKey: LocalizedStringKey, item: URL, subject: String? = nil, message: String? = nil) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { - self.label = Text(titleKey) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } - - init(_ title: Text, item: URL, subject: String? = nil, message: String? = nil) - where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { - self.label = title - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } -} + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("tmp") + .appendingPathExtension(data.pathExtension) -// Sharing an item with a label and preview -public extension ShareLink { - init(_ title: S, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) - where Data == CollectionOfOne, Label == DefaultShareLinkLabel { - self.label = .init(title) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { _ in preview } - } - - init(_ titleKey: LocalizedStringKey, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) - where Data == CollectionOfOne, Label == DefaultShareLinkLabel { - self.label = .init(titleKey) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { _ in preview } + super.init(placeholderItem: url) } - - init(_ title: Text, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) - where Data == CollectionOfOne, Label == DefaultShareLinkLabel { - self.label = .init(title) - self.data = .init(item) - self.subject = subject - self.message = message - self.preview = { _ in preview } - } -} -// Sharing items -public extension ShareLink { - init(items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { - self.label = .init() - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { - self.label = .init() - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } - - init(items: Data, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == String { - self.label = label() - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(items: Data, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL { - self.label = label() - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } -} - -// Sharing items with a preview -public extension ShareLink { - init(items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) - where Data.Element: Transferable, Label == DefaultShareLinkLabel { - self.label = .init() - self.data = items - self.subject = subject - self.message = message - self.preview = preview + override var item: Any { + data.itemProvider as Any } - init(items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview, @ViewBuilder label: () -> Label) - where Data.Element: Transferable { - self.label = label() - self.data = items - self.subject = subject - self.message = message - self.preview = preview + override func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { + let metadata = LPLinkMetadata() + metadata.title = title +// let icon = ImageRenderer(content: activity.icon) +// metadata.iconProvider = NSItemProvider(object: UIImage()) +// metadata.imageProvider = NSItemProvider(object: UIImage()) + return metadata } -} -// Sharing items with a label -public extension ShareLink { - init(_ title: S, items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { - self.label = .init(title) - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { - self.label = .init(titleKey) - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == String, Label == DefaultShareLinkLabel { - self.label = .init(title) - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0) } - } - - init(_ title: S, items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { - self.label = .init(title) - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } - - init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { - self.label = .init(titleKey) - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } - - init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil) - where PreviewImage == Never, PreviewIcon == Never, Data.Element == URL, Label == DefaultShareLinkLabel { - self.label = .init(title) - self.data = items - self.subject = subject - self.message = message - self.preview = { .init($0.absoluteString) } - } -} + override func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { subject ?? "" } -// Sharing items with a label and preview -public extension ShareLink { - init(_ title: S, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) - where Data.Element: Transferable, Label == DefaultShareLinkLabel - { - self.label = .init(title) - self.data = items - self.subject = subject - self.message = message - self.preview = preview - } - - init(_ titleKey: LocalizedStringKey, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) - where Data.Element: Transferable, Label == DefaultShareLinkLabel - { - self.label = .init(titleKey) - self.data = items - self.subject = subject - self.message = message - self.preview = preview - } - - init(_ title: Text, items: Data, subject: String? = nil, message: String? = nil, preview: @escaping (Data.Element) -> SharePreview) - where Data.Element: Transferable, Label == DefaultShareLinkLabel - { - self.label = .init(title) - self.data = items - self.subject = subject - self.message = message - self.preview = preview - } } - -public protocol Transferable { } -extension String: Transferable { } -extension URL: Transferable { } -extension Image: Transferable { } diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/SharePreview.swift b/Sources/SwiftUIBackports/Shared/ShareLink/SharePreview.swift index f2c4a2f2..e1db23f4 100644 --- a/Sources/SwiftUIBackports/Shared/ShareLink/SharePreview.swift +++ b/Sources/SwiftUIBackports/Shared/ShareLink/SharePreview.swift @@ -2,8 +2,8 @@ import SwiftUI public struct SharePreview { let title: String - var icon: () -> UIImage? = { nil } - var image: () -> UIImage? = { nil } + var icon: Icon? + var image: Image? private init() { fatalError() } } @@ -15,12 +15,12 @@ public extension SharePreview { init(_ title: S, icon: Icon) where Icon: View, Image == Never { self.title = title.description - self.icon = { ImageRenderer(content: icon).uiImage } + self.icon = icon } init(_ title: S, image: Image, icon: Icon) where Image: View, Icon: View { self.title = title.description - self.image = { ImageRenderer(content: image).uiImage } - self.icon = { ImageRenderer(content: icon).uiImage } + self.image = image + self.icon = icon } } diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/ShareSheet.swift b/Sources/SwiftUIBackports/Shared/ShareLink/ShareSheet.swift new file mode 100644 index 00000000..8fe69567 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/ShareSheet.swift @@ -0,0 +1,161 @@ +import SwiftUI + +extension View { + @ViewBuilder + func shareSheet(item activityItems: Binding?>) -> some View where Data: RandomAccessCollection, Data.Element: BackportTransferable { +#if os(macOS) + background(ShareSheet(item: activityItems)) +#elseif os(iOS) + background(ShareSheet(item: activityItems)) +#endif + } +} + +#if os(macOS) + +private struct ShareSheet: NSViewRepresentable where Data: RandomAccessCollection, Data.Element: BackportTransferable { + @Binding var item: ActivityItem? + + public func makeNSView(context: Context) -> SourceView { + SourceView(item: $item) + } + + public func updateNSView(_ view: SourceView, context: Context) { + view.item = $item + } + + final class SourceView: NSView, NSSharingServicePickerDelegate, NSSharingServiceDelegate { + var picker: NSSharingServicePicker? + + var item: Binding { + didSet { + updateControllerLifecycle( + from: oldValue.wrappedValue, + to: item.wrappedValue + ) + } + } + + init(item: Binding) { + self.item = item + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateControllerLifecycle(from oldValue: ActivityItem?, to newValue: ActivityItem?) { + switch (oldValue, newValue) { + case (.none, .some): + presentController() + case (.some, .none): + dismissController() + case (.some, .some), (.none, .none): + break + } + } + + func presentController() { + picker = NSSharingServicePicker(items: item.wrappedValue?.items ?? []) + picker?.delegate = self + DispatchQueue.main.async { + guard self.window != nil else { return } + self.picker?.show(relativeTo: self.bounds, of: self, preferredEdge: .minY) + } + } + + func dismissController() { + item.wrappedValue = nil + } + + func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? { + return self + } + + public func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { + sharingServicePicker.delegate = nil + dismissController() + } + + func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { + proposedServices + } + } +} + +#elseif os(iOS) + +private struct ShareSheet: UIViewControllerRepresentable where Data: RandomAccessCollection, Data.Element: BackportTransferable { + @Binding var item: ActivityItem? + + init(item: Binding?>) { + _item = item + } + + func makeUIViewController(context: Context) -> Representable { + Representable(item: $item) + } + + func updateUIViewController(_ controller: Representable, context: Context) { + controller.item = $item + } +} + +private extension ShareSheet { + final class Representable: UIViewController, UIAdaptivePresentationControllerDelegate, UISheetPresentationControllerDelegate { + private weak var controller: UIActivityViewController? + + var item: Binding?> { + didSet { + updateControllerLifecycle( + from: oldValue.wrappedValue, + to: item.wrappedValue + ) + } + } + + init(item: Binding?>) { + self.item = item + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateControllerLifecycle(from oldValue: ActivityItem?, to newValue: ActivityItem?) { + switch (oldValue, newValue) { + case (.none, .some): + presentController() + case (.some, .none): + dismissController() + case (.some, .some), (.none, .none): + break + } + } + + private func presentController() { + let controller = UIActivityViewController(activityItems: item.wrappedValue?.data.map { $0 } ?? [], applicationActivities: nil) + controller.presentationController?.delegate = self + controller.popoverPresentationController?.permittedArrowDirections = .any + controller.popoverPresentationController?.sourceView = view + controller.completionWithItemsHandler = { [weak self] _, _, _, _ in + self?.item.wrappedValue = nil + self?.dismiss(animated: true) + } + present(controller, animated: true) + self.controller = controller + } + + private func dismissController() { + guard let controller = controller else { return } + controller.presentingViewController?.dismiss(animated: true) + } + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + dismissController() + } + } +} +#endif diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Shareable.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Shareable.swift new file mode 100644 index 00000000..8db56c62 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Shareable.swift @@ -0,0 +1,67 @@ +import SwiftUI + +@available(iOS, deprecated: 16.0) +@available(macOS, deprecated: 13.0) +@available(watchOS, deprecated: 9.0) +public protocol BackportTransferable { + var pathExtension: String { get } + var itemProvider: NSItemProvider? { get } +} + +internal struct ActivityItem where Data: RandomAccessCollection, Data.Element: BackportTransferable { + internal var data: Data +} + +extension String: BackportTransferable { + public var pathExtension: String { "txt" } + public var itemProvider: NSItemProvider? { + do { + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("\(UUID().uuidString)") + .appendingPathExtension(pathExtension) + try write(to: url, atomically: true, encoding: .utf8) + return .init(contentsOf: url) + } catch { + return nil + } + } +} + +extension URL: BackportTransferable { + public var itemProvider: NSItemProvider? { + .init(contentsOf: self) + } +} + +extension Image: BackportTransferable { + public var pathExtension: String { "jpg" } + public var itemProvider: NSItemProvider? { + do { + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("\(UUID().uuidString)") + .appendingPathExtension(pathExtension) + let renderer = ImageRenderer(content: self) + let data = renderer.uiImage?.jpegData(compressionQuality: 0.8) + try data?.write(to: url, options: .atomic) + return .init(contentsOf: url) + } catch { + return nil + } + } +} + +extension PlatformImage: BackportTransferable { + public var pathExtension: String { "jpg" } + public var itemProvider: NSItemProvider? { + do { + let url = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("\(UUID().uuidString)") + .appendingPathExtension(pathExtension) + let data = jpegData(compressionQuality: 0.8) + try data?.write(to: url, options: .atomic) + return .init(contentsOf: url) + } catch { + return nil + } + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label+Preview.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label+Preview.swift new file mode 100644 index 00000000..67c84336 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label+Preview.swift @@ -0,0 +1,34 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(_ title: S, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) + where Data == CollectionOfOne, Label == DefaultShareLinkLabel { + self.label = .init(title) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { _ in preview } + } + + init(_ titleKey: LocalizedStringKey, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) + where Data == CollectionOfOne, Label == DefaultShareLinkLabel { + self.label = .init(titleKey) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { _ in preview } + } + + init(_ title: Text, item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) + where Data == CollectionOfOne, Label == DefaultShareLinkLabel { + self.label = .init(title) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { _ in preview } + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label.swift new file mode 100644 index 00000000..54c26742 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Label.swift @@ -0,0 +1,61 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(_ title: S, item: String, subject: String? = nil, message: String? = nil) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { + self.label = Text(title) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(_ titleKey: LocalizedStringKey, item: String, subject: String? = nil, message: String? = nil) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { + self.label = Text(titleKey) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(_ title: Text, item: String, subject: String? = nil, message: String? = nil) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { + self.label = title + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(_ title: S, item: URL, subject: String? = nil, message: String? = nil) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { + self.label = Text(title) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } + + init(_ titleKey: LocalizedStringKey, item: URL, subject: String? = nil, message: String? = nil) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { + self.label = Text(titleKey) + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } + + init(_ title: Text, item: URL, subject: String? = nil, message: String? = nil) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne, Label == Text { + self.label = title + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Preview.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Preview.swift new file mode 100644 index 00000000..61339ff7 --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item+Preview.swift @@ -0,0 +1,25 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(item: I, subject: String? = nil, message: String? = nil, preview: SharePreview) + where Data == CollectionOfOne, Label == DefaultShareLinkLabel { + self.label = .init() + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { _ in preview } + } + + init(item: I, subject: String? = nil, message: String? = nil, preview: SharePreview, @ViewBuilder label: () -> Label) + where Data == CollectionOfOne { + self.label = label() + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { _ in preview } + } +} diff --git a/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item.swift b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item.swift new file mode 100644 index 00000000..330a280a --- /dev/null +++ b/Sources/SwiftUIBackports/Shared/ShareLink/Single Item/Item.swift @@ -0,0 +1,43 @@ +import SwiftUI + +@available(iOS, deprecated: 16) +@available(macOS, deprecated: 13) +@available(watchOS, deprecated: 9) +@available(tvOS, unavailable) +public extension Backport.ShareLink where Wrapped == Any { + init(item: String, subject: String? = nil, message: String? = nil) + where Data == CollectionOfOne, PreviewImage == Never, PreviewIcon == Never, Label == DefaultShareLinkLabel { + self.label = .init() + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(item: URL, subject: String? = nil, message: String? = nil) + where Data == CollectionOfOne, PreviewImage == Never, PreviewIcon == Never, Label == DefaultShareLinkLabel { + self.label = .init() + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } + + init(item: String, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne { + self.label = label() + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0) } + } + + init(item: URL, subject: String? = nil, message: String? = nil, @ViewBuilder label: () -> Label) + where PreviewIcon == Never, PreviewImage == Never, Data == CollectionOfOne { + self.label = label() + self.data = .init(item) + self.subject = subject + self.message = message + self.preview = { .init($0.absoluteString) } + } +}