Show the lock screen placeholder on willResignActive. (#2086)

This commit is contained in:
Doug
2023-11-15 15:31:35 +00:00
committed by GitHub
parent 342feb4113
commit 0b9da83470
10 changed files with 164 additions and 65 deletions
@@ -40,6 +40,11 @@ class WindowManager {
[mainWindow, overlayWindow, alternateWindow]
}
/// The task used to switch windows, so that we don't get stuck in the wrong state with a quick switch.
@CancellableTask private var switchTask: Task<Void, Error>?
/// A duration that allows window switching to wait a couple of frames to avoid a transition through black.
private let windowHideDelay = Duration.milliseconds(33)
/// Configures the window manager to operate on the supplied scene.
func configure(with windowScene: UIWindowScene) {
mainWindow = windowScene.keyWindow
@@ -58,21 +63,33 @@ class WindowManager {
/// Shows the main and overlay window combo, hiding the alternate window.
func switchToMain() {
mainWindow.isHidden = false
overlayWindow.isHidden = false
alternateWindow.isHidden = true
switchTask = Task {
mainWindow.isHidden = false
overlayWindow.isHidden = false
// Delay hiding to make sure the main windows are visible.
try await Task.sleep(for: windowHideDelay)
alternateWindow.isHidden = true
}
}
/// Shows the alternate window, hiding the main and overlay combo.
func switchToAlternate() {
alternateWindow.isHidden = false
overlayWindow.isHidden = true
mainWindow.isHidden = true
// We don't know what route the app will use when returning back
// to the main window, so end any editing operation now to avoid
// e.g. the keyboard being displayed on top of a call sheet.
mainWindow.endEditing(true)
switchTask = Task {
alternateWindow.isHidden = false
// We don't know what route the app will use when returning back
// to the main window, so end any editing operation now to avoid
// e.g. the keyboard being displayed on top of a call sheet.
mainWindow.endEditing(true)
// Delay hiding to make sure the alternate window is visible.
try await Task.sleep(for: windowHideDelay)
overlayWindow.isHidden = true
mainWindow.isHidden = true
}
}
}
@@ -49,15 +49,21 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
// Set the initial background state.
showPlaceholder()
notificationCenter.publisher(for: UIApplication.willResignActiveNotification)
.sink { [weak self] _ in
self?.applicationWillResignActive()
}
.store(in: &cancellables)
notificationCenter.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.applicationDidEnterBackground()
}
.store(in: &cancellables)
notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification)
notificationCenter.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in
self?.applicationWillEnterForeground()
self?.applicationDidBecomeActive()
}
.store(in: &cancellables)
}
@@ -68,19 +74,23 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
// MARK: - App unlock
private func applicationDidEnterBackground() {
private func applicationWillResignActive() {
unlockTask = nil
guard appLockService.isEnabled else { return }
appLockService.applicationDidEnterBackground()
showPlaceholder()
}
private func applicationWillEnterForeground() {
private func applicationDidEnterBackground() {
guard appLockService.isEnabled else { return }
appLockService.applicationDidEnterBackground()
showPlaceholder() // Double call but just to be safe
}
private func applicationDidBecomeActive() {
guard appLockService.isEnabled else { return }
guard appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) else {
guard appLockService.computeNeedsUnlock(didBecomeActiveAt: .now) else {
// Reveal the app again if within the grace period.
actionsSubject.send(.unlockApp)
return
@@ -246,23 +246,23 @@ class AppLockServiceMock: AppLockServiceProtocol {
}
//MARK: - computeNeedsUnlock
var computeNeedsUnlockWillEnterForegroundAtCallsCount = 0
var computeNeedsUnlockWillEnterForegroundAtCalled: Bool {
return computeNeedsUnlockWillEnterForegroundAtCallsCount > 0
var computeNeedsUnlockDidBecomeActiveAtCallsCount = 0
var computeNeedsUnlockDidBecomeActiveAtCalled: Bool {
return computeNeedsUnlockDidBecomeActiveAtCallsCount > 0
}
var computeNeedsUnlockWillEnterForegroundAtReceivedDate: Date?
var computeNeedsUnlockWillEnterForegroundAtReceivedInvocations: [Date] = []
var computeNeedsUnlockWillEnterForegroundAtReturnValue: Bool!
var computeNeedsUnlockWillEnterForegroundAtClosure: ((Date) -> Bool)?
var computeNeedsUnlockDidBecomeActiveAtReceivedDate: Date?
var computeNeedsUnlockDidBecomeActiveAtReceivedInvocations: [Date] = []
var computeNeedsUnlockDidBecomeActiveAtReturnValue: Bool!
var computeNeedsUnlockDidBecomeActiveAtClosure: ((Date) -> Bool)?
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool {
computeNeedsUnlockWillEnterForegroundAtCallsCount += 1
computeNeedsUnlockWillEnterForegroundAtReceivedDate = date
computeNeedsUnlockWillEnterForegroundAtReceivedInvocations.append(date)
if let computeNeedsUnlockWillEnterForegroundAtClosure = computeNeedsUnlockWillEnterForegroundAtClosure {
return computeNeedsUnlockWillEnterForegroundAtClosure(date)
func computeNeedsUnlock(didBecomeActiveAt date: Date) -> Bool {
computeNeedsUnlockDidBecomeActiveAtCallsCount += 1
computeNeedsUnlockDidBecomeActiveAtReceivedDate = date
computeNeedsUnlockDidBecomeActiveAtReceivedInvocations.append(date)
if let computeNeedsUnlockDidBecomeActiveAtClosure = computeNeedsUnlockDidBecomeActiveAtClosure {
return computeNeedsUnlockDidBecomeActiveAtClosure(date)
} else {
return computeNeedsUnlockWillEnterForegroundAtReturnValue
return computeNeedsUnlockDidBecomeActiveAtReturnValue
}
}
//MARK: - unlock
@@ -111,8 +111,8 @@ class AppLockService: AppLockServiceProtocol {
timer.applicationDidEnterBackground()
}
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool {
timer.computeLockState(willEnterForegroundAt: date)
func computeNeedsUnlock(didBecomeActiveAt date: Date) -> Bool {
timer.computeLockState(didBecomeActiveAt: date)
}
func unlock(with pinCode: String) -> Bool {
@@ -59,7 +59,7 @@ protocol AppLockServiceProtocol: AnyObject {
/// Informs the service that the app has entered the background.
func applicationDidEnterBackground()
/// Decides whether the app should be unlocked with a PIN code/biometrics on foregrounding.
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool
func computeNeedsUnlock(didBecomeActiveAt date: Date) -> Bool
/// Attempt to unlock the app with the supplied PIN code.
func unlock(with pinCode: String) -> Bool
@@ -21,13 +21,18 @@ class AppLockTimer {
/// The amount of time the app should remain unlocked for whilst backgrounded.
let gracePeriod: TimeInterval
/// Whether the timer considers the app to be locked or not.
/// Whether the timer considers the app to be locked or not. Always starts with a locked app.
///
/// Internally this value may be incorrect, always call `needsUnlock` to get the correct value.
private var isLocked = true
/// The date when the app was last backgrounded whilst in an unlocked state.
private var lastUnlockedBackground: Date?
/// Whether or not the app has moved to the background.
///
/// This allows us to distinguish between `willResignActive` and `didEnterBackground`.
private var isInBackground = false
/// Creates a new timer.
/// - Parameter gracePeriod: The amount of time the app should remain unlocked for whilst backgrounded.
init(gracePeriod: TimeInterval) {
@@ -36,13 +41,18 @@ class AppLockTimer {
/// Signals to the timer to track how long the app will be backgrounded for.
func applicationDidEnterBackground(date: Date = .now) {
isInBackground = true
// Only update the last background date if the app is currently unlocked.
guard !isLocked else { return }
lastUnlockedBackground = date
}
/// Asks the timer to recompute the lock state on foregrounding.
func computeLockState(willEnterForegroundAt date: Date) -> Bool {
/// Asks the timer to recompute the lock state when active. If ``applicationDidEnterBackground`` hasn't been
/// called since the last computation, then this method won't recompute it, instead directly returning the previous state.
func computeLockState(didBecomeActiveAt date: Date) -> Bool {
guard isInBackground else { return isLocked }
isInBackground = false
guard !isLocked, let lastUnlockedBackground else { return true }
isLocked = date.timeIntervalSince(lastUnlockedBackground) >= gracePeriod
@@ -187,7 +187,9 @@ class MockScreen: Identifiable {
context: context)
if id == .appLockFlowAlternateWindow {
guard case .success = appLockService.setupPINCode("2023") else {
let pinCode = "2023"
guard case .success = appLockService.setupPINCode(pinCode),
appLockService.unlock(with: pinCode) else {
fatalError("Failed to preset the PIN code.")
}
}