diff --git a/CHANGELOG.md b/CHANGELOG.md index e76e4f22..5dfd9b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Oeck provider is available again to free users. -- Randomic crashes on profile updates. +- Randomic crashes on profile updates. [#229](https://github.com/passepartoutvpn/passepartout-apple/pull/229) ## 2.0.0 (2022-10-02) diff --git a/Passepartout/App/Context/AppContext+Shared.swift b/Passepartout/App/Context/AppContext+Shared.swift index 645de501..68e359ab 100644 --- a/Passepartout/App/Context/AppContext+Shared.swift +++ b/Passepartout/App/Context/AppContext+Shared.swift @@ -32,11 +32,3 @@ extension AppContext { extension ProductManager { static let shared = AppContext.shared.productManager } - -extension IntentsManager { - static let shared = AppContext.shared.intentsManager -} - -extension Reviewer { - static let shared = AppContext.shared.reviewer -} diff --git a/Passepartout/App/Context/AppContext.swift b/Passepartout/App/Context/AppContext.swift index bd10a951..852ad66d 100644 --- a/Passepartout/App/Context/AppContext.swift +++ b/Passepartout/App/Context/AppContext.swift @@ -27,14 +27,13 @@ import Foundation import Combine import PassepartoutLibrary +@MainActor class AppContext { private let logManager: LogManager - let productManager: ProductManager + private let reviewer: Reviewer - let intentsManager: IntentsManager - - let reviewer: Reviewer + let productManager: ProductManager private var cancellables: Set = [] @@ -45,32 +44,33 @@ class AppContext { logManager.configureLogging() pp_log.info("Logging to: \(logManager.logFile!)") + reviewer = Reviewer() + reviewer.eventCountBeforeRating = Constants.Rating.eventCount + productManager = ProductManager( appType: Constants.InApp.appType, buildProducts: Constants.InApp.buildProducts ) - intentsManager = IntentsManager() - reviewer = Reviewer() - reviewer.eventCountBeforeRating = Constants.Rating.eventCount - + // post configureObjects(coreContext: coreContext) } private func configureObjects(coreContext: CoreContext) { + coreContext.vpnManager.isOnDemandRulesSupported = { + self.isEligibleForOnDemandRules() + } + coreContext.vpnManager.currentState.$vpnStatus .removeDuplicates() + .receive(on: DispatchQueue.main) .sink { if $0 == .connected { pp_log.info("VPN successful connection, report to Reviewer") self.reviewer.reportEvent() } }.store(in: &cancellables) - - coreContext.vpnManager.isOnDemandRulesSupported = { - self.isEligibleForOnDemandRules() - } } // eligibility: ignore network settings if ineligible diff --git a/Passepartout/App/Intents/IntentsManager.swift b/Passepartout/App/Intents/IntentsManager.swift index 22f9a243..3b7950be 100644 --- a/Passepartout/App/Intents/IntentsManager.swift +++ b/Passepartout/App/Intents/IntentsManager.swift @@ -28,6 +28,7 @@ import Intents import IntentsUI import Combine +@MainActor class IntentsManager: NSObject, ObservableObject { @Published private(set) var isReloadingShortcuts = false @@ -35,28 +36,26 @@ class IntentsManager: NSObject, ObservableObject { let shouldDismissIntentView = PassthroughSubject() + private var continuation: CheckedContinuation<[INVoiceShortcut], Never>? + override init() { super.init() - reloadShortcuts() + Task { + await reloadShortcuts() + } } - func reloadShortcuts() { + func reloadShortcuts() async { isReloadingShortcuts = true - INVoiceShortcutCenter.shared.getAllVoiceShortcuts { vs, error in - if let error = error { - assertionFailure("Unable to fetch existing shortcuts: \(error)") - DispatchQueue.main.async { - self.isReloadingShortcuts = false - } - return - } - let shortcuts = (vs ?? []).reduce(into: [UUID: Shortcut]()) { + do { + let vs = try await INVoiceShortcutCenter.shared.allVoiceShortcuts() + shortcuts = vs.reduce(into: [UUID: Shortcut]()) { $0[$1.identifier] = Shortcut($1) } - DispatchQueue.main.async { - self.shortcuts = shortcuts - self.isReloadingShortcuts = false - } + isReloadingShortcuts = false + } catch { + assertionFailure("Unable to fetch existing shortcuts: \(error)") + isReloadingShortcuts = false } } } @@ -93,9 +92,7 @@ extension IntentsManager: INUIEditVoiceShortcutViewControllerDelegate { // so damn it, reload manually after a delay Task { await Task.maybeWait(forMilliseconds: Constants.Delays.xxxReloadEditedShortcut) - await MainActor.run { - reloadShortcuts() - } + await reloadShortcuts() } } diff --git a/Passepartout/App/Mac/MacBundle.swift b/Passepartout/App/Mac/MacBundle.swift index bcd81809..b7cd2c51 100644 --- a/Passepartout/App/Mac/MacBundle.swift +++ b/Passepartout/App/Mac/MacBundle.swift @@ -32,6 +32,7 @@ class MacBundle { private lazy var bridgeDelegate = MacBundleDelegate(bundle: self) + @MainActor func configure() { guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent(Constants.Plugins.macBridgeName) else { fatalError("Unable to find Mac bundle in plugins") diff --git a/Passepartout/App/Mac/MacBundleDelegate.swift b/Passepartout/App/Mac/MacBundleDelegate.swift index 6a8af0ae..92b1d49e 100644 --- a/Passepartout/App/Mac/MacBundleDelegate.swift +++ b/Passepartout/App/Mac/MacBundleDelegate.swift @@ -28,14 +28,17 @@ import Foundation class MacBundleDelegate: MacMenuDelegate { private weak var bundle: MacBundle? + @MainActor var profileManager: LightProfileManager { DefaultLightProfileManager() } + @MainActor var providerManager: LightProviderManager { DefaultLightProviderManager() } + @MainActor var vpnManager: LightVPNManager { DefaultLightVPNManager() } diff --git a/Passepartout/App/Mac/Models/DefaultLightProviderManager.swift b/Passepartout/App/Mac/Models/DefaultLightProviderManager.swift index bef5d9e0..228f841b 100644 --- a/Passepartout/App/Mac/Models/DefaultLightProviderManager.swift +++ b/Passepartout/App/Mac/Models/DefaultLightProviderManager.swift @@ -103,7 +103,6 @@ class DefaultLightProviderManager: LightProviderManager { .map(DefaultLightProviderCategory.init) } - @MainActor func downloadIfNeeded(_ name: String, vpnProtocol: String) { guard let vpnProtocolType = VPNProtocolType(rawValue: vpnProtocol) else { fatalError("Unrecognized VPN protocol: \(vpnProtocol)") diff --git a/Passepartout/App/Mac/Models/DefaultLightVPNManager.swift b/Passepartout/App/Mac/Models/DefaultLightVPNManager.swift index 5d8ef958..79ede1ff 100644 --- a/Passepartout/App/Mac/Models/DefaultLightVPNManager.swift +++ b/Passepartout/App/Mac/Models/DefaultLightVPNManager.swift @@ -64,28 +64,24 @@ class DefaultLightVPNManager: LightVPNManager { }.store(in: &subscriptions) } - @MainActor func connect(with profileId: UUID) { Task { try? await vpnManager.connect(with: profileId) } } - @MainActor func connect(with profileId: UUID, to serverId: String) { Task { try? await vpnManager.connect(with: profileId, toServer: serverId) } } - @MainActor func disconnect() { Task { await vpnManager.disable() } } - @MainActor func toggle() { Task { if !isEnabled { @@ -96,7 +92,6 @@ class DefaultLightVPNManager: LightVPNManager { } } - @MainActor func reconnect() { Task { await vpnManager.reconnect() diff --git a/Passepartout/App/Reusable/GenericCreditsView.swift b/Passepartout/App/Reusable/GenericCreditsView.swift index 52da9001..db0f2b0b 100644 --- a/Passepartout/App/Reusable/GenericCreditsView.swift +++ b/Passepartout/App/Reusable/GenericCreditsView.swift @@ -159,7 +159,7 @@ extension GenericCreditsView { guard content == nil else { return } - Task { + Task { @MainActor in withAnimation { do { content = try String(contentsOf: url) diff --git a/Passepartout/App/Views/AddHostViewModel.swift b/Passepartout/App/Views/AddHostViewModel.swift index 4348a580..86346a97 100644 --- a/Passepartout/App/Views/AddHostViewModel.swift +++ b/Passepartout/App/Views/AddHostViewModel.swift @@ -52,6 +52,7 @@ extension AddHostView { profileName = url.normalizedFilename } + @MainActor mutating func processURL( _ url: URL, with profileManager: ProfileManager, @@ -96,6 +97,7 @@ extension AddHostView { } } + @MainActor mutating func addProcessedProfile(to profileManager: ProfileManager) -> Bool { guard !processedProfile.isPlaceholder else { assertionFailure("Saving profile without processing first?") diff --git a/Passepartout/App/Views/AddProfileMenu.swift b/Passepartout/App/Views/AddProfileMenu.swift index 59a472a3..66e7baec 100644 --- a/Passepartout/App/Views/AddProfileMenu.swift +++ b/Passepartout/App/Views/AddProfileMenu.swift @@ -58,9 +58,7 @@ struct AddProfileMenu: View { } label: { Label(L10n.Global.Strings.provider, systemImage: themeProviderImage) } - Button { - presentHostFileImporter() - } label: { + Button(action: presentHostFileImporter) { Label(L10n.Menu.Contextual.AddProfile.fromFiles, systemImage: themeHostFilesImage) } // Button { @@ -146,7 +144,7 @@ extension AddProfileMenu { // // https://stackoverflow.com/questions/66965471/swiftui-fileimporter-modifier-not-updating-binding-when-dismissed-by-tapping isHostFileImporterPresented = false - Task { + Task { @MainActor in await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter) isHostFileImporterPresented = true } diff --git a/Passepartout/App/Views/AddProviderViewModel.swift b/Passepartout/App/Views/AddProviderViewModel.swift index 7ea83630..1054dacf 100644 --- a/Passepartout/App/Views/AddProviderViewModel.swift +++ b/Passepartout/App/Views/AddProviderViewModel.swift @@ -27,8 +27,6 @@ import Foundation import PassepartoutLibrary extension AddProviderView { - - @MainActor class ViewModel: ObservableObject { enum PendingOperation { case index @@ -75,18 +73,16 @@ extension AddProviderView { metadata.name, vpnProtocol: selectedVPNProtocol ) else { - Task { - await selectProviderAfterFetchingInfrastructure(metadata, providerManager) - } + selectProviderAfterFetchingInfrastructure(metadata, providerManager) return } doSelectProvider(metadata, server) } - private func selectProviderAfterFetchingInfrastructure(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) async { + private func selectProviderAfterFetchingInfrastructure(_ metadata: ProviderMetadata, _ providerManager: ProviderManager) { errorMessage = nil pendingOperation = .provider(metadata.name) - Task { + Task { @MainActor in do { try await providerManager.fetchProviderPublisher( withName: metadata.name, @@ -117,7 +113,7 @@ extension AddProviderView { func updateIndex(_ providerManager: ProviderManager) { errorMessage = nil pendingOperation = .index - Task { + Task { @MainActor in do { try await providerManager.fetchProvidersIndexPublisher( priority: .remoteThenBundle @@ -153,6 +149,7 @@ extension AddProviderView.NameView { profileName = metadata.fullName } + @MainActor mutating func addProfile( _ profile: Profile, to profileManager: ProfileManager, diff --git a/Passepartout/App/Views/OnDemandView+SSID.swift b/Passepartout/App/Views/OnDemandView+SSID.swift index 13734661..d6ec7ec3 100644 --- a/Passepartout/App/Views/OnDemandView+SSID.swift +++ b/Passepartout/App/Views/OnDemandView+SSID.swift @@ -34,11 +34,7 @@ extension OnDemandView { var body: some View { EditableTextList(elements: allSSIDs, allowsDuplicates: false, mapping: mapElements) { text in - reader.requestCurrentSSID { - if !withSSIDs.keys.contains($0) { - text.wrappedValue = $0 - } - } + requestSSID(text) } textField: { ssidRow(callback: $0) } addLabel: { @@ -74,6 +70,15 @@ extension OnDemandView { onCommit: callback.onCommit ).themeValidSSID(callback.text.wrappedValue) } + + private func requestSSID(_ text: Binding) { + Task { @MainActor in + let ssid = try await reader.requestCurrentSSID() + if !withSSIDs.keys.contains(ssid) { + text.wrappedValue = ssid + } + } + } } } diff --git a/Passepartout/App/Views/OrganizerView+Profiles.swift b/Passepartout/App/Views/OrganizerView+Profiles.swift index 610753e5..991af740 100644 --- a/Passepartout/App/Views/OrganizerView+Profiles.swift +++ b/Passepartout/App/Views/OrganizerView+Profiles.swift @@ -41,9 +41,8 @@ extension OrganizerView { if !profileManager.hasProfiles { emptyView } - }.onAppear { - performMigrationsIfNeeded() - }.onReceive(profileManager.didCreateProfile) { + }.onAppear(perform: performMigrationsIfNeeded) + .onReceive(profileManager.didCreateProfile) { profileManager.currentProfileId = $0.id } } @@ -141,7 +140,7 @@ extension OrganizerView { } private func performMigrationsIfNeeded() { - Task { + Task { @MainActor in UpgradeManager.shared.doMigrations(profileManager) } } diff --git a/Passepartout/App/Views/OrganizerView+Scene.swift b/Passepartout/App/Views/OrganizerView+Scene.swift index aec43e19..4215df2b 100644 --- a/Passepartout/App/Views/OrganizerView+Scene.swift +++ b/Passepartout/App/Views/OrganizerView+Scene.swift @@ -87,7 +87,7 @@ extension OrganizerView { switch phase { case .active: if productManager.hasRefunded() { - Task { + Task { @MainActor in await vpnManager.uninstall() } } diff --git a/Passepartout/App/Views/OrganizerView.swift b/Passepartout/App/Views/OrganizerView.swift index c2e3ea48..22a1bf57 100644 --- a/Passepartout/App/Views/OrganizerView.swift +++ b/Passepartout/App/Views/OrganizerView.swift @@ -102,7 +102,7 @@ extension OrganizerView { assertionFailure("Empty URLs from file importer?") return } - Task { + Task { @MainActor in await Task.maybeWait(forMilliseconds: Constants.Delays.xxxPresentFileImporter) addProfileModalType = .addHost(url, false) } diff --git a/Passepartout/App/Views/ProfileView+MainMenu.swift b/Passepartout/App/Views/ProfileView+MainMenu.swift index b3266a38..1cc736ef 100644 --- a/Passepartout/App/Views/ProfileView+MainMenu.swift +++ b/Passepartout/App/Views/ProfileView+MainMenu.swift @@ -138,7 +138,7 @@ extension ProfileView { } private func uninstallVPN() { - Task { + Task { @MainActor in await vpnManager.uninstall() } } diff --git a/Passepartout/App/Views/ProfileView+Provider.swift b/Passepartout/App/Views/ProfileView+Provider.swift index 1a527fff..43ca1c55 100644 --- a/Passepartout/App/Views/ProfileView+Provider.swift +++ b/Passepartout/App/Views/ProfileView+Provider.swift @@ -127,7 +127,7 @@ extension ProfileView { private func refreshInfrastructure() { isRefreshingInfrastructure = true - Task { + Task { @MainActor in try await providerManager.fetchRemoteProviderPublisher(forProfile: profile).async() isRefreshingInfrastructure = false } diff --git a/Passepartout/App/Views/ShortcutsView.swift b/Passepartout/App/Views/ShortcutsView.swift index 7d7dae13..d606f6f2 100644 --- a/Passepartout/App/Views/ShortcutsView.swift +++ b/Passepartout/App/Views/ShortcutsView.swift @@ -43,7 +43,7 @@ struct ShortcutsView: View { } } - @ObservedObject private var intentsManager: IntentsManager + @StateObject private var intentsManager = IntentsManager() @Environment(\.presentationMode) private var presentationMode @@ -56,7 +56,6 @@ struct ShortcutsView: View { @State private var pendingShortcut: INShortcut? init(target: Profile) { - intentsManager = .shared self.target = target } @@ -69,11 +68,7 @@ struct ShortcutsView: View { }.toolbar { themeCloseItem(presentationMode: presentationMode) }.sheet(item: $modalType, content: presentedModal) - - // reloading - .onAppear { - intentsManager.reloadShortcuts() - }.themeAnimation(on: intentsManager.isReloadingShortcuts) + .themeAnimation(on: intentsManager.isReloadingShortcuts) // IntentsUI .onReceive(intentsManager.shouldDismissIntentView) { _ in diff --git a/Passepartout/App/Views/VPNToggle.swift b/Passepartout/App/Views/VPNToggle.swift index 286ed5e5..7160050b 100644 --- a/Passepartout/App/Views/VPNToggle.swift +++ b/Passepartout/App/Views/VPNToggle.swift @@ -77,11 +77,10 @@ struct VPNToggle: View { } private func enableVPN() { - Task { + Task { @MainActor in canToggle = false - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(rateLimit)) { - canToggle = true - } + await Task.maybeWait(forMilliseconds: rateLimit) + canToggle = true do { let profile = try await vpnManager.connect(with: profileId) donateIntents(withProfile: profile) @@ -93,7 +92,7 @@ struct VPNToggle: View { } private func disableVPN() { - Task { + Task { @MainActor in canToggle = false await vpnManager.disable() canToggle = true diff --git a/Passepartout/AppShared/Context/CoreContext+Shared.swift b/Passepartout/AppShared/Context/CoreContext+Shared.swift index 46e244e9..b4fe829d 100644 --- a/Passepartout/AppShared/Context/CoreContext+Shared.swift +++ b/Passepartout/AppShared/Context/CoreContext+Shared.swift @@ -27,8 +27,6 @@ import Foundation import PassepartoutLibrary extension CoreContext { - - @MainActor static let shared = CoreContext(store: UserDefaultsStore(defaults: .standard)) } @@ -49,5 +47,7 @@ extension VPNManager { } extension ObservableVPNState { + + @MainActor static let shared = CoreContext.shared.vpnManager.currentState } diff --git a/Passepartout/AppShared/Context/CoreContext.swift b/Passepartout/AppShared/Context/CoreContext.swift index 684374a4..dbafa064 100644 --- a/Passepartout/AppShared/Context/CoreContext.swift +++ b/Passepartout/AppShared/Context/CoreContext.swift @@ -27,6 +27,7 @@ import Foundation import Combine import PassepartoutLibrary +@MainActor class CoreContext { let store: KeyValueStore @@ -52,7 +53,6 @@ class CoreContext { private var cancellables: Set = [] - @MainActor init(store: KeyValueStore) { self.store = store diff --git a/Passepartout/AppShared/Mac/MacMenu.swift b/Passepartout/AppShared/Mac/MacMenu.swift index c7eea905..5a347274 100644 --- a/Passepartout/AppShared/Mac/MacMenu.swift +++ b/Passepartout/AppShared/Mac/MacMenu.swift @@ -25,6 +25,7 @@ import Foundation +@MainActor @objc public protocol MacMenu { var delegate: MacMenuDelegate? { get set } diff --git a/Passepartout/AppShared/Mac/Models/LightProfileManager.swift b/Passepartout/AppShared/Mac/Models/LightProfileManager.swift index df0c26e7..89db4acd 100644 --- a/Passepartout/AppShared/Mac/Models/LightProfileManager.swift +++ b/Passepartout/AppShared/Mac/Models/LightProfileManager.swift @@ -46,6 +46,7 @@ extension LightProfile { } } +@MainActor @objc public protocol LightProfileManager { var hasProfiles: Bool { get } diff --git a/Passepartout/AppShared/Mac/Models/LightProviderManager.swift b/Passepartout/AppShared/Mac/Models/LightProviderManager.swift index baaae0a1..d76b69d8 100644 --- a/Passepartout/AppShared/Mac/Models/LightProviderManager.swift +++ b/Passepartout/AppShared/Mac/Models/LightProviderManager.swift @@ -56,6 +56,7 @@ public protocol LightProviderServer { var serverId: String { get } } +@MainActor @objc public protocol LightProviderManager { var delegate: LightProviderManagerDelegate? { get set } diff --git a/Passepartout/AppShared/Mac/Models/LightVPNManager.swift b/Passepartout/AppShared/Mac/Models/LightVPNManager.swift index c807fc1c..4a827dd2 100644 --- a/Passepartout/AppShared/Mac/Models/LightVPNManager.swift +++ b/Passepartout/AppShared/Mac/Models/LightVPNManager.swift @@ -36,6 +36,7 @@ public enum LightVPNStatus: Int { case disconnected } +@MainActor @objc public protocol LightVPNManager { var isEnabled: Bool { get } diff --git a/Passepartout/Mac/Menu/HostProfileItem+ViewModel.swift b/Passepartout/Mac/Menu/HostProfileItem+ViewModel.swift index acad42cb..30b87f80 100644 --- a/Passepartout/Mac/Menu/HostProfileItem+ViewModel.swift +++ b/Passepartout/Mac/Menu/HostProfileItem+ViewModel.swift @@ -26,6 +26,8 @@ import Foundation extension HostProfileItem { + + @MainActor class ViewModel { let profile: LightProfile @@ -41,7 +43,9 @@ extension HostProfileItem { } deinit { - vpnManager.removeDelegate(withIdentifier: profile.id.uuidString) + Task { @MainActor in + vpnManager.removeDelegate(withIdentifier: profile.id.uuidString) + } } @objc func connectTo() { diff --git a/Passepartout/Mac/Menu/LaunchOnLoginItem+ViewModel.swift b/Passepartout/Mac/Menu/LaunchOnLoginItem+ViewModel.swift index 42da45eb..ce31d537 100644 --- a/Passepartout/Mac/Menu/LaunchOnLoginItem+ViewModel.swift +++ b/Passepartout/Mac/Menu/LaunchOnLoginItem+ViewModel.swift @@ -28,6 +28,8 @@ import Combine import ServiceManagement extension LaunchOnLoginItem { + + @MainActor class ViewModel: ObservableObject { let title: String diff --git a/Passepartout/Mac/Menu/PassepartoutMenu+StatusButton.swift b/Passepartout/Mac/Menu/PassepartoutMenu+StatusButton.swift index 950dfd7b..97d4468b 100644 --- a/Passepartout/Mac/Menu/PassepartoutMenu+StatusButton.swift +++ b/Passepartout/Mac/Menu/PassepartoutMenu+StatusButton.swift @@ -27,6 +27,8 @@ import Foundation import AppKit extension PassepartoutMenu { + + @MainActor class StatusButton { private lazy var statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) @@ -50,7 +52,9 @@ extension PassepartoutMenu { } deinit { - vpnManager.removeDelegate(withIdentifier: "PassepartoutMenu") + Task { @MainActor in + vpnManager.removeDelegate(withIdentifier: "PassepartoutMenu") + } } func install(systemMenu: SystemMenu) { diff --git a/Passepartout/Mac/Menu/PassepartoutMenu.swift b/Passepartout/Mac/Menu/PassepartoutMenu.swift index 195c5268..c177ba63 100644 --- a/Passepartout/Mac/Menu/PassepartoutMenu.swift +++ b/Passepartout/Mac/Menu/PassepartoutMenu.swift @@ -26,6 +26,7 @@ import Foundation import AppKit +@MainActor class PassepartoutMenu { private let macMenuDelegate: MacMenuDelegate diff --git a/Passepartout/Mac/Menu/ProviderLocationItem+ViewModel.swift b/Passepartout/Mac/Menu/ProviderLocationItem+ViewModel.swift index 6c833f97..4554a692 100644 --- a/Passepartout/Mac/Menu/ProviderLocationItem+ViewModel.swift +++ b/Passepartout/Mac/Menu/ProviderLocationItem+ViewModel.swift @@ -26,6 +26,8 @@ import Foundation extension ProviderLocationItem { + + @MainActor class ViewModel { private let profile: LightProfile diff --git a/Passepartout/Mac/Menu/ProviderProfileItem+ViewModel.swift b/Passepartout/Mac/Menu/ProviderProfileItem+ViewModel.swift index 277eccd9..01ae0d3a 100644 --- a/Passepartout/Mac/Menu/ProviderProfileItem+ViewModel.swift +++ b/Passepartout/Mac/Menu/ProviderProfileItem+ViewModel.swift @@ -26,6 +26,8 @@ import Foundation extension ProviderProfileItem { + + @MainActor class ViewModel { let profile: LightProfile @@ -44,7 +46,9 @@ extension ProviderProfileItem { } deinit { - vpnManager.removeDelegate(withIdentifier: profile.id.uuidString) + Task { @MainActor in + vpnManager.removeDelegate(withIdentifier: profile.id.uuidString) + } } private var providerName: String { diff --git a/Passepartout/Mac/Menu/ProviderServerItem+ViewModel.swift b/Passepartout/Mac/Menu/ProviderServerItem+ViewModel.swift index f0c64a91..42aaa84a 100644 --- a/Passepartout/Mac/Menu/ProviderServerItem+ViewModel.swift +++ b/Passepartout/Mac/Menu/ProviderServerItem+ViewModel.swift @@ -26,6 +26,8 @@ import Foundation extension ProviderServerItem { + + @MainActor class ViewModel { private let profile: LightProfile diff --git a/Passepartout/Mac/Menu/VPNItemGroup+ViewModel.swift b/Passepartout/Mac/Menu/VPNItemGroup+ViewModel.swift index a3e68124..1f118ac9 100644 --- a/Passepartout/Mac/Menu/VPNItemGroup+ViewModel.swift +++ b/Passepartout/Mac/Menu/VPNItemGroup+ViewModel.swift @@ -27,6 +27,8 @@ import Foundation import Combine extension VPNItemGroup { + + @MainActor class ViewModel { private let vpnManager: LightVPNManager @@ -51,7 +53,9 @@ extension VPNItemGroup { } deinit { - vpnManager.removeDelegate(withIdentifier: "VPNItemGroup") + Task { @MainActor in + vpnManager.removeDelegate(withIdentifier: "VPNItemGroup") + } } var toggleTitle: String { diff --git a/Passepartout/Mac/Menu/VisibilityItem+ViewModel.swift b/Passepartout/Mac/Menu/VisibilityItem+ViewModel.swift index 2bfcbeec..1199a904 100644 --- a/Passepartout/Mac/Menu/VisibilityItem+ViewModel.swift +++ b/Passepartout/Mac/Menu/VisibilityItem+ViewModel.swift @@ -27,6 +27,8 @@ import Foundation import AppKit extension VisibilityItem { + + @MainActor class ViewModel { private let transformer: ObservableProcessTransformer diff --git a/Passepartout/Mac/PassepartoutMac.swift b/Passepartout/Mac/PassepartoutMac.swift index 06bd9d93..e333ea4b 100644 --- a/Passepartout/Mac/PassepartoutMac.swift +++ b/Passepartout/Mac/PassepartoutMac.swift @@ -32,5 +32,6 @@ class PassepartoutMac: NSObject, MacBridge { let utils: MacUtils = DefaultMacUtils() + @MainActor let menu: MacMenu = DefaultMacMenu() } diff --git a/Passepartout/Mac/Reusable/ItemGroup.swift b/Passepartout/Mac/Reusable/ItemGroup.swift index 61b7e224..7048c8b2 100644 --- a/Passepartout/Mac/Reusable/ItemGroup.swift +++ b/Passepartout/Mac/Reusable/ItemGroup.swift @@ -26,6 +26,7 @@ import Foundation import AppKit +@MainActor protocol ItemGroup { func asMenuItems(withParent parent: NSMenu) -> [NSMenuItem] } diff --git a/Passepartout/Mac/Reusable/SystemMenu.swift b/Passepartout/Mac/Reusable/SystemMenu.swift index 51c1b755..6ea79179 100644 --- a/Passepartout/Mac/Reusable/SystemMenu.swift +++ b/Passepartout/Mac/Reusable/SystemMenu.swift @@ -26,6 +26,7 @@ import Foundation import AppKit +@MainActor protocol SystemMenu { var asMenu: NSMenu { get } } diff --git a/PassepartoutLibrary/Sources/PassepartoutLibrary/Managers/UpgradeManager.swift b/PassepartoutLibrary/Sources/PassepartoutLibrary/Managers/UpgradeManager.swift index 6cc33366..4fabaa48 100644 --- a/PassepartoutLibrary/Sources/PassepartoutLibrary/Managers/UpgradeManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutLibrary/Managers/UpgradeManager.swift @@ -29,6 +29,7 @@ import SwiftyBeaver import PassepartoutCore import PassepartoutUtils +@MainActor public final class UpgradeManager: ObservableObject { // MARK: Initialization diff --git a/PassepartoutLibrary/Sources/PassepartoutProfiles/Managers/ProfileManager.swift b/PassepartoutLibrary/Sources/PassepartoutProfiles/Managers/ProfileManager.swift index 650c7ec1..fd61e9a8 100644 --- a/PassepartoutLibrary/Sources/PassepartoutProfiles/Managers/ProfileManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutProfiles/Managers/ProfileManager.swift @@ -30,6 +30,7 @@ import PassepartoutCore import PassepartoutUtils import PassepartoutProviders +@MainActor public final class ProfileManager: ObservableObject { public typealias ProfileEx = (profile: Profile, isReady: Bool) @@ -292,10 +293,8 @@ extension ProfileManager { currentProfile.isLoading = true Task { try await makeProfileReady(profile) - await MainActor.run { - currentProfile.value = profile - currentProfile.isLoading = false - } + currentProfile.value = profile + currentProfile.isLoading = false } } } diff --git a/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/SSIDReader.swift b/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/SSIDReader.swift index 7a411b9d..a8290a66 100644 --- a/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/SSIDReader.swift +++ b/PassepartoutLibrary/Sources/PassepartoutUtils/Reusable/SSIDReader.swift @@ -25,48 +25,50 @@ import Foundation import CoreLocation -import Combine -public class SSIDReader: NSObject, ObservableObject, CLLocationManagerDelegate { +@MainActor +public class SSIDReader: NSObject, ObservableObject { private let manager = CLLocationManager() - - private let publisher = PassthroughSubject() - private var cancellables: Set = [] - - public func requestCurrentSSID(onSSID: @escaping (String) -> Void) { - publisher - .sink(receiveValue: onSSID) - .store(in: &cancellables) - + private var continuation: CheckedContinuation? + + private func currentSSID() async -> String { + await Utils.currentWifiSSID() ?? "" + } + + public func requestCurrentSSID() async throws -> String { switch manager.authorizationStatus { case .authorizedAlways, .authorizedWhenInUse, .denied: - notifyCurrentSSID() - return - + return await currentSSID() + default: - manager.delegate = self - manager.requestWhenInUseAuthorization() - } - } - - private func notifyCurrentSSID() { - Task { - let currentSSID = await Utils.currentWifiSSID() ?? "" - await MainActor.run { - publisher.send(currentSSID) - cancellables.removeAll() + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + + manager.delegate = self + manager.requestWhenInUseAuthorization() } } } +} +extension SSIDReader: CLLocationManagerDelegate { public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways, .denied: - notifyCurrentSSID() + Task { + continuation?.resume(returning: await currentSSID()) + continuation = nil + } default: - cancellables.removeAll() + continuation?.resume(with: .success("")) + continuation = nil } } + + public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + continuation?.resume(throwing: error) + continuation = nil + } } diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/TunnelKitVPNManagerStrategy.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/TunnelKitVPNManagerStrategy.swift index dc4152d9..ff9847e1 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/TunnelKitVPNManagerStrategy.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/TunnelKitVPNManagerStrategy.swift @@ -70,7 +70,6 @@ public class TunnelKitVPNManagerStrategy: VPNManagerStrategy where private var currentBundleIdentifier: String? - @MainActor public init( appGroup: String, tunnelBundleIdentifier: @escaping (VPNProtocolType) -> String, diff --git a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift index f8461ed6..4436d620 100644 --- a/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift +++ b/PassepartoutLibrary/Sources/PassepartoutVPN/Managers/VPNManager.swift @@ -32,6 +32,7 @@ import PassepartoutProfiles import PassepartoutProviders import PassepartoutUtils +@MainActor public final class VPNManager: ObservableObject { // MARK: Initialization