Resolve issues with lock screen (#273)

* Make lock screen a View extension

- Reuse in global theme (apply to all modals)

- Use a ZStack rather than replace (retain content/navigation)

- Share lock state across all LockableView
This commit is contained in:
Davide De Rosa 2023-03-25 16:47:08 +01:00 committed by GitHub
parent 76084dbd30
commit 6af4bb7e0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 68 additions and 49 deletions

View File

@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Option to lock app when entering background (iOS). [#270](https://github.com/passepartoutvpn/passepartout-apple/pull/270) - Option to lock app when entering background (iOS). [#270](https://github.com/passepartoutvpn/passepartout-apple/pull/270), [#271](https://github.com/passepartoutvpn/passepartout-apple/pull/271), [#273](https://github.com/passepartoutvpn/passepartout-apple/pull/273)
- 3D Touch items (iOS). [#267](https://github.com/passepartoutvpn/passepartout-apple/pull/267) - 3D Touch items (iOS). [#267](https://github.com/passepartoutvpn/passepartout-apple/pull/267)
- Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243) - Ukranian translations (Dmitry Chirkin). [#243](https://github.com/passepartoutvpn/passepartout-apple/pull/243)
- Restore DNS "Domain" setting. [#260](https://github.com/passepartoutvpn/passepartout-apple/pull/260) - Restore DNS "Domain" setting. [#260](https://github.com/passepartoutvpn/passepartout-apple/pull/260)

View File

@ -25,6 +25,7 @@
import SwiftUI import SwiftUI
import PassepartoutLibrary import PassepartoutLibrary
import LocalAuthentication
extension View { extension View {
var themeIdiom: UIUserInterfaceIdiom { var themeIdiom: UIUserInterfaceIdiom {
@ -57,6 +58,9 @@ extension View {
extension View { extension View {
func themeGlobal() -> some View { func themeGlobal() -> some View {
themeNavigationViewStyle() themeNavigationViewStyle()
#if !targetEnvironment(macCatalyst)
.themeLockScreen()
#endif
.themeTint() .themeTint()
.listStyle(themeListStyleValue()) .listStyle(themeListStyleValue())
.toggleStyle(themeToggleStyleValue()) .toggleStyle(themeToggleStyleValue())
@ -490,6 +494,42 @@ extension View {
} }
} }
// MARK: Lock screen
extension View {
func themeLockScreen() -> some View {
@AppStorage(AppPreference.locksInBackground.rawValue) var locksInBackground = false
return LockableView(
locksInBackground: $locksInBackground,
content: {
self
},
lockedContent: LogoView.init,
unlockBlock: Self.themeUnlockScreenBlock
)
}
private static func themeUnlockScreenBlock(isLocked: Binding<Bool>) {
let context = LAContext()
let policy: LAPolicy = .deviceOwnerAuthentication
var error: NSError?
guard context.canEvaluatePolicy(policy, error: &error) else {
isLocked.wrappedValue = false
return
}
Task { @MainActor in
do {
let isAuthorized = try await context.evaluatePolicy(
policy,
localizedReason: L10n.Global.Messages.unlockApp
)
isLocked.wrappedValue = !isAuthorized
} catch {
}
}
}
}
// MARK: Validation // MARK: Validation
extension View { extension View {

View File

@ -30,11 +30,9 @@ import PassepartoutLibrary
struct PassepartoutApp: App { struct PassepartoutApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@AppStorage(AppPreference.locksInBackground.rawValue) private var locksInBackground = false
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
WindowGroup { WindowGroup {
mainView MainView()
.withoutTitleBar() .withoutTitleBar()
.onIntentActivity(IntentDispatcher.connectVPN) .onIntentActivity(IntentDispatcher.connectVPN)
.onIntentActivity(IntentDispatcher.disableVPN) .onIntentActivity(IntentDispatcher.disableVPN)
@ -46,19 +44,6 @@ struct PassepartoutApp: App {
.onIntentActivity(IntentDispatcher.untrustCurrentNetwork) .onIntentActivity(IntentDispatcher.untrustCurrentNetwork)
} }
} }
private var mainView: some View {
#if targetEnvironment(macCatalyst)
MainView()
#else
LockableView(
reason: L10n.Global.Messages.unlockApp,
locksInBackground: $locksInBackground,
content: MainView.init,
lockedContent: LogoView.init
)
#endif
}
} }
extension View { extension View {

View File

@ -24,37 +24,35 @@
// //
import SwiftUI import SwiftUI
import LocalAuthentication
struct LockableView<Content: View, LockedContent: View>: View { struct LockableView<Content: View, LockedContent: View>: View {
let reason: String
@Binding var locksInBackground: Bool @Binding var locksInBackground: Bool
let content: () -> Content let content: () -> Content
let lockedContent: () -> LockedContent let lockedContent: () -> LockedContent
let unlockBlock: (Binding<Bool>) -> Void
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase
@State private var didAppear = false @ObservedObject private var lock: Lock = .shared
@State private var isLocked = false private var isLocked: Binding<Bool> {
.init {
Lock.shared.isActive
} set: {
Lock.shared.isActive = $0
}
}
var body: some View { var body: some View {
Group { ZStack {
if !isLocked { content()
content() if isLocked.wrappedValue {
} else {
lockedContent() lockedContent()
} }
}.onChange(of: scenePhase, perform: onScenePhase) }.onChange(of: scenePhase, perform: onScenePhase)
.onAppear {
if !didAppear && locksInBackground {
didAppear = true
isLocked = true
}
}
} }
private func onScenePhase(_ scenePhase: ScenePhase) { private func onScenePhase(_ scenePhase: ScenePhase) {
@ -74,30 +72,26 @@ struct LockableView<Content: View, LockedContent: View>: View {
guard locksInBackground else { guard locksInBackground else {
return return
} }
isLocked = true isLocked.wrappedValue = true
} }
func unlockIfNeeded() { func unlockIfNeeded() {
guard locksInBackground else { guard locksInBackground else {
isLocked = false isLocked.wrappedValue = false
return return
} }
guard isLocked else { guard isLocked.wrappedValue else {
return return
} }
let context = LAContext() unlockBlock(isLocked)
let policy: LAPolicy = .deviceOwnerAuthentication }
var error: NSError? }
guard context.canEvaluatePolicy(policy, error: &error) else {
isLocked = false private class Lock: ObservableObject {
return static let shared = Lock()
}
Task { @MainActor in @Published var isActive = true
do {
let isAuthorized = try await context.evaluatePolicy(policy, localizedReason: reason) private init() {
isLocked = !isAuthorized
} catch {
}
}
} }
} }