Prerequisites
Background: Why a New Architecture is Needed
When developing new apps with SwiftUI, TCA (The Composable Architecture) is likely the first architecture that comes to mind. Having used TCA in production for over a year, I've experienced its benefits in terms of consistent implementation and testability. However, TCA can become a constraint during certain phases of app development. During the initial app launch phase when concepts aren't yet solidified, rapid iteration of creation and destruction is essential, making speed and flexibility more important than robust implementations with comprehensive tests. While TCA is convenient, its strong writing constraints can turn what would be a few minutes of pure Swift implementation into hours of struggle. (I've even reached out to Point-Free about this.) TCA is an excellent architecture when you're on track and requirements don't fundamentally change, but we determined it wasn't suitable for our current needs.
Additionally, while this isn't directly TCA's fault, the current Xcode specifications make it difficult to handle this library properly. The compiler fails to display errors correctly, and we're plagued with numerous warnings about missing test code, resulting in an unfortunately poor development experience. However, TCA's writing style and testability are so appealing that I didn't want to abandon them.
Thus, we needed a pure Swift/SwiftUI architecture that would restore flexible and fast development experience by moving away from TCA while retaining TCA's appeal.
Design Concepts
Characteristic | Requirements |
---|---|
Maintainability | Improve maintainability through clear separation of concerns and consistent patterns |
Testability | Easy dependency injection and component-by-component testing through Unit Tests |
Scalability | Improve extensibility through hierarchical state management and easy addition of new features |
Type Safety | Type-safe Actions and Navigation |
SwiftUI Integration | Ensure compatibility with SwiftUI |
Requirements
- Not unnecessarily complex, suitable for personal development and app launch phases
- Implementable using only Apple's native frameworks and swiftlang/apple OSS libraries
- Xcode 16.4+, Swift 6.1+, iOS 17+, macOS 14+
LUCA
LUCA is a practical architecture optimized for the SwiftUI × Observation era.
L | Layered | Clear 3-layer separation of concerns |
U | Unidirectional Data Flow | Single-direction data flow |
C | Composable | Composability and testability like TCA |
A | Architecture | Architecture |
In other words, it's a design that maintains unidirectional data flow, performs hierarchical state management, and is both extensible and easily testable. Strongly influenced by TCA, it's implemented with a focus on being lightweight and compilable using pure Swift language features while maximizing compatibility with SwiftUI APIs.
As AI puts it:
"Clean layers, Clear flow, Composable design"
Overall Architecture
┌───────────────────┐
│ UserInterface │ ← UI provision and event handling
├───────────────────┤
│ Model │ ← Business logic and state management
├───────────────────┤
│ DataSource │ ← Data access and infrastructure abstraction
└───────────────────┘
Composed of three layers: DataSource
, Model
, and UserInterface
, each layer is provided as a module in a local package (multi-module configuration using Swift Package).
For implementation and maintenance ease, the architecture doesn't require special frameworks in principle (meaning it can be implemented using just Swift language features). However, for testability, dependency injection is made possible using SwiftUI's EnvironmentValues
.
File Structure:
.
├── LocalPackage
│ ├── Package.swift
│ ├── Sources
│ │ ├── DataSource
│ │ │ ├── Dependencies
│ │ │ │ └── AppStateClient.swift
│ │ │ ├── Entities
│ │ │ │ └── AppState.swift
│ │ │ ├── Extensions
│ │ │ ├── Repositories
│ │ │ └── DependencyClient.swift
│ │ ├── Model
│ │ │ ├── Extensions
│ │ │ ├── Services
│ │ │ ├── Stores
│ │ │ ├── AppDelegate.swift (optional)
│ │ │ ├── AppDependencies.swift
│ │ │ └── Composable.swift
│ │ └── UserInterface
│ │ ├── Extensions
│ │ ├── Resources
│ │ ├── Scenes
│ │ └── Views
│ └── Tests
│ └── ModelTests
│ ├── ServiceTests
│ └── StoreTests
├── ProjectName
│ └── ProjectNameApp.swift
└── ProjectName.xcodeproj
Role of Each Layer
UserInterface (formerly Presentation)
Handles UI provision and event handling.
Directory | Roles |
---|---|
Extensions | • Implementation of extensions |
Resources | • Provision of resources like String/Asset Catalog |
Scenes | • Provision of Scenes used in the app |
Views | • Provision of Views used in Scenes |
Image and text resources are placed only here. Therefore, when resources are needed in the Model layer, techniques to avoid direct usage are necessary. For example, the Model layer only handles String.LocalizationValue
, while the UserInterface layer handles the actual resources. Also, when handling images or text from enum
or struct
defined in the DataSource layer, extensions are added to access resources.
Model (formerly Domain)
Handles business logic and state management.
Directory/Special File | Roles |
---|---|
Extensions | • Implementation of extensions |
Services | • Process and manipulate data using Dependencies and Repositories |
Stores | • Implementation of business logic • Preparation of data to display in Views • Handling events and user actions |
AppDelegate.swift (optional) | • Trigger for app lifecycle events |
AppDependencies.swift | • Singleton management of Dependencies and provision of access methods to Views |
Composable.swift | • Protocol for Stores |
Tests involve writing Unit Tests for Services
and Stores
. If LUCA is implemented well, Unit Tests alone can provide substantial coverage of app functionality.
DataSource (formerly DataLayer)
Handles data access and infrastructure abstraction.
Directory/Special File | Roles |
---|---|
Dependencies | • Indirectly provide APIs containing side effects outside our control • Make them replaceable during dependency injection in Services and Stores |
Entities | • Definition of data types to handle |
Extensions | • Implementation of extensions |
Repositories | • Handle data reading and writing |
AppStateClient.swift | • Special DependencyClient for AppState |
AppState.swift | • Management of state needed throughout the app lifecycle • Provision of Streams for state update propagation |
DependencyClient.swift | • Protocol for Dependencies |
Since Repositories often handle fixed keys, it's good to extend String in Extensions to make keys type-safe.
What are APIs containing side effects outside our control?
FileManager.default.moveItem(at:to:)
UserDefaults.standard.set(_:forKey:)
NSApplication.shared.terminate(_:)
NSWorkspace.shared.open(_:)
These are APIs like file I/O or calls to other processes. We consider the actual behavior guarantee to be outside the app's jurisdiction. Therefore, third-party library APIs may also fall under this criterion depending on the case.
Implementation Examples
Specific Implementation Methods
I'll introduce LUCA's implementation methods using a simple BMI calculation app as an example.
Configure Local Package with Swift Package Manager
It's convenient to save the template as a snippet.
Package.swift
// swift-tools-version: 6.1
import PackageDescription
let swiftSettings: [SwiftSetting] = [
.enableUpcomingFeature("ExistentialAny"),
]
let package = Package(
name: "LocalPackage",
defaultLocalization: "en",
platforms: [
.iOS(.v18),
],
products: [
.library(
name: "DataSource",
targets: ["DataSource"]
),
.library(
name: "Model",
targets: ["Model"]
),
.library(
name: "UserInterface",
targets: ["UserInterface"]
),
],
dependencies: [],
targets: [
.target(
name: "DataSource",
swiftSettings: swiftSettings
),
.target(
name: "Model",
dependencies: [
"DataSource",
],
swiftSettings: swiftSettings
),
.target(
name: "UserInterface",
dependencies: [
"Model",
],
resources: [.process("Resources")],
swiftSettings: swiftSettings
),
.testTarget(
name: "ModelTests",
dependencies: [
"Model",
],
swiftSettings: swiftSettings
),
]
)
DataSource Layer Implementation
1. General Entity Implementation
Define data structures using struct
or enum
. Make them conform to Identifiable, Hashable, Codable, etc., as needed.
LocalPackage/Sources/DataSource/Entities/Person.swift
import Foundation
public struct Person: Codable, Sendable, Equatable {
public var name: String
public var weight: Double // kg
public var height: Double // cm
public init(name: String, weight: Double, height: Double) {
self.name = name
self.weight = weight
self.height = height
}
public static let empty = Person(name: "", weight: .zero, height: .zero)
}
Coding Rules
- Don't write business logic in Entities
2. DependencyClient.swift Implementation
Define the protocol that all Dependencies should conform to. Also define convenience functions for easy test implementation.
LocalPackage/Sources/DataSource/DependencyClient.swift
public protocol DependencyClient: Sendable {
static var liveValue: Self { get }
static var testValue: Self { get }
}
public func testDependency<D: DependencyClient>(of type: D.Type, injection: (inout D) -> Void) -> D {
var dependencyClient = type.testValue
injection(&dependencyClient)
return dependencyClient
}
3. General Dependency Implementation
Abstract access to APIs containing side effects outside our control and make them mockable during testing.
LocalPackage/Sources/DataSource/Dependencies/UserDefaultsClient.swift
import Foundation
public struct UserDefaultsClient: DependencyClient {
var data: @Sendable (String) -> Data?
var setData: @Sendable (Data?, String) -> Void
public static let liveValue = Self(
data: { UserDefaults.standard.data(forKey: $0) },
setData: { UserDefaults.standard.set($0, forKey: $1) }
)
public static let testValue = Self(
data: { _ in nil },
setData: { _, _ in }
)
}
Coding Rules
- In principle, provide the original API interface as-is
- Don't change property names or function names unnecessarily
- Don't reduce arguments using default values just because they're unnecessary
- When the original has properties or functions on instances, receive the instance as the first argument
public struct DataClient: DependencyClient {
public var write: @Sendable (Data, URL) throws -> Void
public static let liveValue = Self(
write: { try $0.write(to: $1) }
)
public static let testValue = Self(
write: { _, _ in }
)
}
4. General Repository Implementation
Encapsulate direct data reading and writing in Repositories. Only perform data I/O through Repositories. This is what contributes to testability.
LocalPackage/Sources/DataSource/Repositories/UserDefaultsRepository.swift
import Foundation
public struct UserDefaultsRepository: Sendable {
private var userDefaultsClient: UserDefaultsClient
public var person: Person {
get {
guard let data = userDefaultsClient.data(.person) else { return Person.empty }
return (try? JSONDecoder().decode(Person.self, from: data)) ?? Person.empty
}
nonmutating set {
let data = try? JSONEncoder().encode(newValue)
userDefaultsClient.setData(data, .person)
}
}
public init(_ userDefaultsClient: UserDefaultsClient) {
self.userDefaultsClient = userDefaultsClient
}
}
Make string keys type-safe when handling them. (Of course, methods other than String extensions are also fine.)
LocalPackage/Sources/DataSource/Extensions/String+Extension.swift
extension String {
static let person = "person"
}
Coding Rules
- Pass necessary Dependencies as
init
arguments
5. AppState.swift Implementation
Implement AppState, a special Entity that manages state shared across the entire app.
LocalPackage/Sources/DataSource/Entities/AppState.swift
import Combine
public struct AppState: Sendable {
public var hasAlreadyTutorial: Bool = false
}
6. AppStateClient.swift Implementation
Implement AppStateClient, a special Dependency that provides safe access methods to AppState.
LocalPackage/Sources/DataSource/Dependencies/AppStateClient.swift
import os
public struct AppStateClient: DependencyClient {
var getAppState: @Sendable () -> AppState
var setAppState: @Sendable (AppState) -> Void
public func withLock<R: Sendable>(_ body: @Sendable (inout AppState) throws -> R) rethrows -> R {
var state = getAppState()
let result = try body(&state)
setAppState(state)
return result
}
public static let liveValue: Self = {
let state = OSAllocatedUnfairLock<AppState>(initialState: .init())
return Self(
getAppState: { state.withLock(\.self) },
setAppState: { value in state.withLock { $0 = value } }
)
}()
public static let testValue = Self(
getAppState: { .init() },
setAppState: { _ in }
)
}
Model Layer Implementation
1. AppDependencies.swift Implementation
Implement AppDependencies that aggregates all dependencies and provides them as environment variables.
LocalPackage/Sources/Model/AppDependencies.swift
import DataSource
import SwiftUI
public struct AppDependencies: Sendable {
public var appStateClient = AppStateClient.liveValue
public var userDefaultsClient = UserDefaultsClient.liveValue
// Implement when AppDelegate is needed
static let shared = AppDependencies()
}
extension EnvironmentValues {
@Entry public var appDependencies = AppDependencies.shared
}
// Define convenience functions to make test implementation easier
extension AppDependencies {
public static func testDependencies(
appStateClient: AppStateClient = .testValue,
userDefaultsClient: UserDefaultsClient = .testValue
) -> AppDependencies {
AppDependencies(
appStateClient: appStateClient,
userDefaultsClient: userDefaultsClient
)
}
}
2. AppDelegate.swift Implementation (Optional)
Implement when app lifecycle events are needed.
LocalPackage/Sources/Model/AppDelegate.swift
import DataSource
import SwiftUI
@MainActor public final class AppDelegate: NSObject, NSApplicationDelegate {
private let appDependencies = AppDependencies.shared
public func applicationDidFinishLaunching(_ notification: Notification) {
// Do what you want to do here
// Example: log system initialization, setting loading, etc.
}
public func applicationWillTerminate(_ notification: Notification) {
// App termination processing
}
}
3. General Service Implementation
Implement Services that provide business logic. Services themselves don't hold state, so when state is needed, provide it as arguments or obtain it via AppStateClient.
LocalPackage/Sources/Model/Services/BMIService.swift
import DataSource
public struct BMIService {
private let appStateClient: AppStateClient
public init(_ appDependencies: AppDependencies) {
self.appStateClient = appDependencies.appStateClient
}
public func calculateBMI(weight: Double, height: Double) -> Double {
guard height > 0 else { return 0 }
let heightInMeters = height / 100
return (100 * weight / (heightInMeters * heightInMeters)).rounded() / 100
}
}
Coding Rules
- Make
init
argumentsAppDependencies
instead of direct Dependencies - When Dependencies or Repositories are needed, construct them from
AppDependencies
- Services are basically defined as
struct
, and asactor
when necessary
4. Composable.swift Implementation
Define the protocol that all Stores should conform to.
LocalPackage/Sources/Model/Composable.swift
import Observation
@MainActor
public protocol Composable: AnyObject {
associatedtype Action: Sendable
var action: (Action) async -> Void { get }
func reduce(_ action: Action) async
}
public extension Composable {
func reduce(_ action: Action) async {}
func send(_ action: Action) async {
await self.action(action)
await reduce(action)
}
}
5. General Store Implementation
Implement Stores that handle screen state management and event handling.
LocalPackage/Sources/Model/Stores/PersonBMI.swift
import Foundation
import DataSource
import Observation
@MainActor @Observable public final class PersonBMI: Composable {
private let userDefaultsRepository: UserDefaultsRepository
private let bmiService: BMIService
private let appStateClient: AppStateClient
public var person: Person
public var calculatedBMI: Double
public var isPresentedTutorial: Bool
public let action: (Action) async -> Void
public init(
_ appDependencies: AppDependencies,
person: Person = .empty,
calculatedBMI: Double = .zero,
isPresentedTutorial: Bool = false,
action: @escaping (Action) async -> Void
) {
self.userDefaultsRepository = .init(appDependencies.userDefaultsClient)
self.bmiService = .init(appDependencies)
self.appStateClient = appDependencies.appStateClient
self.person = person
self.calculatedBMI = calculatedBMI
self.isPresentedTutorial = isPresentedTutorial
self.action = action
}
public func reduce(_ action: Action) async {
switch action {
case .task:
person = userDefaultsRepository.person
calculatedBMI = bmiService.calculateBMI(weight: person.weight, height: person.height)
isPresentedTutorial = appStateClient.withLock {
if $0.hasAlreadyTutorial {
return false
} else {
$0.hasAlreadyTutorial = true
return true
}
}
case .calculateButtonTapped:
calculatedBMI = bmiService.calculateBMI(weight: person.weight, height: person.height)
case .saveButtonTapped:
userDefaultsRepository.person = person
}
}
public enum Action {
case task
case calculateButtonTapped
case saveButtonTapped
}
}
Coding Rules
- Make all properties passable as
init
arguments (similar tostruct
's memberwise initializer) - When you want to pass default values to properties, receive default arguments in
init
rather than defining them at definition time -
Action
case
naming conventions- Use SwiftUI event names basically as-is
case task case onDisappear case onTapGesture case onChangeSomeValue // For onChange, add what changed to the end for clarity
- For user action-based cases, use unified naming patterns by UI component
-
Button:
〜ButtonTapped
case cancelButtonTapped case createImageButtonTapped case deleteButtonTapped
-
Toggle:
〜ToggleSwitched
case notificationsToggleSwitched(Bool) case darkModeToggleSwitched(Bool)
-
Picker:
〜PickerSelected
case themePickerSelected(Theme) case languagePickerSelected(Language)
- For cases involving communication, use naming like
~Response
and receiveResult
case submitRecordResponse(Result<String, any Error>) case fetchImageResponse(Result<UIImage, any Error>)
UserInterface Layer Implementation
1. General View Implementation
Implement as a regular SwiftUI View. Focus on having a corresponding Store and reflecting the data held by that Store.
LocalPackage/Sources/UserInterface/Views/PersonBMIView.swift
import DataSource
import Model
import SwiftUI
struct PersonBMIView: View {
@State var store: PersonBMI
var body: some View {
Form {
Section {
LabeledContent("Name") {
TextField("Enter name", text: $store.person.name)
.textFieldStyle(.roundedBorder)
}
LabeledContent("Weight (kg)") {
TextField("Weight", value: $store.person.weight, format: .number)
.textFieldStyle(.roundedBorder)
}
LabeledContent("Height (cm)") {
TextField("Height", value: $store.person.height, format: .number)
.textFieldStyle(.roundedBorder)
}
}
Section {
LabeledContent("BMI") {
Text(String(format: "%.1f", store.calculatedBMI))
}
Button("Calculate BMI") {
Task {
await store.send(.calculateButtonTapped)
}
}
.buttonStyle(.borderedProminent)
Button("Save") {
Task {
await store.send(.saveButtonTapped)
}
}
.buttonStyle(.bordered)
}
}
.task {
await store.send(.task)
}
.alert("Tutorial", isPresented: $store.isPresentedTutorial) {
Button("OK") {}
}
}
}
#Preview {
PersonBMIView(store: .init(.testDependencies()))
}
Coding Rules
- Define Store as
store
- Also name it
store
when getting it in ForEach, etc.
- Also name it
- Use
store.send(Action)
to communicate events to Store- Use Unstructured Task in Button, etc., since
await
is required
- Use Unstructured Task in Button, etc., since
- Write Preview macro so Xcode Preview works
2. General Scene Implementation
Define a Scene that displays the defined View. Since Store basically needs AppDependencies
, obtain it via Environment
.
LocalPackage/Sources/UserInterface/Scenes/PersonBMIScene.swift
import Model
import SwiftUI
public struct PersonBMIScene: Scene {
@Environment(\.appDependencies) private var appDependencies
public init() {}
public var body: some Scene {
WindowGroup {
NavigationView {
PersonBMIView(store: .init(appDependencies))
}
}
}
}
App Implementation
Implement the application entry point. In other words, do nothing more than that. Keep it contained within the Local Package.
BMI/BMIApp.swift
import UserInterface
import SwiftUI
@main
struct BMIApp: App {
// Define when AppDelegate is needed
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
PersonBMIScene()
}
}
Test Implementation
In LUCA, you can test the main app functionality by writing unit tests for Services and Stores.
General Service Tests
Stateless Services can be easily tested as pure functions.
LocalPackage/Tests/ModelTests/ServiceTests/BMIServiceTests.swift
import Testing
import DataSource
@testable import Model
@MainActor struct BMIServiceTests {
@Test
func calculateBMI_HeightNotZero_ReturnsValidValue() {
let sut = BMIService(.testDependencies())
let actual = sut.calculateBMI(weight: 70, height: 175)
#expect(actual == 22.86)
}
@Test
func calculateBMI_HeightIsZero_ReturnsZero() {
let sut = BMIService(.testDependencies())
let actual = sut.calculateBMI(weight: 70, height: 0)
#expect(actual == 0)
}
}
General Store Tests
Test Stores by mocking AppDependencies.
LocalPackage/Tests/ModelTests/StoreTests/PersonBMITests.swift
import os
import Testing
@testable import DataSource
@testable import Model
struct PersonTests {
@MainActor @Test
func send_task_SavedDataIsRestored() async {
let sut = PersonBMI(.testDependencies(
userDefaultsClient: testDependency(of: UserDefaultsClient.self) {
$0.data = { _ in
return try! JSONEncoder().encode(Person(name: "Test", weight: 70.0, height: 175.0))
}
}
))
await sut.send(.task)
#expect(sut.person == Person(name: "Test", weight: 70.0, height: 175.0))
}
@MainActor @Test
func send_calculateButtonTapped_BMIIsCalculated() async {
let sut = PersonBMI(
.testDependencies(),
person: Person(name: "Test", weight: 70.0, height: 175.0)
)
await sut.send(.calculateButtonTapped)
#expect(sut.calculatedBMI == 22.86)
}
@MainActor @Test
func send_saveButtonTapped_DataIsSaved() async {
var savedData = OSAllocatedUnfairLock<Data?>(initialState: nil)
let sut = PersonBMI(
.testDependencies(
userDefaultsClient: testDependency(of: UserDefaultsClient.self) {
$0.setData = { data, _ in
savedData.withLock { $0 = data }
}
}
),
person: Person(name: "Test", weight: 70.0, height: 175.0)
)
await sut.send(.saveButtonTapped)
#expect(savedData.withLock(\.self) != nil)
}
}
This way, dependency injection allows independent testing of each component, which is a major advantage of LUCA.
Coding Rules
- Test case names should be
send_{ActionName}_{condition(optional)}_{expectedResult}
func send_task_LogIsOutput()
func send_deleteButtonTapped_ImageIsSelected_ImageIsDeleted()
func send_notificationsToggleSwitched_NotificationSettingIsDisabled_NotificationSettingIsEnabled()
func send_themePickerSelected_ThemeIsChanged()
Advanced Implementation
Handling Child Events in Parent
When you want to handle child Store events in parent Store, delegate using Action closures.
Child Store Implementation:
@MainActor @Observable public final class Child: Composable {
public let action: (Action) -> Void
public init(
action: @escaping (Action) -> Void
) {
self.action = action
}
public func reduce(_ action: Action) async {
switch action {
case .closeButtonTapped:
break
}
}
public enum Action {
case closeButtonTapped
}
}
Parent Store Implementation:
@MainActor @Observable public final class Parent: Composable {
public var child: Child?
public let action: (Action) -> Void
public init(
child: Child? = nil,
action: @escaping (Action) async -> Void = { _ in }
) {
self.child = child
self.action = action
}
public func reduce(_ action: Action) async {
switch action {
case .openChildButtonTapped:
child = .init(action: { [weak self] in
self?.send(.child($0))
})
// Handle child's Action
case .child(.closeButtonTapped):
child = nil
case .child:
break
}
}
public enum Action {
case openChildButtonTapped
case child(Child.Action)
}
}
Passing AppDependencies to Child
When child initialization needs AppDependencies
, pass the value from View's EnvironmentValues
through Action
.
Parent Store Implementation:
@MainActor @Observable public final class Parent: Composable {
public var child: Child?
public let action: (Action) -> Void
public init(
child: Child? = nil,
action: @escaping (Action) async -> Void = { _ in }
) {
self.child = child
self.action = action
}
public func reduce(_ action: Action) async {
switch action {
case let .openChildButtonTapped(appDependencies):
child = .init(appDependencies, action: { [weak self] in
self?.send(.child($0))
})
case .child:
break
}
}
public enum Action {
case openChildButtonTapped(AppDependencies) // Receive via View
case child(Child.Action)
}
}
Navigation
Implement type-safe navigation management using NavigationStack.
Store Implementation with Path Definition:
@MainActor @Observable public final class Fruits: Composable {
public var path: [Path]
public var bananas: [Banana]
public let action: (Action) async -> Void
public init(
_ appDependencies: AppDependencies,
path: [Path] = [],
bananas: [Banana] = [],
action: @escaping (Action) async -> Void
) { /* omitted */ }
public func reduce(_ action: Action) async {
switch action {
case let .appleButtonTapped(appDependencies):
path.append(.apple(.init(appDependencies, action: { [weak self] in
self?.send(.settings($0))
})))
case let .bananaButtonTapped(store):
path.append(.banana(store))
case .apple:
break
case .banana:
break
}
}
public enum Action {
case appleButtonTapped(AppDependencies) // Pattern of creating new Store
case bananaButtonTapped(Banana) // Pattern of receiving already created Store
case apple(Apple.Action)
case banana(Banana.Action)
}
public enum Path: Hashable {
case apple(Apple)
case banana(Banana)
public static func ==(lhs: Path, rhs: Path) -> Bool {
lhs.id == rhs.id
}
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
// Destination Store must conform to Identifiable
var id: Int {
switch self {
case let .apple(value):
Int(bitPattern: ObjectIdentifier(value))
case let .banana(value):
Int(bitPattern: ObjectIdentifier(value))
}
}
}
}
View Implementation using NavigationStack and navigationDestination:
struct FruitsView: View {
@Environment(\.appDependencies) private var appDependencies
@State var store: Fruits
var body: some View {
NavigationStack(path: $store.path) {
VStack {
Button("Apple") {
Task {
await store.send(.appleButtonTapped(appDependencies))
}
}
ForEach(store.bananas) { store in
Button("Banana: \(store.id)") {
Task {
await store.send(.bananaButtonTapped(store))
}
}
}
}
.navigationDestination(for: Fruits.Path.self) { path in
switch path {
case let .apple(store):
AppleView(store: store)
case let .banana(store):
BananaView(store: store)
}
}
}
}
}
Summary
LUCA is a practical architecture optimized for the SwiftUI × Observation era. It uses only Apple's native frameworks and achieves a lightweight design suitable for personal development.
LUCA Characteristics
Characteristic | Description |
---|---|
Maintainability | Improved maintainability through clear separation of concerns Clear role of each layer, limiting scope of change impact |
Testability | Easy Unit Tests through dependency injection Comprehensive app functionality coverage through Service and Store tests |
Scalability | Ensured extensibility through hierarchical state management Minimize impact on existing code when adding new features |
Type Safety | Type-safe state management through Action-centered unified event processing |
Consistency | Improved development efficiency through consistent patterns Maintain code uniformity even in team development |
SwiftUI Integration | Maximize compatibility with SwiftUI APIs and leverage framework characteristics |
Action-centered unified event processing, stateless Services, and centralized state management via AppStateClient achieve improved traceability and clarified data flow.
I hope this architecture can contribute to solving challenges in SwiftUI app development and help achieve better application development.
Top comments (0)