mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Simplify the screen templates
This commit is contained in:
committed by
Stefan Ceriu
parent
28c2490910
commit
6e5854abf9
@@ -148,10 +148,8 @@ class MockScreen: Identifiable {
|
|||||||
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
userIndicatorController: ServiceLocator.shared.userIndicatorController))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
case .simpleRegular:
|
case .templateScreen:
|
||||||
return TemplateScreenCoordinator(parameters: .init(promptType: .regular))
|
return TemplateScreenCoordinator(parameters: .init())
|
||||||
case .simpleUpgrade:
|
|
||||||
return TemplateScreenCoordinator(parameters: .init(promptType: .upgrade))
|
|
||||||
case .home:
|
case .home:
|
||||||
let navigationStackCoordinator = NavigationStackCoordinator()
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
|
let session = MockUserSession(clientProxy: MockClientProxy(userID: "@mock:matrix.org"),
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case analyticsPrompt
|
case analyticsPrompt
|
||||||
case analyticsSettingsScreen
|
case analyticsSettingsScreen
|
||||||
case migration
|
case migration
|
||||||
case simpleRegular
|
case templateScreen
|
||||||
case simpleUpgrade
|
|
||||||
case home
|
case home
|
||||||
case settings
|
case settings
|
||||||
case bugReport
|
case bugReport
|
||||||
|
|||||||
+7
-11
@@ -17,13 +17,10 @@
|
|||||||
import Combine
|
import Combine
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TemplateScreenCoordinatorParameters {
|
struct TemplateScreenCoordinatorParameters { }
|
||||||
let promptType: TemplateScreenPromptType
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TemplateScreenCoordinatorAction {
|
enum TemplateScreenCoordinatorAction {
|
||||||
case accept
|
case done
|
||||||
case cancel
|
|
||||||
|
|
||||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||||
}
|
}
|
||||||
@@ -41,18 +38,17 @@ final class TemplateScreenCoordinator: CoordinatorProtocol {
|
|||||||
init(parameters: TemplateScreenCoordinatorParameters) {
|
init(parameters: TemplateScreenCoordinatorParameters) {
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
viewModel = TemplateScreenViewModel(promptType: parameters.promptType)
|
viewModel = TemplateScreenViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
viewModel.actions.sink { [weak self] action in
|
viewModel.actions.sink { [weak self] action in
|
||||||
|
MXLog.info("Coordinator: received view model action: \(action)")
|
||||||
|
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
switch action {
|
switch action {
|
||||||
case .accept:
|
case .done:
|
||||||
MXLog.info("User accepted the prompt.")
|
self.actionsSubject.send(.done)
|
||||||
self.actionsSubject.send(.accept)
|
|
||||||
case .cancel:
|
|
||||||
self.actionsSubject.send(.cancel)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|||||||
@@ -16,50 +16,25 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum TemplateScreenPromptType {
|
|
||||||
case regular
|
|
||||||
case upgrade
|
|
||||||
}
|
|
||||||
|
|
||||||
extension TemplateScreenPromptType: Identifiable, CaseIterable {
|
|
||||||
var id: Self { self }
|
|
||||||
|
|
||||||
var title: String {
|
|
||||||
switch self {
|
|
||||||
case .regular:
|
|
||||||
return "Make this chat public?"
|
|
||||||
case .upgrade:
|
|
||||||
return "Privacy warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var imageSystemName: String {
|
|
||||||
switch self {
|
|
||||||
case .regular:
|
|
||||||
return "app.gift"
|
|
||||||
case .upgrade:
|
|
||||||
return "shield"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TemplateScreenViewModelAction {
|
enum TemplateScreenViewModelAction {
|
||||||
case accept
|
case done
|
||||||
case cancel
|
|
||||||
|
|
||||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TemplateScreenViewState: BindableState {
|
struct TemplateScreenViewState: BindableState {
|
||||||
var promptType: TemplateScreenPromptType
|
var title: String
|
||||||
var count: Int
|
var placeholder: String
|
||||||
|
var bindings: TemplateScreenViewStateBindings
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TemplateScreenViewStateBindings {
|
||||||
|
var composerText: String
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TemplateScreenViewAction {
|
enum TemplateScreenViewAction {
|
||||||
case incrementCount
|
case done
|
||||||
case decrementCount
|
case textChanged
|
||||||
case accept
|
|
||||||
case cancel
|
|
||||||
|
|
||||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -26,22 +26,22 @@ class TemplateScreenViewModel: TemplateScreenViewModelType, TemplateScreenViewMo
|
|||||||
actionsSubject.eraseToAnyPublisher()
|
actionsSubject.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(promptType: TemplateScreenPromptType, initialCount: Int = 0) {
|
init() {
|
||||||
super.init(initialViewState: TemplateScreenViewState(promptType: promptType, count: 0))
|
super.init(initialViewState: TemplateScreenViewState(title: "Template title",
|
||||||
|
placeholder: "Enter something here",
|
||||||
|
bindings: .init(composerText: "Initial composer text")))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
|
|
||||||
override func process(viewAction: TemplateScreenViewAction) {
|
override func process(viewAction: TemplateScreenViewAction) {
|
||||||
|
MXLog.info("View model: received view action: \(viewAction)")
|
||||||
|
|
||||||
switch viewAction {
|
switch viewAction {
|
||||||
case .accept:
|
case .done:
|
||||||
actionsSubject.send(.accept)
|
actionsSubject.send(.done)
|
||||||
case .cancel:
|
case .textChanged:
|
||||||
actionsSubject.send(.cancel)
|
MXLog.info("View model: composer text changed to: \(state.bindings.composerText)")
|
||||||
case .incrementCount:
|
|
||||||
state.count += 1
|
|
||||||
case .decrementCount:
|
|
||||||
state.count -= 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,75 +17,30 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TemplateScreen: View {
|
struct TemplateScreen: View {
|
||||||
@Environment(\.colorScheme) private var colorScheme
|
|
||||||
|
|
||||||
var counterColor: Color {
|
|
||||||
colorScheme == .light ? .compound.textSecondary : .compound.textInfoPrimary
|
|
||||||
}
|
|
||||||
|
|
||||||
@ObservedObject var context: TemplateScreenViewModel.Context
|
@ObservedObject var context: TemplateScreenViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
Form {
|
||||||
mainContent
|
Section {
|
||||||
.padding(.top, 50)
|
TextField(text: $context.composerText) {
|
||||||
.padding(.horizontal)
|
Text(context.viewState.placeholder)
|
||||||
.readableFrame()
|
.compoundFormTextFieldPlaceholder()
|
||||||
}
|
|
||||||
.safeAreaInset(edge: .bottom) {
|
|
||||||
buttons
|
|
||||||
.padding(.horizontal)
|
|
||||||
.padding(.vertical)
|
|
||||||
.readableFrame()
|
|
||||||
.background(Color.compound.bgSubtleSecondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The main content of the view to be shown in a scroll view.
|
|
||||||
var mainContent: some View {
|
|
||||||
VStack(spacing: 36) {
|
|
||||||
Text(context.viewState.promptType.title)
|
|
||||||
.font(.compound.headingMDBold)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.foregroundColor(.compound.textPrimary)
|
|
||||||
.accessibilityIdentifier("title")
|
|
||||||
|
|
||||||
Image(systemName: context.viewState.promptType.imageSystemName)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFit()
|
|
||||||
.frame(width: 100)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Counter: \(context.viewState.count)")
|
|
||||||
.font(.compound.bodyLG)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.foregroundColor(counterColor)
|
|
||||||
|
|
||||||
Button("−") {
|
|
||||||
context.send(viewAction: .decrementCount)
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.elementGhost())
|
.textFieldStyle(.compoundForm)
|
||||||
|
|
||||||
Button("+") {
|
Button {
|
||||||
context.send(viewAction: .incrementCount)
|
context.send(viewAction: .done)
|
||||||
|
} label: {
|
||||||
|
Label("Done", systemImage: "door.left.hand.closed")
|
||||||
}
|
}
|
||||||
.buttonStyle(.elementGhost())
|
.buttonStyle(.compoundFormCentred())
|
||||||
}
|
}
|
||||||
|
.compoundFormSection()
|
||||||
}
|
}
|
||||||
}
|
.compoundForm()
|
||||||
|
.navigationTitle(context.viewState.title)
|
||||||
/// The action buttons shown at the bottom of the view.
|
.onChange(of: context.composerText) { _ in
|
||||||
var buttons: some View {
|
context.send(viewAction: .textChanged)
|
||||||
VStack {
|
|
||||||
Button { context.send(viewAction: .accept) } label: {
|
|
||||||
Text("Accept")
|
|
||||||
}
|
|
||||||
.buttonStyle(.elementAction(.xLarge))
|
|
||||||
|
|
||||||
Button { context.send(viewAction: .cancel) } label: {
|
|
||||||
Text("Cancel")
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,12 +48,10 @@ struct TemplateScreen: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct TemplateScreen_Previews: PreviewProvider {
|
struct TemplateScreen_Previews: PreviewProvider {
|
||||||
static let regularViewModel = TemplateScreenViewModel(promptType: .regular)
|
static let viewModel = TemplateScreenViewModel()
|
||||||
static let upgradeViewModel = TemplateScreenViewModel(promptType: .upgrade)
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
TemplateScreen(context: regularViewModel.context)
|
NavigationStack {
|
||||||
.previewDisplayName("Regular")
|
TemplateScreen(context: viewModel.context)
|
||||||
TemplateScreen(context: upgradeViewModel.context)
|
}
|
||||||
.previewDisplayName("Upgrade")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,25 +19,12 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class TemplateScreenUITests: XCTestCase {
|
class TemplateScreenUITests: XCTestCase {
|
||||||
func testRegularScreen() async throws {
|
func testScreen() async throws {
|
||||||
let app = Application.launch(.simpleRegular)
|
let app = Application.launch(.templateScreen)
|
||||||
|
|
||||||
let title = app.staticTexts["title"]
|
let title = app.staticTexts["Template title"]
|
||||||
XCTAssert(title.exists)
|
XCTAssert(title.exists)
|
||||||
|
|
||||||
XCTAssertEqual(title.label, "Make this chat public?")
|
|
||||||
|
|
||||||
try await app.assertScreenshot(.simpleRegular)
|
try await app.assertScreenshot(.templateScreen)
|
||||||
}
|
|
||||||
|
|
||||||
func testUpgradeScreen() async throws {
|
|
||||||
let app = Application.launch(.simpleUpgrade)
|
|
||||||
|
|
||||||
let title = app.staticTexts["title"]
|
|
||||||
XCTAssert(title.exists)
|
|
||||||
|
|
||||||
XCTAssertEqual(title.label, "Privacy warning")
|
|
||||||
|
|
||||||
try await app.assertScreenshot(.simpleUpgrade)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-14
@@ -20,10 +20,6 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class TemplateScreenViewModelTests: XCTestCase {
|
class TemplateScreenViewModelTests: XCTestCase {
|
||||||
private enum Constants {
|
|
||||||
static let counterInitialValue = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var viewModel: TemplateScreenViewModelProtocol!
|
var viewModel: TemplateScreenViewModelProtocol!
|
||||||
|
|
||||||
var context: TemplateScreenViewModelType.Context {
|
var context: TemplateScreenViewModelType.Context {
|
||||||
@@ -31,21 +27,17 @@ class TemplateScreenViewModelTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
viewModel = TemplateScreenViewModel(promptType: .regular, initialCount: Constants.counterInitialValue)
|
viewModel = TemplateScreenViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func testInitialState() {
|
func testInitialState() {
|
||||||
XCTAssertEqual(context.viewState.count, Constants.counterInitialValue)
|
XCTAssertFalse(context.viewState.placeholder.isEmpty)
|
||||||
|
XCTAssertFalse(context.composerText.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCounter() async throws {
|
func testCounter() async throws {
|
||||||
context.send(viewAction: .incrementCount)
|
context.composerText = "123"
|
||||||
XCTAssertEqual(context.viewState.count, 1)
|
context.send(viewAction: .textChanged)
|
||||||
|
XCTAssertEqual(context.composerText, "123")
|
||||||
context.send(viewAction: .incrementCount)
|
|
||||||
XCTAssertEqual(context.viewState.count, 2)
|
|
||||||
|
|
||||||
context.send(viewAction: .decrementCount)
|
|
||||||
XCTAssertEqual(context.viewState.count, 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user