Skip to content

Commit

Permalink
access keychain on bg thread
Browse files Browse the repository at this point in the history
  • Loading branch information
hiroshihorie committed Feb 12, 2022
1 parent 319f328 commit ec74a4c
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 76 deletions.
8 changes: 4 additions & 4 deletions LiveKitExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
50 changes: 30 additions & 20 deletions Shared/Controllers/AppContext.swift
Original file line number Diff line number Diff line change
@@ -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<SecureStoreKeys>
private let store: ValueStore<Preferences>

@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<ConnectionHistory> {
didSet { store.set(.connectionHistory, value: connectionHistory) }
@Published var connectionHistory: Set<ConnectionHistory> = [] {
didSet { store.value.connectionHistory = connectionHistory }
}

public init(store: SecureStore<SecureStoreKeys>) {
public init(store: ValueStore<Preferences>) {
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<ConnectionHistory>()

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
}
}
}
49 changes: 26 additions & 23 deletions Shared/Controllers/RoomContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<SecureStoreKeys>
private let store: ValueStore<Preferences>

// Used to show connection error dialog
// private var didClose: Bool = false
Expand All @@ -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<SecureStoreKeys>) {
public init(store: ValueStore<Preferences>) {
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<Room> {
Expand Down
9 changes: 6 additions & 3 deletions Shared/LiveKitExample.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import SwiftUI
import Logging
import LiveKit
import KeychainAccess

let store = SecureStore<SecureStoreKeys>(service: "io.livekit.example")
let sync = ValueStore<Preferences>(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
Expand Down Expand Up @@ -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 {

Expand Down
103 changes: 77 additions & 26 deletions Shared/Support/SecureStore.swift
Original file line number Diff line number Diff line change
@@ -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<ConnectionHistory>()
}

class SecureStore<K: RawRepresentable> 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<T: Decodable>(_ key: String) -> Promise<T?> {
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<T: Decodable>(_ 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<T: Encodable>(_ key: String, value: T) -> Promise<Void> {
Promise(on: .global()) { () -> Void in
let data = try encoder.encode(value)
try self.set(data, key: key)
}
}
}

class ValueStore<T: Codable & Equatable>: ObservableObject {

private let store: Keychain
private let key: String
private let message = ""
private weak var timer: Timer?

public let onLoaded = Promise<T>.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<T: Encodable>(_ 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)
}
}
2 changes: 2 additions & 0 deletions iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Keychain is used to store all preferences.</string>
</dict>
</plist>

0 comments on commit ec74a4c

Please sign in to comment.