English | 日本語
The Framework-Independent Architecture (FIA) is a newly proposed architecture for Swift application development. This architecture adopts a multi-module, multi-project structure using the Swift Package Manager and is based on the design principles of the Clean Architecture.
The main goal of FIA is to reduce Xcode build time while enjoying the benefits of independence and testability that the Clean Architecture provides.
This architecture uses dependency injection at the application entry point to provide a Clean Area not only at the Presentation Layer, but also at the Application Layer.
This design enables fast application builds independent of the Framework Layer, aiming to reduce build time significantly.
The diagram below shows the overall architecture of the FIA and the direction of dependencies. It visually represents a Clean Area extended to the Application Layer for development.
The circular architecture in the figure below also illustrates the inward-looking nature of the dependencies in FIA, maintaining clear boundaries and independence among the layers.
The "Framework" in the Framework-Independent Architecture (FIA) refers to the "Framework Layer" in the figure and corresponds to the outermost layer of the Clean Architecture. This layer is also called the Data Access Layer or Infrastructure Layer and depends on details such as external frameworks, databases, network communications, etc. In FIA, this layer is positioned as a Non-Clean Area. FIA positions this layer as a Non-Clean Area and adopts a structure where the development Application Layer is kept clean, while only the production Application Layer depends on the Non-Clean Area. This approach aims to reduce application build time during development.
With the advent of Swift Package Manager, applications can now be easily adapted to multi-module and multi-project configurations, as seen in isowords.
While module partitioning in the Swift Package Manager is usually done on a feature-by-feature basis, FIA uses the Clean Architecture design principles to partition modules by layer.
Also, through the description of dependencies in Package.swift, Swift Package Manager can easily manage the dependency direction between modules. In this respect, Swift Package Manager is a good match for the Clean Architecture, which emphasizes the directionality of dependencies, making Swift Package Manager suitable for implementing the Clean Architecture.
In addition, FIA allows the selection of the appropriate DI Container by injecting dependencies at the entry point of the application. This allows you to take advantage of multi-project configurations to set up a development project using a Mock DI Container that is independent of the Framework Layer, and a production project using the actual DI Container that depends on the Framework Layer.
Development projects can significantly reduce application build times by not relying on external libraries with long build times, such as the Firebase SDK as an example. This effect also contributes to the build speed of Xcode Previews.
The following links are repositories containing sample code that apply FIA concepts to real projects. These examples will help you better understand and apply FIA design principles to your own projects.
- framework-independent-architecture/FIASample (this repository)
- FIA Practical Sample
Note
We are looking for more sample code that employs the FIA architecture. If you have adopted FIA in your own project, please share the repository link. Shared projects will be featured in this section.
The detailed architecture of the FIA implementation is shown in the figure below.
The configuration shown in this figure is only an example and can be customized according to the requirements of your project. Also, the actual code we are about to show you is a partially modified version of the code in this repository, but the basic structure is the same.
The following is a demonstration of an application created by the sample code presented in this chapter. This application provides a simple View that displays license information.
Package.swift
let package = Package(
// ... omitted ...
dependencies: [
// sample third party library
.package(url: "https://github.com/maiyama18/LicensesPlugin", from: "0.1.6"),
],
targets: [
.target(
name: "DependencyInjectionLayer",
dependencies: ["FrameworkLayer", "PresentationLayer"]
),
.target(
name: "DomainLayer"
),
.target(
name: "FrameworkLayer",
dependencies: ["DomainLayer"],
plugins: [.plugin(name: "LicensesPlugin", package: "LicensesPlugin")]
),
.target(
name: "PresentationLayer",
dependencies: ["DomainLayer"]
)
]
)graph TD;
DependencyInjectionLayer-->FrameworkLayer;
DependencyInjectionLayer-->PresentationLayer;
PresentationLayer-->DomainLayer;
FrameworkLayer-->DomainLayer;
FrameworkLayer-->LicensesPlugin;
Domain Layer
public struct License: Identifiable, Equatable {
public let id: String
public let name: String
public let body: String
public init(id: String, name: String, body: String) {
self.id = id
self.name = name
self.body = body
}
}public protocol LicenseDriverProtocol {
func getLicenses() -> [License]
}Presentation Layer
public struct LicenseListView<Dependency: DIContainerDependency>: View {
private let dependency: Dependency
@State private var presenter: LicenseListPresenter<Dependency>
public init(dependency: Dependency) {
self.dependency = dependency
presenter = LicenseListPresenter(dependency: dependency)
}
public var body: some View {
List {
ForEach(presenter.licenses) { license in
Button {
presenter.onTapLicense(license)
} label: {
Text(license.name)
}
}
}
.navigationTitle("Licenses")
.sheet(item: $presenter.selectedLicense, content: { license in
NavigationStack {
ScrollView {
Text(license.body).padding()
}
.navigationTitle(license.name)
}
})
.onAppear {
presenter.onAppear()
}
}
}public protocol LicenseListPresenterDependency {
associatedtype LicenseDriverProtocolAssocType: LicenseDriverProtocol
var licenseDriver: LicenseDriverProtocolAssocType { get }
}@Observable
final class LicenseListPresenter<Dependency: LicenseListPresenterDependency> {
private(set) var licenses: [License] = []
var selectedLicense: License?
private let dependency: Dependency
init(dependency: dependency) {
self.dependency = dependency
}
func onAppear() {
licenses = dependency.licenseDriver.getLicenses()
}
func onTapLicense(_ license: License) {
selectedLicense = license
}
}public protocol DIContainerDependency: LicenseListPresenterDependency {}public final class MockDIContainer<LicenseDriver: LicenseDriverProtocol>: DIContainerDependency {
public let licenseDriver: LicenseDriver
public init(licenseDriver: LicenseDriver = MockLicenseDriver(getLicenses: [
License(id: UUID().uuidString, name: "Sample License 1", body: "Sample License Body 1"),
License(id: UUID().uuidString, name: "Sample License 2", body: "Sample License Body 2"),
License(id: UUID().uuidString, name: "Sample License 3", body: "Sample License Body 3"),
])) {
self.licenseDriver = licenseDriver
}
}
public final class MockLicenseDriver: LicenseDriverProtocol {
private let _getLicenses: [License]
public init(getLicenses: [License] = []) {
self._getLicenses = getLicenses
}
public func getLicenses() -> [License] {
return _getLicenses
}
}※ UseCase, Interactor is used to organize complex processing in Presenter. Since UseCase, Interactor is not used in this case, please refer to the more practical sample project here.
Framework Layer
public class LicenseDriver: LicenseDriverProtocol {
public init() {}
public func getLicenses() -> [DomainLayer.License] {
LicensesPlugin.licenses.map { library in
License(from: library)
}
}
}
extension DomainLayer.License {
// Convert Framework Entity to Domain Entity
init(from licensesPluginLicense: LicensesPlugin.License) {
self.init(id: licensesPluginLicense.id, name: licensesPluginLicense.name, body: licensesPluginLicense.licenseText ?? "")
}
}DI Layer
public final class DIContainer<LicenseDriver: LicenseDriverProtocol>: DIContainerDependency {
public let licenseDriver: LicenseDriver
public init(licenseDriver: LicenseDriver = FrameworkLayer.LicenseDriver()) {
self.licenseDriver = licenseDriver
}
}Application Layer (Entry Point)
@main
struct DevelopmentApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
// Mock DI Container
LicenseListView(dependency: MockDIContainer())
}
}
}
}@main
struct ProductionApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
// Actual DI Container
LicenseListView(dependency: DIContainer())
}
}
}
}FIA is based on the Clean Architecture. This architecture allows for the injection of third-party libraries and dependencies that replace communication with external APIs with mocks. This allows each layer to write independent test code.
The following table shows the types of tests that can be performed with FIA and the scope covered by each test.
| Test Type | DI Container | Test Target: | ||||
|---|---|---|---|---|---|---|
| View Interaction |
View Variation |
Presenter | Interactor | Driver | ||
| UI Test (XCUITest) | Actual / Mock | ◎ | ◯ | ◯ | ◯ | ◯ / - |
| Xcode Previews | Mock | - | ◎ | ◯ | ◯ | - |
| Presenter UT | Actual / Mock | - | - | ◎ | ◯ | ◯ / - |
| Interactor UT | Actual / Mock | - | - | - | ◎ | ◯ / - |
| Driver UT | Actual | - | - | - | - | ◎ |
※ ◎ : Object to be tested, ◯ : Object to be tested incidentally
By referring to this table, the scope of test objects that can be covered by each test execution becomes clear, helping to improve the quality of the test code.
FIA is based on Clean Architecture design principles and offers the advantages of independence, testability, maintainability, reusability, and extensibility. Particular emphasis is placed on reducing build time, which is a major advantage of FIA.
On the other hand, there are some disadvantages of adopting Clean Architecture, such as increased implementation complexity, higher learning cost, and risk of overengineering.
Technical challenges that may be encountered in the process of implementing FIA are summarized below:
- Type complexity:
- When using
protocol, the use ofsomeinstead ofanyrequires type resolution, which increases the complexity of the code.
- When using
- Increase boiler code:
- A lot of boilerplate code is needed to implement the architecture, even for a single simple View.
- Prepare Mock DI Container:
- Mock DI Container must be modified each time a dependency changes. This is a frequent and time-consuming task.
- View testing constraints:
- View testing can be done by running tests in XCUITest or visually in Xcode Previews.
- XCUITest has a long execution time and is less maintainable when multiple test cases are created or modified.
- Xcode Previews does not provide snapshot testing as a standard feature, so visual verification is required and problems are not automatically detected.
- View testing can be done by running tests in XCUITest or visually in Xcode Previews.
To address these issues, you can use Sourcery, Mockolo, PreviewSnapshots. For concrete examples of implementations employing these libraries, please refer to this FIA sample repository.
See the LICENSE file for license rights and limitations (MIT).
Japanese Speaker Deck slides that supplement the FIA are presented below. If you are interested, please refer to them.
- 【Swift】【クリーンアーキテクチャ】Clean Architecture で iOS アプリを爆速でビルドする方法 Framework-Independent Architecture (FIA)
For questions or collaboration, please contact us at
or feel free to contact us at Issue on GitHub.
