Skip to content
29 changes: 29 additions & 0 deletions Domain/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Domain",
platforms: [.iOS(.v13)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Domain",
targets: ["Domain"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Domain",
dependencies: []),
.testTarget(
name: "DomainTests",
dependencies: ["Domain"])
]
)
5 changes: 5 additions & 0 deletions Domain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Domain

The Domain Layer is a crucial component of a software architecture that represents the
core business logic and rules of the application. It acts as the heart of the system,
encapsulating all the domain-specific logic and entities.
9 changes: 9 additions & 0 deletions Domain/Sources/Domain/Entities/User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public struct User {
public let email: String
public let tokenID: String

public init(email: String, tokenID: String) {
self.email = email
self.tokenID = tokenID
}
}
3 changes: 3 additions & 0 deletions Domain/Sources/Domain/UseCases/LoginUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public protocol LoginUseCase {
func login(email: String, password: String) async throws -> User
}
10 changes: 10 additions & 0 deletions Domain/Tests/DomainTests/DomainTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import XCTest
@testable import Domain

final class DomainTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
}
}
35 changes: 34 additions & 1 deletion Healthy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
025511D42A3D0A8B00295B91 /* CreateAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025511CC2A3D0A8B00295B91 /* CreateAccountViewModel.swift */; };
025511D52A3D0A8B00295B91 /* CreateAccountviewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025511CD2A3D0A8B00295B91 /* CreateAccountviewModelType.swift */; };
025511D62A3D0A8B00295B91 /* CreateAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025511CE2A3D0A8B00295B91 /* CreateAccountViewController.swift */; };
02557D712A697DF90022756A /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02557D702A697DF90022756A /* LoginUseCase.swift */; };
02557D732A697E8E0022756A /* Domain in Frameworks */ = {isa = PBXBuildFile; productRef = 02557D722A697E8E0022756A /* Domain */; };
025A47532A336B05008BF85A /* DashboardViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025A474F2A336B05008BF85A /* DashboardViewModelType.swift */; };
025A47542A336B05008BF85A /* DashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025A47502A336B05008BF85A /* DashboardViewController.swift */; };
025A47552A336B05008BF85A /* DashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025A47512A336B05008BF85A /* DashboardViewModel.swift */; };
Expand Down Expand Up @@ -72,6 +74,7 @@
029C89142A71E79A00AF380B /* NewRecipesCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029C89132A71E79A00AF380B /* NewRecipesCollectionViewLayout.swift */; };
02C37B862A71CF5600FD58E5 /* NewRecipesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C37B852A71CF5600FD58E5 /* NewRecipesView.swift */; };
02C3F8402A71CB3A000E62B2 /* UIView+ReuseIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7918F03D2A1F6448000F36A5 /* UIView+ReuseIdentifier.swift */; };
02CB9B642A6ADE4E00C1E765 /* DefaultLoginUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CB9B632A6ADE4E00C1E765 /* DefaultLoginUseCaseTests.swift */; };
02EEC3FB2A4C22C90007DA0C /* SavedRecipesViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EEC3FA2A4C22C90007DA0C /* SavedRecipesViewModelMock.swift */; };
02EEC3FD2A4C23090007DA0C /* SavedRecipesViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EEC3FC2A4C23090007DA0C /* SavedRecipesViewControllerTests.swift */; };
02EEC3FF2A4C244A0007DA0C /* SavedRecipesViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EEC3FE2A4C244A0007DA0C /* SavedRecipesViewModelTests.swift */; };
Expand Down Expand Up @@ -200,6 +203,8 @@
025511CC2A3D0A8B00295B91 /* CreateAccountViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountViewModel.swift; sourceTree = "<group>"; };
025511CD2A3D0A8B00295B91 /* CreateAccountviewModelType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountviewModelType.swift; sourceTree = "<group>"; };
025511CE2A3D0A8B00295B91 /* CreateAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateAccountViewController.swift; sourceTree = "<group>"; };
02557D6E2A697C3D0022756A /* Domain */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Domain; sourceTree = "<group>"; };
02557D702A697DF90022756A /* LoginUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCase.swift; sourceTree = "<group>"; };
025A474F2A336B05008BF85A /* DashboardViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModelType.swift; sourceTree = "<group>"; };
025A47502A336B05008BF85A /* DashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewController.swift; sourceTree = "<group>"; };
025A47512A336B05008BF85A /* DashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -231,6 +236,7 @@
029C89112A71E78000AF380B /* NSLayoutConstraint+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraint+Helpers.swift"; sourceTree = "<group>"; };
029C89132A71E79A00AF380B /* NewRecipesCollectionViewLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRecipesCollectionViewLayout.swift; sourceTree = "<group>"; };
02C37B852A71CF5600FD58E5 /* NewRecipesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRecipesView.swift; sourceTree = "<group>"; };
02CB9B632A6ADE4E00C1E765 /* DefaultLoginUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultLoginUseCaseTests.swift; sourceTree = "<group>"; };
02EEC3FA2A4C22C90007DA0C /* SavedRecipesViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedRecipesViewModelMock.swift; sourceTree = "<group>"; };
02EEC3FC2A4C23090007DA0C /* SavedRecipesViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedRecipesViewControllerTests.swift; sourceTree = "<group>"; };
02EEC3FE2A4C244A0007DA0C /* SavedRecipesViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedRecipesViewModelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -321,6 +327,7 @@
6D611B9F2A299E3600A1FC65 /* GoogleSignInSwift in Frameworks */,
025511B72A3C623000295B91 /* NewRelic in Frameworks */,
6DDA138D2A1109E8004390D4 /* Factory in Frameworks */,
02557D732A697E8E0022756A /* Domain in Frameworks */,
0296F7952A4342B500DBC86A /* FacebookLogin in Frameworks */,
6D611B9D2A299E3600A1FC65 /* GoogleSignIn in Frameworks */,
);
Expand Down Expand Up @@ -457,6 +464,14 @@
path = CreateAccount;
sourceTree = "<group>";
};
02557D6F2A697D8E0022756A /* UseCases */ = {
isa = PBXGroup;
children = (
02557D702A697DF90022756A /* LoginUseCase.swift */,
);
path = UseCases;
sourceTree = "<group>";
};
025A474E2A336AE3008BF85A /* Dashboard */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -523,6 +538,7 @@
027DDA122A0E6A660052818C = {
isa = PBXGroup;
children = (
02557D6E2A697C3D0022756A /* Domain */,
02892EC52A585558001A3DB4 /* Networking */,
027DDA1D2A0E6A660052818C /* Healthy */,
027DDA342A0E6A680052818C /* HealthyTests */,
Expand Down Expand Up @@ -597,6 +613,7 @@
025511C32A3D058800295B91 /* AppCoordinator.swift */,
027DDA1E2A0E6A660052818C /* AppDelegate.swift */,
027DDA202A0E6A660052818C /* SceneDelegate.swift */,
02557D6F2A697D8E0022756A /* UseCases */,
);
path = Classes;
sourceTree = "<group>";
Expand Down Expand Up @@ -680,6 +697,14 @@
name = Frameworks;
sourceTree = "<group>";
};
02CB9B5F2A6AD5DD00C1E765 /* LoginUseCase */ = {
isa = PBXGroup;
children = (
02CB9B632A6ADE4E00C1E765 /* DefaultLoginUseCaseTests.swift */,
);
path = LoginUseCase;
sourceTree = "<group>";
};
02EEC3F92A4C22B70007DA0C /* Mocks */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -826,6 +851,7 @@
6DFEC6202A2E97FF0090B2E2 /* Modules */ = {
isa = PBXGroup;
children = (
02CB9B5F2A6AD5DD00C1E765 /* LoginUseCase */,
6DA228F92A431FC60011E43E /* Search */,
B2B7AEE22A3B4CE500AF04F5 /* MainTabBarController */,
A21C51F62A42FF6400850B15 /* SavedRecipes */,
Expand Down Expand Up @@ -1067,6 +1093,7 @@
0296F7922A4342B500DBC86A /* FacebookCore */,
0296F7942A4342B500DBC86A /* FacebookLogin */,
02892EC72A58575C001A3DB4 /* Networking */,
02557D722A697E8E0022756A /* Domain */,
);
productName = Healthy;
productReference = 027DDA1B2A0E6A660052818C /* Healthy.app */;
Expand Down Expand Up @@ -1213,7 +1240,7 @@
025511B82A3C624E00295B91 /* NewRelic */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
buildActionMask = 12;
files = (
);
inputFileListPaths = (
Expand Down Expand Up @@ -1330,6 +1357,7 @@
79311FD82A19214700764707 /* UITextField+Style.swift in Sources */,
0255119F2A3C5D7300295B91 /* SearchViewModel.swift in Sources */,
B2D402DF2A3BB1D700FDB941 /* UILabel+Style.swift in Sources */,
02557D712A697DF90022756A /* LoginUseCase.swift in Sources */,
A2FA41112A44412500C9C9A0 /* UIView+Style.swift in Sources */,
6D2145472A44C9EB0085C519 /* SearchFilter.swift in Sources */,
79311FD02A191D3700764707 /* FormTextField.swift in Sources */,
Expand Down Expand Up @@ -1376,6 +1404,7 @@
6DFEC6232A2E982C0090B2E2 /* LoginViewControllerTests.swift in Sources */,
B85D26A72A3902DF000A463D /* EmailValidatorsTests.swift in Sources */,
6DFEC6252A2EA5770090B2E2 /* LoginViewModelMock.swift in Sources */,
02CB9B642A6ADE4E00C1E765 /* DefaultLoginUseCaseTests.swift in Sources */,
B29ABA5C2A3469CA00171A0C /* MainTapBarControllerTest.swift in Sources */,
A21C51FB2A43036A00850B15 /* UITableViewMock.swift in Sources */,
A21C51F92A42FF8D00850B15 /* SavedRecipesTableViewCellTests.swift in Sources */,
Expand Down Expand Up @@ -1766,6 +1795,10 @@
package = 025511B52A3C623000295B91 /* XCRemoteSwiftPackageReference "newrelic-ios-agent-spm" */;
productName = NewRelic;
};
02557D722A697E8E0022756A /* Domain */ = {
isa = XCSwiftPackageProductDependency;
productName = Domain;
};
02892EC72A58575C001A3DB4 /* Networking */ = {
isa = XCSwiftPackageProductDependency;
productName = Networking;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,20 @@ private extension LoginViewController {
viewModel.isLoadingIndicatorPublisher
.sink { _ in
// TODO: Show loading indicator.

}
.store(in: &subscriptions)
}

func bindErrorMessage() {
viewModel.isShowErrorMessagePublisher
.sink { _ in
// TODO: Show error message.
viewModel.errorPublisher
.sink { [weak self] error in
let alertController = UIAlertController(
title: "Error!!",
message: error.localizedDescription,
preferredStyle: .alert)

self?.present(alertController, animated: true)
}
.store(in: &subscriptions)
}
Expand Down
37 changes: 30 additions & 7 deletions Healthy/Classes/Modules/Onboarding/Login/LoginViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import Foundation
import Combine
import Domain
import Factory
import UIKit

final class LoginViewModel {
@Injected(\.loginUseCase) private var loginUseCase
private unowned let coordinator: OnboardingCoordinator
private var subscriptions = Set<AnyCancellable>()
@Published private var email: String = ""
@Published private var password: String = ""
@Published private var isLoadingState: Bool = false
@Published private var isShowErrorMessage: String = ""
@Published private var errorSubject = PassthroughSubject<Error, Never>()
@Published private var isLoginEnabled: Bool = true
@Published private var isLoginStatus: Bool = false

// Add a reference to the UIButton that you want to animate
private weak var signInButton: UIButton?

init(coordinator: OnboardingCoordinator) {
self.coordinator = coordinator
_ = User(email: "[email protected]", tokenID: "12345678")
}
}

Expand All @@ -29,12 +37,25 @@ extension LoginViewModel: LoginViewModelInput {
}

func performSignIn() {
// Sending API Request
// Caching User
// Did complete signIn
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
Task { @MainActor in
// show loading
DispatchQueue.main.async {
self.signInButton?.startAnimating()
}
defer {
// dismiss loading
DispatchQueue.main.async {
self.signInButton?.stopAnimating()
}
}
do {
_ = try await loginUseCase.login(
email: email,
password: password)
self.coordinator.didFinishSignIn()
} catch {
// handle error
print(error)
}
}
}
Expand All @@ -51,6 +72,7 @@ extension LoginViewModel: LoginViewModelInput {
// End loading
} catch {
// Show error
errorSubject.send(error)
}
}
}
Expand All @@ -59,12 +81,13 @@ extension LoginViewModel: LoginViewModelInput {
// MARK: Output

extension LoginViewModel: LoginViewModelOutput {

var isLoadingIndicatorPublisher: AnyPublisher<Bool, Never> {
$isLoadingState.eraseToAnyPublisher()
}

var isShowErrorMessagePublisher: AnyPublisher<String, Never> {
$isShowErrorMessage.eraseToAnyPublisher()
var errorPublisher: AnyPublisher<Error, Never> {
errorSubject.eraseToAnyPublisher()
}

var isLoginEnabledPublisher: AnyPublisher<Bool, Never> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ protocol LoginViewModelInput {

protocol LoginViewModelOutput {
var isLoadingIndicatorPublisher: AnyPublisher<Bool, Never> { get }
var isShowErrorMessagePublisher: AnyPublisher<String, Never> { get }
var errorPublisher: AnyPublisher<Error, Never> { get }
var isLoginEnabledPublisher: AnyPublisher<Bool, Never> { get }
var isLoginStatusPublisher: AnyPublisher<Bool, Never> { get }
}
30 changes: 30 additions & 0 deletions Healthy/Classes/UseCases/LoginUseCase.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Domain
import Networking
import Factory
import Foundation

extension Container {
var loginUseCase: Factory<LoginUseCase> {
Factory(self) {
DefaultLoginUseCase()
}
}
}

final class DefaultLoginUseCase: LoginUseCase {

@Injected(\.networking) private var networking

func login(email: String, password: String) async throws -> User {
// TODO: replace mock Request with Actual Request
let request = LoginRequest(email: email, password: password)
do {
_ = try await networking.dispatch(request)
return User(email: "[email protected]", tokenID: "12345678")

} catch {
// If Error
throw NSError(domain: "Some Error", code: -1)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import XCTest
@testable import Domain
@testable import Networking
@testable import Healthy

final class DefaultLoginUseCaseTests: XCTestCase {
var loginUseCase: DefaultLoginUseCase!

override func setUp() {
super.setUp()
loginUseCase = DefaultLoginUseCase()
}

override func tearDown() {
loginUseCase = nil
super.tearDown()
}

func testLogin() async throws {
let email = "[email protected]"
let password = "password"

let user = try await loginUseCase.login(email: email, password: password)

XCTAssertEqual(user.email, "[email protected]")
XCTAssertEqual(user.tokenID, "12345678")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import Combine
@testable import Healthy

final class LoginViewModelMock: LoginViewModelType {
private let errorSubject = PassthroughSubject<Error, Never>()

var isLoadingIndicatorPublisher: AnyPublisher<Bool, Never> {
Just(false).eraseToAnyPublisher()
var errorPublisher: AnyPublisher<Error, Never> {
errorSubject.eraseToAnyPublisher()
}

var isShowErrorMessagePublisher: AnyPublisher<String, Never> {
Just("").eraseToAnyPublisher()
var isLoadingIndicatorPublisher: AnyPublisher<Bool, Never> {
Just(false).eraseToAnyPublisher()
}

var isLoginEnabledPublisher: AnyPublisher<Bool, Never> {
Expand Down
Loading