BIT-2017: Display splash until initial view is shown (#517)

This commit is contained in:
Matt Czech 2024-03-08 16:59:29 -06:00 committed by GitHub
parent 992dfec78b
commit dd977f537d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 98 additions and 125 deletions

View File

@ -5,8 +5,13 @@ import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// MARK: Properties
/// Window shown when the app is backgrounded to prevent private information from being visible in the app switcher.
var privacyWindow: UIWindow?
/// Whether the app is still starting up. This ensures the splash view isn't dismissed on start
/// up until the processor has shown the initial view.
var isStartingUp = true
/// Window shown as either the splash view on startup or when the app is backgrounded to
/// prevent private information from being visible in the app switcher.
var splashWindow: UIWindow?
/// The main window for this scene.
var window: UIWindow?
@ -22,62 +27,69 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
guard let appProcessor = (UIApplication.shared.delegate as? AppDelegateType)?.appProcessor else {
if (UIApplication.shared.delegate as? AppDelegateType)?.isTesting == true {
// If the app is running tests, show a testing view.
window = UIWindow(windowScene: windowScene)
window = buildSplashWindow(windowScene: windowScene)
window?.makeKeyAndVisible()
window?.rootViewController = UIHostingController(rootView: Splash())
}
return
}
let appWindow = UIWindow(windowScene: windowScene)
let rootViewController = RootViewController()
appProcessor.start(
appContext: .mainApp,
navigator: rootViewController,
window: appWindow
)
let appWindow = UIWindow(windowScene: windowScene)
appWindow.rootViewController = rootViewController
appWindow.makeKeyAndVisible()
window = appWindow
// Privacy window.
privacyWindow = UIWindow(windowScene: windowScene)
privacyWindow?.windowLevel = UIWindow.Level.alert + 1
// Splash window. This is initially visible until the app's processor has finished starting.
splashWindow = buildSplashWindow(windowScene: windowScene)
let hostingController = UIHostingController(rootView: PrivacyView())
privacyWindow?.rootViewController = hostingController
privacyWindow?.isHidden = false
privacyWindow?.alpha = 0
// Start the app's processor and show the splash view until the initial view is shown.
Task {
await appProcessor.start(
appContext: .mainApp,
navigator: rootViewController,
window: appWindow
)
hideSplash()
isStartingUp = false
}
}
func sceneWillResignActive(_ scene: UIScene) {
privacyWindow?.alpha = 1
showSplash()
}
func sceneDidBecomeActive(_ scene: UIScene) {
guard !isStartingUp else { return }
hideSplash()
}
// MARK: Private
/// Builds the splash window for display in the specified window scene.
///
/// - Parameter windowScene: The window scene that the splash window will be shown in.
/// - Returns: A window containing the splash view.
///
private func buildSplashWindow(windowScene: UIWindowScene) -> UIWindow {
let window = UIWindow(windowScene: windowScene)
window.isHidden = false
window.rootViewController = UIStoryboard(
name: "LaunchScreen",
bundle: .main
).instantiateInitialViewController()
window.windowLevel = UIWindow.Level.alert + 1
return window
}
/// Hides the splash view.
private func hideSplash() {
UIView.animate(withDuration: UI.duration(0.4)) {
self.privacyWindow?.alpha = 0
self.splashWindow?.alpha = 0
}
}
}
// MARK: - PrivacyView
/// The screen shown when the app is backgrounded to prevent private information
/// from being visible in the app switcher.
///
public struct PrivacyView: View {
public var body: some View {
HStack {
Spacer()
VStack {
Spacer()
Image(decorative: Asset.Images.logo)
Spacer()
}
Spacer()
}
.background(Color(asset: Asset.Colors.backgroundPrimary))
/// Shows the splash view.
private func showSplash() {
splashWindow?.alpha = 1
}
}

View File

@ -42,8 +42,12 @@ class SceneDelegateTests: BitwardenTestCase {
let options = TestInstanceFactory.create(UIScene.ConnectionOptions.self)
subject.scene(scene, willConnectTo: session, options: options)
waitFor(!subject.isStartingUp)
XCTAssertNotNil(appProcessor.coordinator)
XCTAssertNotNil(subject.privacyWindow)
XCTAssertFalse(subject.isStartingUp)
XCTAssertNotNil(subject.splashWindow)
XCTAssertEqual(subject.splashWindow?.alpha, 0)
XCTAssertNotNil(subject.window)
XCTAssertTrue(appModule.appCoordinator.isStarted)
}
@ -64,7 +68,7 @@ class SceneDelegateTests: BitwardenTestCase {
subject.scene(scene, willConnectTo: session, options: options)
XCTAssertNil(appProcessor.coordinator)
XCTAssertNil(subject.privacyWindow)
XCTAssertNil(subject.splashWindow)
XCTAssertNil(subject.window)
XCTAssertFalse(appModule.appCoordinator.isStarted)
}
@ -86,6 +90,6 @@ class SceneDelegateTests: BitwardenTestCase {
subject.scene(scene, willConnectTo: session, options: options)
subject.sceneWillResignActive(scene)
XCTAssertEqual(subject.privacyWindow?.alpha, 1)
XCTAssertEqual(subject.splashWindow?.alpha, 1)
}
}

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina5_9" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
@ -18,12 +18,12 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="logoBitwarden" translatesAutoresizingMaskIntoConstraints="NO" id="BLO-Tb-egS">
<rect key="frame" x="68.666666666666686" y="344" width="238" height="124"/>
<rect key="frame" x="46.666666666666657" y="384" width="282" height="44"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="tintColor" name="tintSplash"/>
<constraints>
<constraint firstAttribute="width" constant="238" id="bDQ-uQ-eee"/>
<constraint firstAttribute="height" constant="124" id="ssv-dM-0pp"/>
<constraint firstAttribute="width" constant="282" id="bDQ-uQ-eee"/>
<constraint firstAttribute="height" constant="44" id="ssv-dM-0pp"/>
</constraints>
</imageView>
</subviews>
@ -43,7 +43,7 @@
<resources>
<image name="logoBitwarden" width="282" height="44"/>
<namedColor name="backgroundSplash">
<color red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="tintSplash">
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>

