diff --git a/LiveKitExample.xcodeproj/project.pbxproj b/LiveKitExample.xcodeproj/project.pbxproj index f688ea7..5415a29 100644 --- a/LiveKitExample.xcodeproj/project.pbxproj +++ b/LiveKitExample.xcodeproj/project.pbxproj @@ -678,7 +678,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = J48VV6BZV9; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -691,7 +691,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.Multiplatform-SwiftUI"; PRODUCT_NAME = LiveKitExample; SDKROOT = macosx; @@ -708,7 +708,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = J48VV6BZV9; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; @@ -721,7 +721,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.0.2; PRODUCT_BUNDLE_IDENTIFIER = "io.livekit.example.Multiplatform-SwiftUI"; PRODUCT_NAME = LiveKitExample; SDKROOT = macosx; diff --git a/Shared/Controllers/AppContext.swift b/Shared/Controllers/AppContext.swift index b962a61..f73c64c 100644 --- a/Shared/Controllers/AppContext.swift +++ b/Shared/Controllers/AppContext.swift @@ -1,43 +1,53 @@ import SwiftUI import LiveKit import WebRTC +import Combine + +extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { + func notify() { + DispatchQueue.main.async { self.objectWillChange.send() } + } +} // This class contains the logic to control behavior of the whole app. final class AppContext: ObservableObject { - private let store: SecureStore + private let store: ValueStore - @Published var videoViewVisible: Bool { - didSet { store.set(.videoViewVisible, value: videoViewVisible) } + @Published var videoViewVisible: Bool = true { + didSet { store.value.videoViewVisible = videoViewVisible } } - @Published var showInformationOverlay: Bool { - didSet { store.set(.showInformationOverlay, value: showInformationOverlay) } + @Published var showInformationOverlay: Bool = false { + didSet { store.value.showInformationOverlay = showInformationOverlay } } - @Published var preferMetal: Bool { - didSet { store.set(.preferMetal, value: preferMetal) } + @Published var preferMetal: Bool = true { + didSet { store.value.preferMetal = preferMetal } } - @Published var videoViewMode: VideoView.Mode { - didSet { store.set(.videoViewMode, value: videoViewMode) } + @Published var videoViewMode: VideoView.Mode = .fit { + didSet { store.value.videoViewMode = videoViewMode } } - @Published var videoViewMirrored: Bool { - didSet { store.set(.videoViewMirrored, value: videoViewMirrored) } + @Published var videoViewMirrored: Bool = false { + didSet { store.value.videoViewMirrored = videoViewMirrored } } - @Published var connectionHistory: Set { - didSet { store.set(.connectionHistory, value: connectionHistory) } + @Published var connectionHistory: Set = [] { + didSet { store.value.connectionHistory = connectionHistory } } - public init(store: SecureStore) { + public init(store: ValueStore) { self.store = store - self.videoViewVisible = store.get(.videoViewVisible) ?? true - self.showInformationOverlay = store.get(.showInformationOverlay) ?? false - self.preferMetal = store.get(.preferMetal) ?? true - self.videoViewMode = store.get(.videoViewMode) ?? .fit - self.videoViewMirrored = store.get(.videoViewMirrored) ?? false - self.connectionHistory = store.get(.connectionHistory) ?? Set() + + store.onLoaded.then { preferences in + self.videoViewVisible = preferences.videoViewVisible + self.showInformationOverlay = preferences.showInformationOverlay + self.preferMetal = preferences.preferMetal + self.videoViewMode = preferences.videoViewMode + self.videoViewMirrored = preferences.videoViewMirrored + self.connectionHistory = preferences.connectionHistory + } } } diff --git a/Shared/Controllers/RoomContext.swift b/Shared/Controllers/RoomContext.swift index 027b92a..ad7e9f0 100644 --- a/Shared/Controllers/RoomContext.swift +++ b/Shared/Controllers/RoomContext.swift @@ -6,7 +6,7 @@ import Promises // This class contains the logic to control behavior of the whole app. final class RoomContext: ObservableObject { - private let store: SecureStore + private let store: ValueStore // Used to show connection error dialog // private var didClose: Bool = false @@ -18,46 +18,49 @@ final class RoomContext: ObservableObject { room.room.connectionState } - @Published var url: String { - didSet { store.set(.url, value: url) } + @Published var url: String = "" { + didSet { store.value.url = url } } - @Published var token: String { - didSet { store.set(.token, value: token) } + @Published var token: String = "" { + didSet { store.value.token = token } } // RoomOptions - @Published var simulcast: Bool { - didSet { store.set(.simulcast, value: simulcast) } + @Published var simulcast: Bool = true { + didSet { store.value.simulcast = simulcast } } - @Published var adaptiveStream: Bool { - didSet { store.set(.adaptiveStream, value: adaptiveStream) } + @Published var adaptiveStream: Bool = false { + didSet { store.value.adaptiveStream = adaptiveStream } } - @Published var dynacast: Bool { - didSet { store.set(.dynacast, value: dynacast) } + @Published var dynacast: Bool = false { + didSet { store.value.dynacast = dynacast } } // ConnectOptions - @Published var autoSubscribe: Bool { - didSet { store.set(.autoSubscribe, value: autoSubscribe) } + @Published var autoSubscribe: Bool = true { + didSet { store.value.autoSubscribe = autoSubscribe} } - @Published var publish: Bool { - didSet { store.set(.publishMode, value: publish) } + @Published var publish: Bool = false { + didSet { store.value.publishMode = publish } } - public init(store: SecureStore) { + public init(store: ValueStore) { self.store = store - self.url = store.get(.url) ?? "" - self.token = store.get(.token) ?? "" - self.simulcast = store.get(.simulcast) ?? true - self.adaptiveStream = store.get(.adaptiveStream) ?? false - self.dynacast = store.get(.dynacast) ?? false - self.autoSubscribe = store.get(.autoSubscribe) ?? true - self.publish = store.get(.publishMode) ?? false room.room.add(delegate: self) + + store.onLoaded.then { preferences in + self.url = preferences.url + self.token = preferences.token + self.simulcast = preferences.simulcast + self.adaptiveStream = preferences.adaptiveStream + self.dynacast = preferences.dynacast + self.autoSubscribe = preferences.autoSubscribe + self.publish = preferences.publishMode + } } func connect(entry: ConnectionHistory? = nil) -> Promise { diff --git a/Shared/LiveKitExample.swift b/Shared/LiveKitExample.swift index 3a83fe6..c5884ba 100644 --- a/Shared/LiveKitExample.swift +++ b/Shared/LiveKitExample.swift @@ -1,12 +1,15 @@ import SwiftUI import Logging import LiveKit +import KeychainAccess -let store = SecureStore(service: "io.livekit.example") +let sync = ValueStore(store: Keychain(service: "io.livekit.example"), + key: "preferences", + default: Preferences()) struct RoomContextView: View { - @StateObject var roomCtx = RoomContext(store: store) + @StateObject var roomCtx = RoomContext(store: sync) var shouldShowRoomView: Bool { roomCtx.connectionState.isConnected || roomCtx.connectionState.isReconnecting @@ -77,7 +80,7 @@ extension Decimal { @main struct LiveKitExample: App { - @StateObject var appCtx = AppContext(store: store) + @StateObject var appCtx = AppContext(store: sync) func nearestSafeScale(for target: Int, scale: Double) -> Decimal { diff --git a/Shared/Support/SecureStore.swift b/Shared/Support/SecureStore.swift index 6c5d4ca..a07efd8 100644 --- a/Shared/Support/SecureStore.swift +++ b/Shared/Support/SecureStore.swift @@ -1,47 +1,98 @@ import SwiftUI import KeychainAccess import Combine +import LiveKit +import Promises -enum SecureStoreKeys: String { - case url = "url" - case token = "token" +struct Preferences: Codable, Equatable { + var url = "" + var token = "" // Connect options - case autoSubscribe = "autoSubscribe" - case publishMode = "publishMode" + var autoSubscribe = true + var publishMode = false // Room options - case simulcast = "simulcast" - case adaptiveStream = "adaptiveStream" - case dynacast = "dynacast" + var simulcast = true + var adaptiveStream = false + var dynacast = false // Settings - case videoViewVisible = "videoViewVisible" - case showInformationOverlay = "showInformationOverlay" - case preferMetal = "preferMetal" - case videoViewMode = "videoViewMode" - case videoViewMirrored = "videoViewMirrored" + var videoViewVisible = true + var showInformationOverlay = false + var preferMetal = true + var videoViewMode: VideoView.Mode = .fit + var videoViewMirrored = false - case connectionHistory = "connectionHistory" + var connectionHistory = Set() } -class SecureStore where K.RawValue == String { +let encoder = JSONEncoder() +let decoder = JSONDecoder() - let keychain: Keychain - let encoder = JSONEncoder() - let decoder = JSONDecoder() +// Promise version +extension Keychain { - init(service: String) { - self.keychain = Keychain(service: service) + @discardableResult + func get(_ key: String) -> Promise { + Promise(on: .global()) { () -> T? in + guard let data = try self.getData(key) else { return nil } + return try decoder.decode(T.self, from: data) + } } - func get(_ key: K) -> T? { - guard let data = try? keychain.getData(key.rawValue) else { return nil } - return try? decoder.decode(T.self, from: data) + @discardableResult + func set(_ key: String, value: T) -> Promise { + Promise(on: .global()) { () -> Void in + let data = try encoder.encode(value) + try self.set(data, key: key) + } + } +} + +class ValueStore: ObservableObject { + + private let store: Keychain + private let key: String + private let message = "" + private weak var timer: Timer? + + public let onLoaded = Promise.pending() + + public var value: T { + didSet { + guard oldValue != value else { return } + lazySync() + } + } + + private var storeWithOptions: Keychain { + store.accessibility(.whenUnlocked) + } + + public init(store: Keychain, key: String, `default`: T) { + self.store = store + self.key = key + self.value = `default` + + storeWithOptions.get(key).then { (result: T?) -> Void in + self.value = result ?? self.value + self.onLoaded.fulfill(self.value) + } + } + + deinit { + timer?.invalidate() + } + + public func lazySync() { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: 1, + repeats: false, + block: { _ in self.sync() }) } - func set(_ key: K, value: T) { - guard let data = try? encoder.encode(value) else { return } - try? keychain.set(data, key: key.rawValue) + public func sync() { + storeWithOptions.set(key, value: value) } } diff --git a/iOS/Info.plist b/iOS/Info.plist index 93decbf..463fcdf 100644 --- a/iOS/Info.plist +++ b/iOS/Info.plist @@ -62,5 +62,7 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSFaceIDUsageDescription + Keychain is used to store all preferences.