RadrootsAppleExternalActions.swift (3936B)
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 83 -> RadrootsExternalActionCapability 84 { 85 let url = await resolvedURL(for: destination) 86 guard let url else { 87 return RadrootsExternalActionCapability(destination: destination, canOpen: false) 88 } 89 let canOpen = await adapters.canOpenURL(url) 90 return RadrootsExternalActionCapability( 91 destination: destination, 92 canOpen: canOpen 93 ) 94 } 95 96 public func open(_ request: RadrootsExternalActionRequest) async throws { 97 guard let url = await resolvedURL(for: request.destination) else { 98 throw RadrootsExternalActionError.unavailable 99 } 100 let success = await adapters.openURL(url) 101 guard success else { 102 throw RadrootsExternalActionError.transientFailure 103 } 104 } 105 106 private func resolvedURL(for destination: RadrootsExternalActionDestination) async -> URL? { 107 switch destination.kind { 108 case .appSettings: 109 await adapters.appSettingsURL() 110 case .web, .nostr, .appleMaps: 111 destination.url 112 } 113 } 114 } 115 116 #if canImport(UIKit) 117 extension RadrootsAppleExternalActionsAdapters { 118 @MainActor 119 fileprivate static func openUIKitURL(_ url: URL) async -> Bool { 120 await withCheckedContinuation { continuation in 121 UIApplication.shared.open(url, options: [:]) { success in 122 continuation.resume(returning: success) 123 } 124 } 125 } 126 } 127 #endif