mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -33,6 +33,7 @@ final class AppSettings {
|
||||
case readReceiptsEnabled
|
||||
case locationEventsEnabled
|
||||
case shareLocationEnabled
|
||||
case hasShownWelcomeScreen
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
@@ -89,6 +90,9 @@ final class AppSettings {
|
||||
|
||||
/// The task identifier used for background app refresh. Also used in main target's the Info.plist
|
||||
let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh"
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.hasShownWelcomeScreen, defaultValue: false, storageType: .userDefaults(store))
|
||||
var hasShownWelcomeScreen: Bool
|
||||
|
||||
// MARK: - Authentication
|
||||
|
||||
@@ -119,7 +123,7 @@ final class AppSettings {
|
||||
/// proxy that it is the first sync (or that an upgrade on the backend will involve a slower sync).
|
||||
@UserPreference(key: UserDefaultsKeys.migratedAccounts, defaultValue: [:], storageType: .userDefaults(store))
|
||||
var migratedAccounts: [String: Bool]
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
var pusherAppId: String {
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
|
||||
private let detailNavigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
|
||||
private let selectedRoomSubject = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
|
||||
@@ -85,6 +85,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
if appSettings.migratedAccounts[userSession.userID] != true {
|
||||
// Show the migration screen for a new account.
|
||||
stateMachine.processEvent(.startWithMigration)
|
||||
} else if !appSettings.hasShownWelcomeScreen {
|
||||
stateMachine.processEvent(.startWithWelcomeScreen)
|
||||
} else {
|
||||
// Otherwise go straight to the home screen.
|
||||
stateMachine.processEvent(.start)
|
||||
@@ -106,7 +108,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
return // Not ready to handle a route.
|
||||
case .roomList:
|
||||
break // Nothing to tidy up on the home screen.
|
||||
case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen:
|
||||
case .feedbackScreen, .sessionVerificationScreen, .settingsScreen, .startChatScreen, .invitesScreen, .welcomeScreen:
|
||||
navigationSplitCoordinator.setSheetCoordinator(nil, animated: animated)
|
||||
}
|
||||
|
||||
@@ -135,13 +137,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
let animated = (context.userInfo as? UserSessionFlowCoordinatorStateMachine.EventUserInfo)?.animated ?? true
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .start, .roomList):
|
||||
self.presentHomeScreen()
|
||||
presentHomeScreen()
|
||||
|
||||
case (.initial, .startWithMigration, .migration):
|
||||
self.presentMigrationScreen() // Full screen cover
|
||||
self.presentHomeScreen() // Have the home screen ready to show underneath
|
||||
presentMigrationScreen() // Full screen cover
|
||||
presentHomeScreen() // Have the home screen ready to show underneath
|
||||
case (.migration, .completeMigration, .roomList):
|
||||
self.dismissMigrationScreen()
|
||||
dismissMigrationScreen()
|
||||
|
||||
case (.initial, .startWithWelcomeScreen, .welcomeScreen):
|
||||
presentHomeScreen()
|
||||
presentWelcomeScreen()
|
||||
case (.roomList, .presentWelcomeScreen, .welcomeScreen):
|
||||
presentWelcomeScreen()
|
||||
case (.welcomeScreen, .dismissedWelcomeScreen, .roomList):
|
||||
break
|
||||
|
||||
case(.roomList, .selectRoom, .roomList):
|
||||
break
|
||||
@@ -154,27 +164,27 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
break
|
||||
|
||||
case (.roomList, .showSessionVerificationScreen, .sessionVerificationScreen):
|
||||
self.presentSessionVerification(animated: animated)
|
||||
presentSessionVerification(animated: animated)
|
||||
case (.sessionVerificationScreen, .dismissedSessionVerificationScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .showSettingsScreen, .settingsScreen):
|
||||
self.presentSettingsScreen(animated: animated)
|
||||
presentSettingsScreen(animated: animated)
|
||||
case (.settingsScreen, .dismissedSettingsScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .feedbackScreen, .feedbackScreen):
|
||||
self.presentFeedbackScreen(animated: animated)
|
||||
presentFeedbackScreen(animated: animated)
|
||||
case (.feedbackScreen, .dismissedFeedbackScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .showStartChatScreen, .startChatScreen):
|
||||
self.presentStartChat(animated: animated)
|
||||
presentStartChat(animated: animated)
|
||||
case (.startChatScreen, .dismissedStartChatScreen, .roomList):
|
||||
break
|
||||
|
||||
case (.roomList, .showInvitesScreen, .invitesScreen):
|
||||
self.presentInvitesList(animated: animated)
|
||||
presentInvitesList(animated: animated)
|
||||
case (.invitesScreen, .showInvitesScreen, .invitesScreen):
|
||||
break
|
||||
case (.invitesScreen, .closedInvitesScreen, .roomList):
|
||||
@@ -216,6 +226,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func dismissMigrationScreen() {
|
||||
navigationSplitCoordinator.setFullScreenCoverCoordinator(nil)
|
||||
|
||||
// Not sure why but the full screen closure dismissal closure doesn't seem to work properly
|
||||
// And not using the DispatchQueue.main results in the the screen getting presented as full screen too.
|
||||
if !appSettings.hasShownWelcomeScreen {
|
||||
DispatchQueue.main.async {
|
||||
self.stateMachine.processEvent(.presentWelcomeScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
@@ -257,6 +275,21 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
sidebarNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
}
|
||||
|
||||
private func presentWelcomeScreen() {
|
||||
let welcomeScreenCoordinator = WelcomeScreenScreenCoordinator()
|
||||
welcomeScreenCoordinator.actions.sink { [weak self] action in
|
||||
switch action {
|
||||
case .dismiss:
|
||||
self?.navigationSplitCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationSplitCoordinator.setSheetCoordinator(welcomeScreenCoordinator) { [weak self] in
|
||||
self?.stateMachine.processEvent(.dismissedWelcomeScreen)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
/// Showing the migration screen whilst the proxy performs an initial sync.
|
||||
case migration
|
||||
|
||||
/// Showing the welcome screen.
|
||||
case welcomeScreen
|
||||
|
||||
/// Showing the home screen. The `selectedRoomId` represents the timeline shown on the detail panel (if any)
|
||||
case roomList(selectedRoomId: String?)
|
||||
@@ -53,11 +56,15 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
enum Event: EventType {
|
||||
/// Start the user session flows
|
||||
case start
|
||||
|
||||
/// Starts the user session flows with the welcome screen
|
||||
case startWithWelcomeScreen
|
||||
/// Start the user session flows with a migration screen.
|
||||
case startWithMigration
|
||||
/// Request to transition from the migration state to the home screen.
|
||||
case completeMigration
|
||||
|
||||
case presentWelcomeScreen
|
||||
case dismissedWelcomeScreen
|
||||
|
||||
/// Request presentation for a particular room
|
||||
/// - Parameter roomId:the room identifier
|
||||
@@ -106,8 +113,10 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
private func configure() {
|
||||
stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomId: nil)])
|
||||
stateMachine.addRoutes(event: .startWithMigration, transitions: [.initial => .migration])
|
||||
stateMachine.addRoutes(event: .startWithWelcomeScreen, transitions: [.initial => .welcomeScreen])
|
||||
stateMachine.addRoutes(event: .completeMigration, transitions: [.migration => .roomList(selectedRoomId: nil)])
|
||||
|
||||
stateMachine.addRoutes(event: .dismissedWelcomeScreen, transitions: [.welcomeScreen => .roomList(selectedRoomId: nil)])
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (event, fromState) {
|
||||
case (.selectRoom(let roomId), .roomList):
|
||||
@@ -146,6 +155,9 @@ class UserSessionFlowCoordinatorStateMachine {
|
||||
|
||||
case (.closedInvitesScreen, .invitesScreen(let selectedRoomId)):
|
||||
return .roomList(selectedRoomId: selectedRoomId)
|
||||
|
||||
case (.presentWelcomeScreen, .roomList):
|
||||
return .welcomeScreen
|
||||
|
||||
default:
|
||||
return nil
|
||||
|
||||
@@ -704,7 +704,7 @@ public enum L10n {
|
||||
public static var screenRoomDetailsInvitePeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_invite_people_title") }
|
||||
/// Leave room
|
||||
public static var screenRoomDetailsLeaveRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_leave_room_title") }
|
||||
/// Notification
|
||||
/// Notifications
|
||||
public static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") }
|
||||
/// People
|
||||
public static var screenRoomDetailsPeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_people_title") }
|
||||
@@ -850,6 +850,20 @@ public enum L10n {
|
||||
public static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") }
|
||||
/// You're in.
|
||||
public static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") }
|
||||
/// Calls, location sharing, search and more will be added later this year.
|
||||
public static var screenWelcomeBullet1: String { return L10n.tr("Localizable", "screen_welcome_bullet_1") }
|
||||
/// Message history for encrypted rooms won’t be available in this update.
|
||||
public static var screenWelcomeBullet2: String { return L10n.tr("Localizable", "screen_welcome_bullet_2") }
|
||||
/// We’d love to hear from you, let us know what you think via the settings page.
|
||||
public static var screenWelcomeBullet3: String { return L10n.tr("Localizable", "screen_welcome_bullet_3") }
|
||||
/// Let's go!
|
||||
public static var screenWelcomeButton: String { return L10n.tr("Localizable", "screen_welcome_button") }
|
||||
/// Here’s what you need to know:
|
||||
public static var screenWelcomeSubtitle: String { return L10n.tr("Localizable", "screen_welcome_subtitle") }
|
||||
/// Welcome to %1$@!
|
||||
public static func screenWelcomeTitle(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "screen_welcome_title", String(describing: p1))
|
||||
}
|
||||
/// Looks like you’re using a new device. Verify with another device to access your encrypted messages moving forwards.
|
||||
public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") }
|
||||
/// Verify it’s you
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+41
-25
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user