Welcome Screen (#1259)

* the screen renders and changed the analytics checkmark to be more generic

* welcome screen

* transition

* tests... TODO

* changelog

* changed assets and added multilineTextAlignment

* better naming

* fix for missing after migration but has a weird presentaiton bug

* fix

* icon color
This commit is contained in:
Mauro
2023-07-05 19:55:24 +02:00
committed by GitHub
parent 9ebc57e1cf
commit 00636d98da
17 changed files with 501 additions and 49 deletions
@@ -58,17 +58,32 @@ struct AnalyticsPromptScreen: View {
.foregroundColor(.compound.textSecondary)
}
}
@ViewBuilder
private var checkMark: some View {
Image(systemName: "checkmark.circle")
.symbolVariant(.fill)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.compound.iconAccentTertiary, Color.compound.textOnSolidPrimary)
}
/// The list of re-assurances about analytics.
private var checkmarkList: some View {
VStack(alignment: .leading, spacing: 4) {
AnalyticsPromptScreenCheckmarkItem(title: context.viewState.strings.point1, listPosition: .top)
AnalyticsPromptScreenCheckmarkItem(title: context.viewState.strings.point2, listPosition: .middle)
AnalyticsPromptScreenCheckmarkItem(title: context.viewState.strings.point3, listPosition: .bottom)
checkMarkItem(title: context.viewState.strings.point1, position: .top)
checkMarkItem(title: context.viewState.strings.point2, position: .middle)
checkMarkItem(title: context.viewState.strings.point3, position: .bottom)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
}
@ViewBuilder
private func checkMarkItem(title: String, position: ListPosition) -> some View {
RoundedLabelItem(title: title, listPosition: position) {
checkMark
}
}
/// The stack of enable/disable buttons.
private var buttons: some View {
@@ -16,33 +16,31 @@
import SwiftUI
struct AnalyticsPromptScreenCheckmarkItem: View {
/// Represents the position of a checkmark item in a list.
enum ListPosition {
case top, middle, bottom
/// The corners that should be rounded for this position.
var roundedCorners: UIRectCorner {
switch self {
case .top:
return [.topLeft, .topRight]
case .middle:
return []
case .bottom:
return [.bottomLeft, .bottomRight]
}
/// Represents the position of a checkmark item in a list.
enum ListPosition {
case top, middle, bottom
/// The corners that should be rounded for this position.
var roundedCorners: UIRectCorner {
switch self {
case .top:
return [.topLeft, .topRight]
case .middle:
return []
case .bottom:
return [.bottomLeft, .bottomRight]
}
}
}
struct RoundedLabelItem<Icon: View>: View {
let title: String
let listPosition: ListPosition
let iconContent: () -> Icon
var body: some View {
Label { Text(title) } icon: {
Image(systemName: "checkmark.circle")
.symbolVariant(.fill)
.symbolRenderingMode(.palette)
.foregroundStyle(Color.compound.iconAccentTertiary, Color.compound.textOnSolidPrimary)
iconContent()
}
.labelStyle(CheckmarkLabelStyle())
.padding(.horizontal, 20)
@@ -68,14 +66,32 @@ private struct CheckmarkLabelStyle: LabelStyle {
struct AnalyticsPromptScreenCheckmarkItem_Previews: PreviewProvider {
static let strings = AnalyticsPromptScreenStrings(termsURL: ServiceLocator.shared.settings.analyticsConfiguration.termsURL)
@ViewBuilder
static var testImage1: some View {
Image(systemName: "circle")
}
@ViewBuilder
static var testImage2: some View {
Image(systemName: "square")
}
static var previews: some View {
VStack(alignment: .leading, spacing: 4) {
AnalyticsPromptScreenCheckmarkItem(title: strings.point1, listPosition: .top)
AnalyticsPromptScreenCheckmarkItem(title: strings.point2, listPosition: .middle)
AnalyticsPromptScreenCheckmarkItem(title: "This is a short string.", listPosition: .middle)
AnalyticsPromptScreenCheckmarkItem(title: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.",
listPosition: .bottom)
RoundedLabelItem(title: strings.point1, listPosition: .top) {
testImage1
}
RoundedLabelItem(title: strings.point2, listPosition: .middle) {
testImage2
}
RoundedLabelItem(title: "This is a short string.", listPosition: .middle) {
testImage1
}
RoundedLabelItem(title: "This is a very long string that will be used to test the layout over multiple lines of text to ensure everything is correct.",
listPosition: .bottom) {
testImage2
}
}
.padding()
}
@@ -29,7 +29,9 @@ struct UIConstants {
/// The padding used to the top of the view for breaker screens that don't have a navigation bar.
static let onboardingBreakerScreenTopPadding: CGFloat = 80
static let welcomeScreenTopPadding: CGFloat = 122
/// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`.
static func spacerHeight(in geometry: GeometryProxy) -> CGFloat {
max(0, (geometry.size.height - maxContentHeight) / 2)
@@ -0,0 +1,108 @@
//
// Copyright 2022 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 SwiftUI
struct WelcomeScreen: View {
@ScaledMetric var iconSize = 20
@ObservedObject var context: WelcomeScreenScreenViewModel.Context
var body: some View {
FullscreenDialog(topPadding: UIConstants.welcomeScreenTopPadding) {
mainContent
} bottomContent: {
button
}
.background(OnboardingBackgroundImage())
.environment(\.backgroundStyle, AnyShapeStyle(Color.clear))
.onAppear {
context.send(viewAction: .appeared)
}
}
@ViewBuilder
private var mainContent: some View {
VStack(spacing: 42) {
header
list
}
}
@ViewBuilder
private var header: some View {
VStack(spacing: 32) {
Image(asset: Asset.Images.launchLogo)
.resizable()
.scaledToFit()
.frame(width: 118, height: 118)
.accessibilityHidden(true)
title
}
}
@ViewBuilder
private var title: some View {
VStack(spacing: 12) {
Text(context.viewState.title)
.font(Font.compound.headingLGBold)
.foregroundColor(Color.compound.textPrimary)
.multilineTextAlignment(.center)
Text(context.viewState.subtitle)
.font(Font.compound.bodyMD)
.foregroundColor(Color.compound.textPrimary)
.multilineTextAlignment(.center)
}
}
private var list: some View {
VStack(alignment: .leading, spacing: 4) {
RoundedLabelItem(title: context.viewState.bullet1, listPosition: .top) {
Image(systemName: "exclamationmark.transmission")
.foregroundColor(.compound.iconSecondary)
}
RoundedLabelItem(title: context.viewState.bullet2, listPosition: .middle) {
Image(systemName: "lock")
.foregroundColor(.compound.iconSecondary)
}
RoundedLabelItem(title: context.viewState.bullet3, listPosition: .bottom) {
Image(systemName: "plus.bubble")
.foregroundColor(.compound.iconSecondary)
}
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
}
@ViewBuilder
private var button: some View {
Button {
context.send(viewAction: .doneTapped)
} label: {
Text(context.viewState.buttonTitle)
}
.buttonStyle(.elementAction(.xLarge))
}
}
// MARK: - Previews
struct WelcomeScreen_Previews: PreviewProvider {
static let viewModel = WelcomeScreenScreenViewModel()
static var previews: some View {
WelcomeScreen(context: viewModel.context)
}
}
@@ -0,0 +1,51 @@
//
// Copyright 2022 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 Combine
import SwiftUI
enum WelcomeScreenScreenCoordinatorAction {
case dismiss
}
final class WelcomeScreenScreenCoordinator: CoordinatorProtocol {
private var viewModel: WelcomeScreenScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<WelcomeScreenScreenCoordinatorAction, Never> = .init()
private var cancellables: Set<AnyCancellable> = .init()
var actions: AnyPublisher<WelcomeScreenScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init() {
viewModel = WelcomeScreenScreenViewModel()
}
func start() {
viewModel.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
actionsSubject.send(.dismiss)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(WelcomeScreen(context: viewModel.context))
}
}
@@ -0,0 +1,35 @@
//
// Copyright 2022 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 Foundation
enum WelcomeScreenScreenViewModelAction {
case dismiss
}
struct WelcomeScreenScreenViewState: BindableState {
let title = L10n.screenWelcomeTitle(InfoPlistReader.main.bundleDisplayName)
let subtitle = L10n.screenWelcomeSubtitle
let bullet1 = L10n.screenWelcomeBullet1
let bullet2 = L10n.screenWelcomeBullet2
let bullet3 = L10n.screenWelcomeBullet3
let buttonTitle = L10n.screenWelcomeButton
}
enum WelcomeScreenScreenViewAction {
case doneTapped
case appeared
}
@@ -0,0 +1,45 @@
//
// Copyright 2022 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 Combine
import SwiftUI
typealias WelcomeScreenScreenViewModelType = StateStoreViewModel<WelcomeScreenScreenViewState, WelcomeScreenScreenViewAction>
class WelcomeScreenScreenViewModel: WelcomeScreenScreenViewModelType, WelcomeScreenScreenViewModelProtocol {
let appSettings: AppSettings
private var actionsSubject: PassthroughSubject<WelcomeScreenScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<WelcomeScreenScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(appSettings: AppSettings = ServiceLocator.shared.settings) {
self.appSettings = appSettings
super.init(initialViewState: WelcomeScreenScreenViewState())
}
// MARK: - Public
override func process(viewAction: WelcomeScreenScreenViewAction) {
switch viewAction {
case .doneTapped:
actionsSubject.send(.dismiss)
case .appeared:
appSettings.hasShownWelcomeScreen = true
}
}
}
@@ -0,0 +1,23 @@
//
// Copyright 2022 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 Combine
@MainActor
protocol WelcomeScreenScreenViewModelProtocol {
var actions: AnyPublisher<WelcomeScreenScreenViewModelAction, Never> { get }
var context: WelcomeScreenScreenViewModelType.Context { get }
}