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
- 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)
- 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)

View File

@ -25,6 +25,7 @@
import SwiftUI
import PassepartoutLibrary
import LocalAuthentication
extension View {
var themeIdiom: UIUserInterfaceIdiom {
@ -57,6 +58,9 @@ extension View {
extension View {
func themeGlobal() -> some View {
themeNavigationViewStyle()
#if !targetEnvironment(macCatalyst)
.themeLockScreen()
#endif
.themeTint()
.listStyle(themeListStyleValue())
.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
extension View {

View File

@ -30,11 +30,9 @@ import PassepartoutLibrary
struct PassepartoutApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@AppStorage(AppPreference.locksInBackground.rawValue) private var locksInBackground = false
@SceneBuilder var body: some Scene {
WindowGroup {
mainView
MainView()
.withoutTitleBar()
.onIntentActivity(IntentDispatcher.connectVPN)
.onIntentActivity(IntentDispatcher.disableVPN)
@ -46,19 +44,6 @@ struct PassepartoutApp: App {
.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 {

View File

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