Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Clean Architecture #2

Merged
merged 9 commits into from
Sep 2, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,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
87 changes: 84 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.
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 `python ./scripts/rename-project.py` or `python3 ./scripts/rename-project.py` from the
project root, to change the project name. The script will ask for your new project name and update all references.

## Features

Expand All @@ -24,6 +27,84 @@ 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

- _UI_ with SwiftUI
- _Presentation_ with the ViewModel
mbernson marked this conversation as resolved.
Show resolved Hide resolved
- _Domain_ for domain models, UseCases and other domain logic.
- _Data_ for data storage and retrieval.

#### Use cases

- Use cases are single-purpose: GetUserUseCase, but also: GetUserWithArticlesUseCase.
- Use cases can call other use-cases.
- Use cases do not have state, state preferably lives in the data layer.
mbernson marked this conversation as resolved.
Show resolved Hide resolved

#### 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.
mbernson marked this conversation as resolved.
Show resolved Hide resolved

### 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.
mbernson marked this conversation as resolved.
Show resolved Hide resolved

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.
mbernson marked this conversation as resolved.
Show resolved Hide resolved

### 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 small and focused. When a view becomes large, split it up into smaller views.
mbernson marked this conversation as resolved.
Show resolved Hide resolved
* 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.
mbernson marked this conversation as resolved.
Show resolved Hide resolved

### Async code

* Asynchronous code should be written using async/await whenever possible.
* [Combine](https://developer.apple.com/documentation/combine) should only be used when async/await or `AsyncSequence` fall short, and more complexity is needed to solve the problem at hand.
mbernson marked this conversation as resolved.
Show resolved Hide resolved

## 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