Skip to content

Commit

Permalink
Merge pull request #2 from Q42/feature/clean-architecture
Browse files Browse the repository at this point in the history
Implement Clean Architecture
  • Loading branch information
mbernson authored Sep 2, 2024
2 parents e0c5999 + 6e0ed20 commit 71e3341
Show file tree
Hide file tree
Showing 28 changed files with 864 additions and 67 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Select Xcode version
run: sudo xcode-select --switch /Applications/Xcode_15.3.app
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Cache Swift Package Manager dependencies
uses: actions/cache@v4
Expand Down
25 changes: 18 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Select Xcode version
run: sudo xcode-select --switch /Applications/Xcode_15.3.app
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Cache Swift Package Manager dependencies
uses: actions/cache@v4
Expand All @@ -38,13 +39,23 @@ jobs:
-resolvePackageDependencies \
-clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages"
- name: Run tests
env:
PLATFORM: ${{ 'iOS Simulator' }}
- name: Determine test device
run: |
echo "PLATFORM=iOS Simulator" >> "$GITHUB_ENV"
# xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
DEVICE=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
xcodebuild test -project "${{ env.XCODE_PROJECT }}" \
echo "DEVICE=$(xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//")" >> "$GITHUB_ENV"
- name: Build for testing
run: |
xcodebuild build-for-testing -project "${{ env.XCODE_PROJECT }}" \
-scheme "${{ env.SCHEME }}" \
-destination "platform=$PLATFORM,name=$DEVICE" \
-clonedSourcePackagesDirPath "${{ runner.temp }}/SourcePackages" \
-disableAutomaticPackageResolution
- name: Run tests
run: |
xcodebuild test-without-building -project "${{ env.XCODE_PROJECT }}" \
-scheme "${{ env.SCHEME }}" \
-testPlan "${{ env.TEST_PLAN }}" \
-destination "platform=$PLATFORM,name=$DEVICE" \
Expand Down
86 changes: 83 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

This is a template for creating iOS projects at Q42. It has opinionated defaults and boilerplate, based on how we do iOS projects at Q42.

## How to use it
## Using this template

1. In GitHub, press "use this template" to create a new repository.
2. Rename your project using the included Python script: `python3 scripts/rename-project.py`. You can delete the script afterwards.
1. Click the green "Use this template" button in the Github repo to create your own repository with
this code template.
1. Clone the new repo locally on your machine.
1. Run `python3 ./scripts/rename-project.py` from the
project root, to change the project name. The script will ask for your new project name. You can delete the script afterwards.

## Features

Expand All @@ -24,6 +27,83 @@ The Xcode project is configured to use 4 spaces for indentation.
For linting Swift source code, we use [SwiftLint](https://github.com/realm/SwiftLint).
A configuration for [SwiftFormat](http://github.com/nicklockwood/SwiftFormat) is also included.

## App architecture

### Core principles

This app is built using SwiftUI and targets iOS 15 and higher. We use SwiftUI as much as possible, but fall back to UIKit views using view(controller) representables where needed.

We try to stick to the Apple conventions and write idiomatic SwiftUI code. Do things the Apple way. Lean in to the platform instead of fighting it.

Keep it simple. Less is more.

### Architecture patterns

We use the [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) pattern, combined with dependency injection.

#### Clean Architecture layers

- *Features* with SwiftUI views and view models
- *Domain* for domain models, UseCases and other domain logic.
- *Data* for persistent data storage and retrieval using Repositories.

#### Use cases

- Use cases are single-purpose: GetUserUseCase, but also: GetUserWithArticlesUseCase.
- Use cases can call other use-cases.
- Use cases do not have persistent state. They are instantiated, called to perform their function once or multiple times, and are then discarded.

#### Dependency injection

Dependency injection (DI) means that objects don't instantiate the objects or configurations that they require themselves, but they are passed in from the outside.
This is useful, because it makes code easier to test and easier to change. It promotes good separation of concerns.

We use [Factory](https://github.com/hmlongco/Factory) as a DI container.

### Modules & libraries

* Preferably use the Swift Package Manager for dependencies. Use other package managers only if there's no other option.
* Only extract code into a package if there are strong reasons to do so. For example:
* It is used from at least two different targets/packages, or is a candidate to be extracted to an open-source package.
* It is completely self-contained.

When choosing a third-party library, prefer libraries that:

* Are written in idiomatic Swift or Objective-C that sticks to best practices.
* Have as few dependencies of its own as possible. Preferably none.
* Aren't too big, in order to keep compile times and bloat in check.

### Testing

* For business logic, we write unit tests.
* For testing the user interface, we write UI tests in a behaviour-driven way using the [Salad](https://github.com/Q42/Salad) library.
* Tests are run on CI (GitHub Actions). Tests must pass before a PR may be merged and before any sort of build is created.

### Views

* Keep views focused (single-responsibility principle from SOLID). When a view becomes large a, split it up into smaller views.
* Every view gets a UI preview if at all possible. The preview should show the view in different states using dummy data.
* We use [custom SF Symbols](https://developer.apple.com/documentation/uikit/uiimage/creating_custom_symbol_images_for_your_app/) whenever a custom icon is needed, so that they render in a consistent manner.

### Accessibility

* Every new component or control should be audited for basic accessibility support:
* Dynamic type size support
* VoiceOver support
* Also consider:
* Bold text support
* High contrast support
* Use `accessibilityRepresentation` on custom controls to make them accessible.

### Localization

String catalogs are used to localize the project. The default languages supported are English and Dutch.

### Async code

* `async`/`await` is preferred over Combine/Promises/etc. to leverage the compiler concurrency checking.
* [Combine](https://developer.apple.com/documentation/combine) can be used when `async`/`await` or `AsyncSequence` fall short, and more complexity is needed to solve the problem at hand.

## Continuous integration

GitHub Actions is used for continuous integration (CI). The CI runs the automated tests when you make a pull request.
Expand Down
Loading

0 comments on commit 71e3341

Please sign in to comment.