View File

@ -57,12 +57,14 @@ class ActionViewController: UIViewController {
nil
}
appProcessor.start(
appContext: .appExtension,
initialRoute: initialRoute,
navigator: self,
window: nil
)
Task {
await appProcessor.start(
appContext: .appExtension,
initialRoute: initialRoute,
navigator: self,
window: nil
)
}
}
}

View File

@ -85,7 +85,9 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
let appProcessor = AppProcessor(appModule: appModule, services: services)
self.appProcessor = appProcessor
appProcessor.start(appContext: .appExtension, navigator: self, window: nil)
Task {
await appProcessor.start(appContext: .appExtension, navigator: self, window: nil)
}
}
}

View File

@ -61,12 +61,14 @@ class ShareViewController: UIViewController {
authCompletionRoute = .sendItem(.add(content: content, hasPremium: hasPremium ?? false))
appProcessor.start(
appContext: .appExtension,
initialRoute: nil,
navigator: self,
window: nil
)
Task {
await appProcessor.start(
appContext: .appExtension,
initialRoute: nil,
navigator: self,
window: nil
)
}
}
}

View File

@ -73,7 +73,7 @@ public class AppProcessor {
initialRoute: AppRoute? = nil,
navigator: RootNavigator,
window: UIWindow?
) {
) async {
let coordinator = appModule.makeAppCoordinator(appContext: appContext, navigator: navigator)
coordinator.start()
self.coordinator = coordinator
@ -85,17 +85,15 @@ public class AppProcessor {
}
}
Task {
await services.migrationService.performMigrations()
await services.migrationService.performMigrations()
await services.environmentService.loadURLsForActiveAccount()
services.application?.registerForRemoteNotifications()
await services.environmentService.loadURLsForActiveAccount()
services.application?.registerForRemoteNotifications()
if let initialRoute {
coordinator.navigate(to: initialRoute)
} else {
await coordinator.handleEvent(.didStart)
}
if let initialRoute {
coordinator.navigate(to: initialRoute)
} else {
await coordinator.handleEvent(.didStart)
}
}

View File

@ -136,12 +136,15 @@ class AppProcessorTests: BitwardenTestCase {
stateService.accounts = [account]
vaultTimeoutService.shouldSessionTimeout[account.profile.userId] = true
subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
let task = Task {
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
}
notificationCenterService.willEnterForegroundSubject.send()
waitFor(vaultTimeoutService.shouldSessionTimeout[account.profile.userId] == true)
waitFor(coordinator.events.count > 1)
task.cancel()
XCTAssertEqual(
coordinator.events.last,
.didTimeout(userId: account.profile.userId)
@ -155,10 +158,10 @@ class AppProcessorTests: BitwardenTestCase {
}
/// `start(navigator:)` builds the AppCoordinator and navigates to the initial route if provided.
func test_start_initialRoute() {
func test_start_initialRoute() async {
let rootNavigator = MockRootNavigator()
subject.start(
await subject.start(
appContext: .mainApp,
initialRoute: .extensionSetup(.extensionActivation(type: .appExtension)),
navigator: rootNavigator,
@ -176,10 +179,10 @@ class AppProcessorTests: BitwardenTestCase {
}
/// `start(navigator:)` builds the AppCoordinator and navigates to the `.didStart` route.
func test_start_authRoute() {
func test_start_authRoute() async {
let rootNavigator = MockRootNavigator()
subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
await subject.start(appContext: .mainApp, navigator: rootNavigator, window: nil)
waitFor(!coordinator.events.isEmpty)

View File

@ -1,50 +0,0 @@
import SwiftUI
// MARK: - Splash
public struct Splash: View, Equatable {
// MARK: Properties
/// The background color
///
let backgroundColor: Color
/// Should the nav bar be hidden?
///
let hidesNavBar: Bool
/// Should the view display the logo?
///
let showsLogo: Bool
// MARK: View
public var body: some View {
ZStack {
backgroundColor
.ignoresSafeArea()
if showsLogo {
Asset.Images.logo.swiftUIImage
.resizable()
.scaledToFit()
.frame(width: 238)
}
}
.ignoresSafeArea()
.navigationBarHidden(hidesNavBar)
.navigationBarBackButtonHidden(hidesNavBar)
}
// MARK: Initializers
public init(
backgroundColor: Color = Asset.Colors.backgroundPrimary.swiftUIColor,
hidesNavBar: Bool = true,
showsLogo: Bool = true
) {
self.backgroundColor = backgroundColor
self.hidesNavBar = hidesNavBar
self.showsLogo = showsLogo
}
}