• Что бы вступить в ряды "Принятый кодер" Вам нужно:
    Написать 10 полезных сообщений или тем и Получить 10 симпатий.
    Для того кто не хочет терять время,может пожертвовать средства для поддержки сервеса, и вступить в ряды VIP на месяц, дополнительная информация в лс.

  • Пользаватели которые будут спамить, уходят в бан без предупреждения. Спам сообщения определяется администрацией и модератором.

  • Гость, Что бы Вы хотели увидеть на нашем Форуме? Изложить свои идеи и пожелания по улучшению форума Вы можете поделиться с нами здесь. ----> Перейдите сюда
  • Все пользователи не прошедшие проверку электронной почты будут заблокированы. Все вопросы с разблокировкой обращайтесь по адресу электронной почте : info@guardianelinks.com . Не пришло сообщение о проверке или о сбросе также сообщите нам.

LUCA: A Modern Architecture for SwiftUI Development

Sascha Оффлайн

Sascha

Заместитель Администратора
Команда форума
Администратор
Регистрация
9 Май 2015
Сообщения
1,477
Баллы
155

Background: Why a New Architecture is Needed


When developing new apps with SwiftUI,

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

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

CharacteristicRequirements
MaintainabilityImprove maintainability through clear separation of concerns and consistent patterns
TestabilityEasy dependency injection and component-by-component testing through Unit Tests
ScalabilityImprove extensibility through hierarchical state management and easy addition of new features
Type SafetyType-safe Actions and Navigation
SwiftUI IntegrationEnsure 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 is a practical architecture optimized for the SwiftUI × Observation era.

LLayeredClear 3-layer separation of concerns
UUnidirectional Data FlowSingle-direction data flow
CComposableComposability and testability like TCA
AArchitectureArchitecture

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.

DirectoryRoles
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 FileRoles
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 FileRoles
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 arguments AppDependencies instead of direct Dependencies
  • When Dependencies or Repositories are needed, construct them from AppDependencies
  • Services are basically defined as struct, and as actor 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 to struct'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 receive Result

    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.
  • Use store.send(Action) to communicate events to Store
    • Use Unstructured Task in Button, etc., since await is required
  • 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)
}
}
}
}
}




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

CharacteristicDescription
MaintainabilityImproved maintainability through clear separation of concerns
Clear role of each layer, limiting scope of change impact
TestabilityEasy Unit Tests through dependency injection
Comprehensive app functionality coverage through Service and Store tests
ScalabilityEnsured extensibility through hierarchical state management
Minimize impact on existing code when adding new features
Type SafetyType-safe state management through Action-centered unified event processing
ConsistencyImproved development efficiency through consistent patterns
Maintain code uniformity even in team development
SwiftUI IntegrationMaximize 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.



Источник:

Пожалуйста Авторизируйтесь или Зарегистрируйтесь для просмотра скрытого текста.

 
Вверх Снизу