Update Tunnel on the main thread

Also updated it to use the new Swift Observable macro
This commit is contained in:
Conrad Kramer 2024-01-20 09:39:30 -08:00
parent b008762a5b
commit 2b9ecb7b6a
10 changed files with 167 additions and 69 deletions

View file

@ -15,7 +15,7 @@ struct BurrowApp: App {
var body: some Scene {
WindowGroup {
TunnelView()
TunnelView(tunnel: Self.tunnel)
}
}
}

View file

@ -8,7 +8,7 @@
import SwiftUI
struct MenuItemToggleView: View {
@ObservedObject var tunnel: Tunnel
var tunnel: Tunnel
var body: some View {
HStack {
@ -23,7 +23,6 @@ struct MenuItemToggleView: View {
.padding(.horizontal, 4)
.padding(10)
.frame(minWidth: 300, minHeight: 32, maxHeight: 32)
.task { await tunnel.update() }
}
}

View file

@ -2,13 +2,13 @@ import NetworkExtension
extension NEVPNManager {
func remove() async throws {
let _: Void = try await withUnsafeThrowingContinuation { continuation in
_ = try await withUnsafeThrowingContinuation { continuation in
removeFromPreferences(completionHandler: completion(continuation))
}
}
func save() async throws {
let _: Void = try await withUnsafeThrowingContinuation { continuation in
_ = try await withUnsafeThrowingContinuation { continuation in
saveToPreferences(completionHandler: completion(continuation))
}
}
@ -18,13 +18,7 @@ extension NETunnelProviderManager {
class var managers: [NETunnelProviderManager] {
get async throws {
try await withUnsafeThrowingContinuation { continuation in
loadAllFromPreferences { managers, error in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: managers ?? [])
}
}
loadAllFromPreferences(completionHandler: completion(continuation))
}
}
}
@ -32,10 +26,20 @@ extension NETunnelProviderManager {
private func completion(_ continuation: UnsafeContinuation<Void, Error>) -> (Error?) -> Void {
return { error in
if let error = error {
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
}
}
private func completion<T>(_ continuation: UnsafeContinuation<T, Error>) -> (T?, Error?) -> Void {
return { value, error in
if let error {
continuation.resume(throwing: error)
} else if let value {
continuation.resume(returning: value)
}
}
}

View file

@ -2,15 +2,16 @@ import Combine
import NetworkExtension
import SwiftUI
@MainActor
class Tunnel: ObservableObject {
@Published private(set) var status: Status = .unknown
@Published private var error: NEVPNError?
@Observable class Tunnel {
private(set) var status: Status = .unknown
private var error: NEVPNError?
private let bundleIdentifier: String
private let configure: (NETunnelProviderManager, NETunnelProviderProtocol) -> Void
private var tasks: [Task<Void, Error>] = []
// Each manager corresponds to one entry in the Settings app.
// Our goal is to maintain a single manager, so we create one if none exist and delete extra if there are any.
private var managers: [NEVPNManager]? {
didSet { status = currentStatus }
}
@ -48,24 +49,31 @@ class Tunnel: ObservableObject {
self.bundleIdentifier = bundleIdentifier
self.configure = configure
listenForUpdates()
Task { await update() }
}
private func listenForUpdates() {
let center = NotificationCenter.default
let statusTask = Task {
for try await _ in NotificationCenter.default.notifications(named: .NEVPNStatusDidChange) {
for try await _ in center.notifications(named: .NEVPNStatusDidChange).map({ _ in () }) {
status = currentStatus
}
}
let configurationTask = Task {
for try await _ in NotificationCenter.default.notifications(named: .NEVPNConfigurationChange) {
for try await _ in center.notifications(named: .NEVPNConfigurationChange).map({ _ in () }) {
await update()
}
}
tasks = [statusTask, configurationTask]
}
func update() async {
private func update() async {
do {
managers = try await NETunnelProviderManager.managers
} catch let error as NEVPNError {
self.error = error
let updated = try await NETunnelProviderManager.managers
await MainActor.run { managers = updated }
} catch let vpnError as NEVPNError {
error = vpnError
} catch {
print(error)
}
@ -109,7 +117,9 @@ class Tunnel: ObservableObject {
}
deinit {
tasks.forEach { $0.cancel() }
for task in tasks {
task.cancel()
}
}
}

View file

@ -1,36 +1,34 @@
import SwiftUI
struct TunnelView: View {
// @ObservedObject var tunnel: Tunnel
var tunnel: Tunnel
var body: some View {
EmptyView()
// VStack {
// Text(verbatim: tunnel.status.description)
// switch tunnel.status {
// case .connected:
// Button("Disconnect", action: stop)
// case .permissionRequired:
// Button("Allow", action: configure)
// case .disconnected:
// Button("Start", action: start)
// default:
// EmptyView()
// }
// }
// .task { await tunnel.update() }
// .padding()
VStack {
Text(verbatim: tunnel.status.description)
switch tunnel.status {
case .connected:
Button("Disconnect", action: stop)
case .permissionRequired:
Button("Allow", action: configure)
case .disconnected:
Button("Start", action: start)
default:
EmptyView()
}
}
.padding()
}
// private func start() {
// try? tunnel.start()
// }
//
// private func stop() {
// tunnel.stop()
// }
//
// private func configure() {
// Task { try await tunnel.configure() }
// }
private func start() {
try? tunnel.start()
}
private func stop() {
tunnel.stop()
}
private func configure() {
Task { try await tunnel.configure() }
}
}