RadrootsAppleExternalActions.swift (3910B)
1 import Foundation 2 3 #if canImport(AppKit) 4 @preconcurrency import AppKit 5 #endif 6 7 #if canImport(UIKit) 8 @preconcurrency import UIKit 9 #endif 10 11 public struct RadrootsAppleExternalActionsAdapters: Sendable { 12 public let appSettingsURL: @Sendable () async -> URL? 13 public let canOpenURL: @Sendable (URL) async -> Bool 14 public let openURL: @Sendable (URL) async -> Bool 15 16 public init( 17 appSettingsURL: @escaping @Sendable () async -> URL?, 18 canOpenURL: @escaping @Sendable (URL) async -> Bool, 19 openURL: @escaping @Sendable (URL) async -> Bool 20 ) { 21 self.appSettingsURL = appSettingsURL 22 self.canOpenURL = canOpenURL 23 self.openURL = openURL 24 } 25 26 public static var live: Self { 27 #if canImport(UIKit) 28 Self( 29 appSettingsURL: { 30 await MainActor.run { 31 URL(string: UIApplication.openSettingsURLString) 32 } 33 }, 34 canOpenURL: { url in 35 await MainActor.run { 36 UIApplication.shared.canOpenURL(url) 37 } 38 }, 39 openURL: { url in 40 await Self.openUIKitURL(url) 41 } 42 ) 43 #elseif canImport(AppKit) 44 Self( 45 appSettingsURL: { 46 nil 47 }, 48 canOpenURL: { url in 49 await MainActor.run { 50 NSWorkspace.shared.urlForApplication(toOpen: url) != nil 51 } 52 }, 53 openURL: { url in 54 await MainActor.run { 55 NSWorkspace.shared.open(url) 56 } 57 } 58 ) 59 #else 60 Self( 61 appSettingsURL: { 62 nil 63 }, 64 canOpenURL: { _ in 65 false 66 }, 67 openURL: { _ in 68 false 69 } 70 ) 71 #endif 72 } 73 } 74 75 public final class RadrootsAppleExternalActions: RadrootsExternalActions, Sendable { 76 private let adapters: RadrootsAppleExternalActionsAdapters 77 78 public init(adapters: RadrootsAppleExternalActionsAdapters = .live) { 79 self.adapters = adapters 80 } 81 82 public func canOpen(_ destination: RadrootsExternalActionDestination) async -> RadrootsExternalActionCapability { 83 let url = await resolvedURL(for: destination) 84 guard let url else { 85 return RadrootsExternalActionCapability(destination: destination, canOpen: false) 86 } 87 let canOpen = await adapters.canOpenURL(url) 88 return RadrootsExternalActionCapability( 89 destination: destination, 90 canOpen: canOpen 91 ) 92 } 93 94 public func open(_ request: RadrootsExternalActionRequest) async throws { 95 guard let url = await resolvedURL(for: request.destination) else { 96 throw RadrootsExternalActionError.unavailable( 97 "\(request.destination.kind.rawValue) external action is unavailable" 98 ) 99 } 100 let success = await adapters.openURL(url) 101 guard success else { 102 throw RadrootsExternalActionError.transientFailure( 103 "failed to open \(request.destination.kind.rawValue) external action" 104 ) 105 } 106 } 107 108 private func resolvedURL(for destination: RadrootsExternalActionDestination) async -> URL? { 109 switch destination.kind { 110 case .appSettings: 111 await adapters.appSettingsURL() 112 case .web, .nostr, .appleMaps: 113 destination.url 114 } 115 } 116 } 117 118 #if canImport(UIKit) 119 private extension RadrootsAppleExternalActionsAdapters { 120 @MainActor 121 static func openUIKitURL(_ url: URL) async -> Bool { 122 await withCheckedContinuation { continuation in 123 UIApplication.shared.open(url, options: [:]) { success in 124 continuation.resume(returning: success) 125 } 126 } 127 } 128 } 129 #endif