Polish for Onboarding Carousel

Add dark mode assets and colours.
Improve layout for 12" iPad and 4" iPhone.
Move isLockedToPortraitOnPhone to RiotNavigationController.
This commit is contained in:
Doug
2022-01-17 17:37:59 +00:00
parent 957815c79e
commit 4fd06abc1b
22 changed files with 2484 additions and 151 deletions
@@ -45,7 +45,6 @@ final class OnboardingSplashScreenCoordinator: Coordinator, Presentable {
let view = OnboardingSplashScreen(viewModel: viewModel.context)
onboardingSplashScreenViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isLockedToPortraitOnPhone = true
onboardingSplashScreenHostingController = hostingController
}
@@ -23,6 +23,7 @@ struct OnboardingSplashScreenPageContent {
let title: String
let message: String
let image: ImageAsset
let darkImage: ImageAsset
let gradient: Gradient
}
@@ -45,7 +46,6 @@ struct OnboardingSplashScreenViewState: BindableState {
var bindings: OnboardingSplashScreenBindings
init() {
#warning("Check why LocaleProvider.locale always returns en")
let locale = Locale.current
let page4Title = locale.identifier.hasPrefix("en") ? "Cut the slack from teams." : VectorL10n.onboardingSplashPage4TitleNoPun
@@ -53,30 +53,34 @@ struct OnboardingSplashScreenViewState: BindableState {
OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage1Title,
message: VectorL10n.onboardingSplashPage1Message,
image: Asset.Images.onboardingSplashScreenPage1,
darkImage: Asset.Images.onboardingSplashScreenPage1Dark,
gradient: Gradient(colors: [
Color(red: 0.73, green: 0.91, blue: 0.81),
Color(red: 0.45, green: 0.78, blue: 0.85)
Color(red: 0.95, green: 0.98, blue: 0.96),
Color(red: 0.89, green: 0.96, blue: 0.97)
])),
OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage2Title,
message: VectorL10n.onboardingSplashPage2Message,
image: Asset.Images.onboardingSplashScreenPage2,
darkImage: Asset.Images.onboardingSplashScreenPage2Dark,
gradient: Gradient(colors: [
Color(red: 0.45, green: 0.78, blue: 0.85),
Color(red: 0.72, green: 0.45, blue: 0.85)
Color(red: 0.89, green: 0.96, blue: 0.97),
Color(red: 0.95, green: 0.89, blue: 0.97)
])),
OnboardingSplashScreenPageContent(title: VectorL10n.onboardingSplashPage3Title,
message: VectorL10n.onboardingSplashPage3Message,
image: Asset.Images.onboardingSplashScreenPage3,
darkImage: Asset.Images.onboardingSplashScreenPage3Dark,
gradient: Gradient(colors: [
Color(red: 0.72, green: 0.45, blue: 0.85),
Color(red: 0.05, green: 0.74, blue: 0.55)
Color(red: 0.95, green: 0.89, blue: 0.97),
Color(red: 0.81, green: 0.95, blue: 0.91)
])),
OnboardingSplashScreenPageContent(title: page4Title,
message: VectorL10n.onboardingSplashPage4Message,
image: Asset.Images.onboardingSplashScreenPage4,
darkImage: Asset.Images.onboardingSplashScreenPage4Dark,
gradient: Gradient(colors: [
Color(red: 0.05, green: 0.74, blue: 0.55),
Color(red: 0.73, green: 0.91, blue: 0.81)
Color(red: 0.81, green: 0.95, blue: 0.91),
Color(red: 0.95, green: 0.98, blue: 0.96)
])),
]
self.bindings = OnboardingSplashScreenBindings()
@@ -1,53 +0,0 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class OnboardingSplashScreenUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockOnboardingSplashScreenScreenState.self
}
override class func createTest() -> MockScreenTest {
return OnboardingSplashScreenUITests(selector: #selector(verifyOnboardingSplashScreenScreen))
}
func verifyOnboardingSplashScreenScreen() throws {
guard let screenState = screenState as? MockOnboardingSplashScreenScreenState else { fatalError("no screen") }
switch screenState {
case .presence(let presence):
verifyOnboardingSplashScreenPresence(presence: presence)
case .longDisplayName(let name):
verifyOnboardingSplashScreenLongName(name: name)
}
}
func verifyOnboardingSplashScreenPresence(presence: OnboardingSplashScreenPresence) {
let presenceText = app.staticTexts["presenceText"]
XCTAssert(presenceText.exists)
XCTAssertEqual(presenceText.label, presence.title)
}
func verifyOnboardingSplashScreenLongName(name: String) {
let displayNameText = app.staticTexts["displayNameText"]
XCTAssert(displayNameText.exists)
XCTAssertEqual(displayNameText.label, name)
}
}
@@ -21,37 +21,5 @@ import Combine
@available(iOS 14.0, *)
class OnboardingSplashScreenViewModelTests: XCTestCase {
private enum Constants {
static let presenceInitialValue: OnboardingSplashScreenPresence = .offline
static let displayName = "Alice"
}
var service: MockOnboardingSplashScreenService!
var viewModel: OnboardingSplashScreenViewModelProtocol!
var context: OnboardingSplashScreenViewModelType.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
service = MockOnboardingSplashScreenService(displayName: Constants.displayName, presence: Constants.presenceInitialValue)
viewModel = OnboardingSplashScreenViewModel.makeOnboardingSplashScreenViewModel(onboardingSplashScreenService: service)
context = viewModel.context
}
func testInitialState() {
XCTAssertEqual(context.viewState.displayName, Constants.displayName)
XCTAssertEqual(context.viewState.presence, Constants.presenceInitialValue)
}
func testFirstPresenceReceived() throws {
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(1).first()
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue])
}
func testPresenceUpdatesReceived() throws {
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(3).first()
let awaitDeferred = xcAwaitDeferred(presencePublisher)
let newPresenceValue1: OnboardingSplashScreenPresence = .online
let newPresenceValue2: OnboardingSplashScreenPresence = .idle
service.simulateUpdate(presence: newPresenceValue1)
service.simulateUpdate(presence: newPresenceValue2)
XCTAssertEqual(try awaitDeferred(), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2])
}
// TODO: Check for any useful tests when finished
}
@@ -38,7 +38,7 @@ struct OnboardingSplashScreen: View {
@ObservedObject var viewModel: OnboardingSplashScreenViewModel.Context
var buttons: some View {
VStack {
VStack(spacing: 12) {
Button { viewModel.send(viewAction: .register) } label: {
Text(VectorL10n.onboardingSplashRegisterButtonTitle)
}
@@ -52,19 +52,27 @@ struct OnboardingSplashScreen: View {
}
var overlay: some View {
VStack {
OnboardingSplashScreenPageIndicator(pageCount: pageCount,
pageIndex: viewModel.pageIndex)
.padding(.vertical, 20)
VStack(spacing: 50) {
Color.clear
Color.clear
buttons
.padding(.horizontal, 16)
VStack {
OnboardingSplashScreenPageIndicator(pageCount: pageCount,
pageIndex: viewModel.pageIndex)
Spacer()
buttons
.padding(.horizontal, 16)
.frame(maxWidth: 600)
Spacer()
}
.background(ViewFrameReader(frame: $overlayFrame))
}
}
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .bottomLeading) {
ZStack(alignment: .leading) {
HStack(spacing: 0) {
OnboardingSplashScreenPage(content: viewModel.viewState.content[pageCount - 1],
overlayHeight: overlayFrame.height + geometry.safeAreaInsets.bottom)
@@ -103,11 +111,9 @@ struct OnboardingSplashScreen: View {
overlay
.frame(width: geometry.size.width)
.background(ViewFrameReader(frame: $overlayFrame))
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 0 : 16)
}
}
.background(theme.colors.background.ignoresSafeArea()) // whilst gradients are transparent
.background(theme.colors.background.ignoresSafeArea())
.accentColor(theme.colors.accent)
.navigationBarHidden(true)
.onAppear { startTimer() }
@@ -28,44 +28,48 @@ struct OnboardingSplashScreenPage: View {
let content: OnboardingSplashScreenPageContent
let overlayHeight: CGFloat
var isDarkModeEnabled: Bool {
theme.identifier != .light
}
// MARK: - Views
@ViewBuilder
var backgroundGradient: some View {
LinearGradient(gradient: content.gradient, startPoint: .leading, endPoint: .trailing)
.flipsForRightToLeftLayoutDirection(true)
.opacity(0.2)
if !isDarkModeEnabled {
LinearGradient(gradient: content.gradient, startPoint: .leading, endPoint: .trailing)
.flipsForRightToLeftLayoutDirection(true)
}
}
var body: some View {
VStack {
Color.clear
.overlay(
Image(content.image.name)
.resizable()
.scaledToFit()
)
Color.clear
.overlay(
VStack(spacing: 8) {
Spacer()
OnboardingSplashScreenTitleText(content.title)
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
Text(content.message)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.multilineTextAlignment(.center)
Spacer()
Spacer()
}
)
Spacer()
.frame(height: overlayHeight)
VStack {
Image(isDarkModeEnabled ? content.darkImage.name : content.image.name)
.resizable()
.scaledToFit()
.frame(maxWidth: 300)
.padding(20)
VStack(spacing: 8) {
OnboardingSplashScreenTitleText(content.title)
.font(theme.fonts.title2B)
.foregroundColor(theme.colors.primaryContent)
Text(content.message)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
.multilineTextAlignment(.center)
}
.padding(.bottom)
Spacer()
Spacer()
.frame(maxHeight: overlayHeight)
}
.padding(.horizontal, 16)
.frame(maxWidth: 600, maxHeight: 750)
}
.padding(.top, 30)
.padding(.horizontal, 16)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(backgroundGradient.ignoresSafeArea())
}
@@ -76,7 +80,7 @@ struct OnboardingSplashScreenPage_Previews: PreviewProvider {
static let content = OnboardingSplashScreenViewState().content
static var previews: some View {
ForEach(0..<content.count, id:\.self) { index in
OnboardingSplashScreenPage(content: content[index], overlayHeight: 55)
OnboardingSplashScreenPage(content: content[index], overlayHeight: 200)
}
}
}