()
25 |
26 | override func setUp() {
27 | super.setUp()
28 | navigator = SettingsNavigatorMock()
29 | useCase = SettingsUseCaseMock()
30 | viewModel = SettingsViewModel(navigator: navigator).with {
31 | $0.settingsUseCase = useCase
32 | }
33 |
34 | input = SettingsViewModel.Input(loadTrigger: loadTrigger.asDriver(),
35 | logoutTrigger: logoutTrigger.asDriver(),
36 | toggleDarkModeTrigger: toggleDarkModeTrigger.asDriver(),
37 | toggleLanguageTrigger: toggleLanguageTrigger.asDriver())
38 | cancelBag = CancelBag()
39 | output = viewModel.transform(input, cancelBag: cancelBag)
40 | }
41 |
42 | func test_logoutTrigger_logout() {
43 | // act
44 | logoutTrigger.send(())
45 |
46 | // assert
47 | wait {
48 | XCTAssertTrue(self.useCase.logoutCalled)
49 | }
50 | }
51 |
52 | func test_loadTrigger_getDarkModeStatusAndCurrentLanguage() {
53 | // act
54 | loadTrigger.send(())
55 |
56 | // assert
57 | wait {
58 | XCTAssertEqual(self.output.isJapanese, false)
59 | XCTAssertEqual(self.output.isDarkMode, false)
60 | }
61 | }
62 |
63 | func test_toggleDarkModeTrigger_toggleDarkModeStatus() {
64 | // act
65 | useCase.getDarkModeStatusReturnValue = false
66 | toggleDarkModeTrigger.send(())
67 |
68 | // assert
69 | wait {
70 | XCTAssertEqual(self.output.isDarkMode, true)
71 | }
72 | }
73 |
74 | func test_toggleLanguageTrigger_toggleLanguage() {
75 | // act
76 | useCase.getCurrentLanguageReturnValue = SupportedLanguage.english.code
77 | toggleLanguageTrigger.send(())
78 |
79 | // assert
80 | wait {
81 | XCTAssertEqual(self.output.isJapanese, true)
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/BaseSwiftUITests/Utils/Extension/XCTestCase+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTestCase+.swift
3 | // BaseSwiftUITests
4 | //
5 | // Created by phan.duong.ngoc on 25/04/2024.
6 | //
7 |
8 | import XCTest
9 |
10 | extension XCTestCase {
11 | func wait(interval: TimeInterval = 0.3, completion: @escaping (() -> Void)) {
12 | let expectation = expectation(description: "")
13 |
14 | DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
15 | completion()
16 | expectation.fulfill()
17 | }
18 |
19 | waitForExpectations(timeout: interval + 0.1) // add 0.1 for sure asyn after called
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/BaseSwiftUITests/Utils/TestError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TestError.swift
3 | // BaseSwiftUITests
4 | //
5 | // Created by phan.duong.ngoc on 25/04/2024.
6 | //
7 |
8 | @testable import BaseSwiftUI
9 | import Foundation
10 |
11 | struct TestError: APIError {
12 | static let message = "Test error message"
13 |
14 | var errorDescription: String? {
15 | return NSLocalizedString(TestError.message,
16 | value: "",
17 | comment: "")
18 | }
19 | }
20 |
21 | struct AccountBannedError: APIError {
22 | static let message = "Your account has been banned."
23 |
24 | var errorDescription: String? {
25 | return NSLocalizedString(AccountBannedError.message,
26 | value: "",
27 | comment: "")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Clean Architecture using SwiftUI + Combine
2 |
3 | The example app uses the Clean Architecture approach with SwiftUI and Combine.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## Features
17 | - **Xcodegen**: Automated project configuration.
18 | - **Clean Architecture**: Separation of concerns with clear boundaries between layers.
19 | - **SwiftUI**: Declarative UI framework for building modern iOS apps.
20 | - **Combine**: Reactive framework for handling asynchronous events and data streams.
21 | - **Dependency Injection**: Easy management of dependencies and testability.
22 | - **Unit Tests**: Includes unit tests for business logic and use cases.
23 | - **Dark Mode**: Programmatically supports Dark Mode switch.
24 | - **SwiftData**: SwiftData support.
25 |
26 | ## Requirements
27 |
28 | - Xcode 15.0+
29 | - Swift 5.7+
30 | - [Homebrew](https://brew.sh/) installed
31 |
32 | ## Installation
33 |
34 | 1. **Install Homebrew Dependencies**:
35 |
36 | ```bash
37 | brew install xcodegen swiftlint swiftformat
38 | ```
39 |
40 | 2. **Clone the Repository**:
41 |
42 | ```bash
43 | git clone git@github.com:ngocpd-1250/clean_swiftui_combine.git
44 | cd BaseSwiftUI
45 | ```
46 |
47 | 3. **Generate Xcode Project**:
48 |
49 | Use `xcodegen` to generate the Xcode project:
50 |
51 | ```bash
52 | xcodegen
53 | ```
54 |
55 | 4. **Open Xcode**:
56 |
57 | Open the generated Xcode project:
58 |
59 | ```bash
60 | open BaseSwiftUI.xcodeproj
61 | ```
62 |
63 | 5. **Build and Run**:
64 |
65 | Build and run the project in Xcode.
66 |
67 | ## Project Structure
68 |
69 | The project follows the Clean Architecture principles with the following layers:
70 |
71 | - **Presentation**: SwiftUI views, navigators and view models.
72 | - **Domain**: Business logic and use cases.
73 | - **Data**: Data sources and repositories.
74 |
75 | ## Linting
76 |
77 | 1. **Linting**: [SwiftLint](https://github.com/realm/SwiftLint)
78 | 2. **Formatting**: [SwiftFormat](https://github.com/nicklockwood/SwiftFormat)
79 |
80 | ## Packages
81 |
82 | 1. [LinkNavigator](https://github.com/interactord/LinkNavigator.git): for navigation management
83 | 2. [Factory](https://github.com/hmlongco/Factory.git): for dependency injection
84 | 3. [Rswift](https://github.com/mac-cain13/R.swift.git): for resource management
85 | 4. [Kingfisher](https://github.com/onevcat/Kingfisher.git): for loading and caching remote images
86 | 5. [Alamofire](https://github.com/Alamofire/Alamofire.git): for API requests
87 |
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | # app_identifier("[[APP_IDENTIFIER]]") # The bundle identifier of your app
2 | # apple_id("[[APPLE_ID]]") # Your Apple Developer Portal username
3 |
4 |
5 | # For more information about the Appfile, see:
6 | # https://docs.fastlane.tools/advanced/#appfile
7 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 |
2 | default_platform(:ios)
3 |
4 | platform :ios do
5 | desc "Run Unit Test"
6 | lane :test_dev do
7 | run_tests(scheme: "BaseSwiftUI Dev",
8 | xcargs: "-skipPackagePluginValidation")
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ----
3 |
4 | # Installation
5 |
6 | Make sure you have the latest version of the Xcode command line tools installed:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
13 |
14 | # Available Actions
15 |
16 | ## iOS
17 |
18 | ### ios test_dev
19 |
20 | ```sh
21 | [bundle exec] fastlane ios test_dev
22 | ```
23 |
24 | Run Unit Test
25 |
26 | ----
27 |
28 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
29 |
30 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
31 |
32 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
33 |
--------------------------------------------------------------------------------
/fastlane/report.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/fastlane/test_output/report.junit:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/screenshots/add_todo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/add_todo.png
--------------------------------------------------------------------------------
/screenshots/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/login.png
--------------------------------------------------------------------------------
/screenshots/movie_detail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/movie_detail.png
--------------------------------------------------------------------------------
/screenshots/onboarding.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/onboarding.png
--------------------------------------------------------------------------------
/screenshots/register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/register.png
--------------------------------------------------------------------------------
/screenshots/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/settings.png
--------------------------------------------------------------------------------
/screenshots/todo_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/todo_list.png
--------------------------------------------------------------------------------
/screenshots/top_movie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngocpd-1250/clean_swiftui_combine/23884c6f09a506d20130d7c3bbfc0a6a42b63be1/screenshots/top_movie.png
--------------------------------------------------------------------------------