Skip to content

Commit

Permalink
Use ViewModels for simplified testing
Browse files Browse the repository at this point in the history
  • Loading branch information
LaurentTreguier committed Jul 30, 2024
1 parent 1cd498d commit 5f56791
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 156 deletions.
48 changes: 32 additions & 16 deletions Fyreplace.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,16 @@
4DB10B502C4FEBFC00634BF6 /* HelpCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB10B4F2C4FEBFC00634BF6 /* HelpCommands.swift */; };
4DB2E36D2C416611007F958D /* SubmitButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB2E36C2C416611007F958D /* SubmitButton.swift */; };
4DB2E36F2C418F5C007F958D /* DynamicForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB2E36E2C418F5C007F958D /* DynamicForm.swift */; };
4DB2E3722C419612007F958D /* LoginScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB2E3712C419612007F958D /* LoginScreenTests.swift */; };
4DB2E3742C428C08007F958D /* AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB2E3732C428C08007F958D /* AppTests.swift */; };
4DB2E3772C428D8B007F958D /* XCUIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB2E3762C428D8B007F958D /* XCUIApplication.swift */; };
4DB2E3792C43E304007F958D /* RegisterScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB2E3782C43E304007F958D /* RegisterScreenTests.swift */; };
4DCE062B2C08E5E200F69AF1 /* CompactNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCE062A2C08E5E200F69AF1 /* CompactNavigation.swift */; };
4DCE062D2C08E65300F69AF1 /* RegularNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DCE062C2C08E65300F69AF1 /* RegularNavigation.swift */; };
4DE140CD2C52688000A699AE /* DestinationCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DE140CC2C52687F00A699AE /* DestinationCommands.swift */; };
4DFB90642C57B98C00D4DABF /* AnyPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFB90632C57B98C00D4DABF /* AnyPublisher.swift */; };
4DFB906C2C58FC6900D4DABF /* LoginScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFB906B2C58FC6900D4DABF /* LoginScreen+ViewModel.swift */; };
4DFB90702C5908DE00D4DABF /* LoginScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFB906F2C5908DE00D4DABF /* LoginScreenTests.swift */; };
4DFB90732C59162000D4DABF /* RegisterScreen+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFB90722C59162000D4DABF /* RegisterScreen+ViewModel.swift */; };
4DFB90762C59173C00D4DABF /* RegisterScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DFB90752C59173C00D4DABF /* RegisterScreenTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -124,10 +126,8 @@
4DB10B4F2C4FEBFC00634BF6 /* HelpCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpCommands.swift; sourceTree = "<group>"; };
4DB2E36C2C416611007F958D /* SubmitButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmitButton.swift; sourceTree = "<group>"; };
4DB2E36E2C418F5C007F958D /* DynamicForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicForm.swift; sourceTree = "<group>"; };
4DB2E3712C419612007F958D /* LoginScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenTests.swift; sourceTree = "<group>"; };
4DB2E3732C428C08007F958D /* AppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTests.swift; sourceTree = "<group>"; };
4DB2E3762C428D8B007F958D /* XCUIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCUIApplication.swift; sourceTree = "<group>"; };
4DB2E3782C43E304007F958D /* RegisterScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterScreenTests.swift; sourceTree = "<group>"; };
4DCE062A2C08E5E200F69AF1 /* CompactNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactNavigation.swift; sourceTree = "<group>"; };
4DCE062C2C08E65300F69AF1 /* RegularNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularNavigation.swift; sourceTree = "<group>"; };
4DCE06312C09E19400F69AF1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
Expand All @@ -136,6 +136,10 @@
4DD826FD2BF9FDC500799CEB /* Config.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = Config.sh; sourceTree = "<group>"; };
4DE140CC2C52687F00A699AE /* DestinationCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationCommands.swift; sourceTree = "<group>"; };
4DFB90632C57B98C00D4DABF /* AnyPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyPublisher.swift; sourceTree = "<group>"; };
4DFB906B2C58FC6900D4DABF /* LoginScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginScreen+ViewModel.swift"; sourceTree = "<group>"; };
4DFB906F2C5908DE00D4DABF /* LoginScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenTests.swift; sourceTree = "<group>"; };
4DFB90722C59162000D4DABF /* RegisterScreen+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RegisterScreen+ViewModel.swift"; sourceTree = "<group>"; };
4DFB90752C59173C00D4DABF /* RegisterScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterScreenTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -252,6 +256,7 @@
4D9B3B432C36E64F00A8F7AD /* Extensions */,
4D13AF7C2C4E904E00845FDB /* Data */,
4D54C95E2BF27C78001DE071 /* Views */,
4DFB906A2C58FC4400D4DABF /* ViewModels */,
4DB10B4E2C4FEBCE00634BF6 /* Commands */,
4D54C92B2BF2608A001DE071 /* FyreplaceApp.swift */,
);
Expand All @@ -269,6 +274,7 @@
4D54C93C2BF26090001DE071 /* FyreplaceTests */ = {
isa = PBXGroup;
children = (
4DFB906E2C5908BF00D4DABF /* ViewModels */,
4D54C93D2BF26090001DE071 /* DestinationTests.swift */,
);
path = FyreplaceTests;
Expand All @@ -278,7 +284,6 @@
isa = PBXGroup;
children = (
4DB2E3752C428D7F007F958D /* Extensions */,
4DB2E3702C4195FB007F958D /* Screens */,
4D54C9472BF26090001DE071 /* NavigationTests.swift */,
4DB2E3732C428C08007F958D /* AppTests.swift */,
);
Expand Down Expand Up @@ -356,15 +361,6 @@
path = Commands;
sourceTree = "<group>";
};
4DB2E3702C4195FB007F958D /* Screens */ = {
isa = PBXGroup;
children = (
4DB2E3712C419612007F958D /* LoginScreenTests.swift */,
4DB2E3782C43E304007F958D /* RegisterScreenTests.swift */,
);
path = Screens;
sourceTree = "<group>";
};
4DB2E3752C428D7F007F958D /* Extensions */ = {
isa = PBXGroup;
children = (
Expand All @@ -387,6 +383,24 @@
path = Config;
sourceTree = "<group>";
};
4DFB906A2C58FC4400D4DABF /* ViewModels */ = {
isa = PBXGroup;
children = (
4DFB906B2C58FC6900D4DABF /* LoginScreen+ViewModel.swift */,
4DFB90722C59162000D4DABF /* RegisterScreen+ViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
4DFB906E2C5908BF00D4DABF /* ViewModels */ = {
isa = PBXGroup;
children = (
4DFB906F2C5908DE00D4DABF /* LoginScreenTests.swift */,
4DFB90752C59173C00D4DABF /* RegisterScreenTests.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -575,8 +589,10 @@
4DB10B502C4FEBFC00634BF6 /* HelpCommands.swift in Sources */,
4D9B3B452C36F46F00A8F7AD /* NSTextContentType.swift in Sources */,
4D54C96F2BF4E9DF001DE071 /* DraftsScreen.swift in Sources */,
4DFB90732C59162000D4DABF /* RegisterScreen+ViewModel.swift in Sources */,
4DB2E36F2C418F5C007F958D /* DynamicForm.swift in Sources */,
4DFB90642C57B98C00D4DABF /* AnyPublisher.swift in Sources */,
4DFB906C2C58FC6900D4DABF /* LoginScreen+ViewModel.swift in Sources */,
4DCE062B2C08E5E200F69AF1 /* CompactNavigation.swift in Sources */,
4DCE062D2C08E65300F69AF1 /* RegularNavigation.swift in Sources */,
4D54C9692BF4E8F4001DE071 /* FeedScreen.swift in Sources */,
Expand All @@ -595,6 +611,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4DFB90702C5908DE00D4DABF /* LoginScreenTests.swift in Sources */,
4DFB90762C59173C00D4DABF /* RegisterScreenTests.swift in Sources */,
4D54C93E2BF26090001DE071 /* DestinationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -605,8 +623,6 @@
files = (
4D54C9482BF26090001DE071 /* NavigationTests.swift in Sources */,
4DB2E3772C428D8B007F958D /* XCUIApplication.swift in Sources */,
4DB2E3722C419612007F958D /* LoginScreenTests.swift in Sources */,
4DB2E3792C43E304007F958D /* RegisterScreenTests.swift in Sources */,
4DB2E3742C428C08007F958D /* AppTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
6 changes: 2 additions & 4 deletions Fyreplace.xcodeproj/xcshareddata/xcschemes/Fyreplace.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4D54C9382BF26090001DE071"
Expand All @@ -42,8 +41,7 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4D54C9422BF26090001DE071"
Expand Down
10 changes: 10 additions & 0 deletions Fyreplace/ViewModels/LoginScreen+ViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

extension LoginScreen {
final class ViewModel: ObservableObject {
@Published
var identifier = ""

var canSubmit: Bool { 3 ... 254 ~= identifier.count }
}
}
15 changes: 15 additions & 0 deletions Fyreplace/ViewModels/RegisterScreen+ViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

extension RegisterScreen {
final class ViewModel: ObservableObject {
@Published
var username = ""

@Published
var email = ""

var isUsernameValid: Bool { 3 ... 50 ~= username.count }
var isEmailValid: Bool { 3 ... 254 ~= email.count && email.contains("@") }
var canSubmit: Bool { isUsernameValid && isEmailValid }
}
}
19 changes: 10 additions & 9 deletions Fyreplace/Views/Screens/LoginScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import SwiftUI
struct LoginScreen: View {
let namespace: Namespace.ID

@SceneStorage("LoginScreen.identifier")
private var identifier = ""
@ObservedObject
var viewModel: ViewModel

@FocusState
private var focused: Bool

private var isIdentifierValid: Bool { 3 ... 254 ~= identifier.count }

var body: some View {
DynamicForm {
let submitButton = SubmitButton(text: "Login.Submit", canSubmit: isIdentifierValid, submit: submit)
let submitButton = SubmitButton(text: "Login.Submit", canSubmit: viewModel.canSubmit, submit: submit)
.matchedGeometryEffect(id: "submit", in: namespace)

#if os(macOS)
Expand All @@ -28,7 +26,7 @@ struct LoginScreen: View {
) {
EnvironmentPicker(namespace: namespace)

TextField("Login.Identifier", text: $identifier, prompt: Text("Login.Identifier.Prompt"))
TextField("Login.Identifier", text: $viewModel.identifier, prompt: Text("Login.Identifier.Prompt"))
.autocorrectionDisabled()
.focused($focused)
.onSubmit(submit)
Expand All @@ -38,7 +36,7 @@ struct LoginScreen: View {
.labelsHidden()
#endif
}
.onAppear { focused = identifier.isEmpty }
.onAppear { focused = viewModel.identifier.isEmpty }

#if !os(macOS)
submitButton
Expand All @@ -48,7 +46,7 @@ struct LoginScreen: View {
}

private func submit() {
guard isIdentifierValid else { return }
guard viewModel.canSubmit else { return }
focused = false
}
}
Expand All @@ -58,6 +56,9 @@ struct LoginScreen: View {
@Namespace
var namespace

LoginScreen(namespace: namespace)
@StateObject
var viewModel = LoginScreen.ViewModel()

LoginScreen(namespace: namespace, viewModel: viewModel)
}
}
28 changes: 12 additions & 16 deletions Fyreplace/Views/Screens/RegisterScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,15 @@ import SwiftUI
struct RegisterScreen: View {
let namespace: Namespace.ID

@SceneStorage("LoginScreen.username")
private var username = ""

@SceneStorage("LoginScreen.email")
private var email = ""
@ObservedObject
var viewModel: ViewModel

@FocusState
private var focused: FocusedField?

private var isUsernameValid: Bool { 3 ... 50 ~= username.count }
private var isEmailValid: Bool { 3 ... 254 ~= email.count && email.contains("@") }
private var canSubmit: Bool { isUsernameValid && isEmailValid }

var body: some View {
DynamicForm {
let submitButton = SubmitButton(text: "Register.Submit", canSubmit: canSubmit, submit: submit)
let submitButton = SubmitButton(text: "Register.Submit", canSubmit: viewModel.canSubmit, submit: submit)
.matchedGeometryEffect(id: "submit", in: namespace)

#if os(macOS)
Expand All @@ -37,7 +30,7 @@ struct RegisterScreen: View {
) {
EnvironmentPicker(namespace: namespace)

TextField("Register.Username", text: $username, prompt: usernamePrompt)
TextField("Register.Username", text: $viewModel.username, prompt: usernamePrompt)
.textContentType(.username)
.autocorrectionDisabled()
.focused($focused, equals: .username)
Expand All @@ -46,7 +39,7 @@ struct RegisterScreen: View {
.accessibilityIdentifier("username")
.matchedGeometryEffect(id: "first-field", in: namespace)

TextField("Register.Email", text: $email, prompt: emailPrompt)
TextField("Register.Email", text: $viewModel.email, prompt: emailPrompt)
.textContentType(.email)
.autocorrectionDisabled()
.focused($focused, equals: .email)
Expand All @@ -55,9 +48,9 @@ struct RegisterScreen: View {
.accessibilityIdentifier("email")
}
.onAppear {
if username.isEmpty {
if viewModel.username.isEmpty {
focused = .username
} else if email.isEmpty {
} else if viewModel.email.isEmpty {
focused = .email
}
}
Expand All @@ -70,7 +63,7 @@ struct RegisterScreen: View {
}

private func submit() {
guard canSubmit else { return }
guard viewModel.canSubmit else { return }
focused = nil
}
}
Expand All @@ -85,6 +78,9 @@ private enum FocusedField {
@Namespace
var namespace

RegisterScreen(namespace: namespace)
@StateObject
var viewModel = RegisterScreen.ViewModel()

RegisterScreen(namespace: namespace, viewModel: viewModel)
}
}
10 changes: 8 additions & 2 deletions Fyreplace/Views/Screens/Screen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ struct Screen: View {
@Namespace
private var namespace

@StateObject
private var loginViewModel = LoginScreen.ViewModel()

@StateObject
private var registerViewModel = RegisterScreen.ViewModel()

var body: some View {
switch destination {
case .feed:
Expand All @@ -21,9 +27,9 @@ struct Screen: View {
case .settings:
SettingsScreen()
case .login:
LoginScreen(namespace: namespace)
LoginScreen(namespace: namespace, viewModel: loginViewModel)
case .register:
RegisterScreen(namespace: namespace)
RegisterScreen(namespace: namespace, viewModel: registerViewModel)
}
}
}
23 changes: 23 additions & 0 deletions FyreplaceTests/ViewModels/LoginScreenTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import XCTest

@testable
import Fyreplace

final class LoginScreenViewModelTests: XCTestCase {
func testIdentifierMustHaveCorrectLength() {
let viewModel = LoginScreen.ViewModel()

for i in 0 ..< 3 {
viewModel.identifier = .init(repeating: "a", count: i)
XCTAssertFalse(viewModel.canSubmit)
}

for i in 3 ... 254 {
viewModel.identifier = .init(repeating: "a", count: i)
XCTAssertTrue(viewModel.canSubmit)
}

viewModel.identifier = .init(repeating: "a", count: 255)
XCTAssertFalse(viewModel.canSubmit)
}
}
Loading

0 comments on commit 5f56791

Please sign in to comment.