mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
BIT-2017: Display splash until initial view is shown (#517)
This commit is contained in:
parent
992dfec78b
commit
dd977f537d
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user