Skip to content

Commit

Permalink
Leif/0.2.0 (#13)
Browse files Browse the repository at this point in the history
* Update Dependencies

* Updat macOS Action
  • Loading branch information
0xLeif authored Nov 3, 2023
1 parent 2314fb2 commit 8008c52
Show file tree
Hide file tree
Showing 11 changed files with 386 additions and 70 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/macOS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ jobs:
- uses: actions/checkout@v3
- name: Build for release
run: swift build -v -c release
- name: Test
run: swift test -v
4 changes: 0 additions & 4 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

Expand All @@ -12,7 +11,6 @@ let package = Package(
.tvOS(.v16)
],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "AppState",
targets: ["AppState"]
Expand All @@ -22,8 +20,6 @@ let package = Package(
.package(url: "https://github.com/0xLeif/Cache", from: "2.0.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "AppState",
dependencies: [
Expand Down
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ AppState is a Swift Package that simplifies the management of application state

- **State:** Dedicated struct type for encapsulating and broadcasting value changes within the app's scope.

- **Dependency:** Dedicated struct type for encapsulating dependencies within the app's scope.

- **Scope:** Representation of a specific context within an app, defined by a unique name and ID.

- **AppState (property wrapper):** A property wrapper that elegantly bridges `Application.State` with `SwiftUI` for seamless integration.

- **AppDependency (property wrapper):** A property wrapper that simplifies the handling of dependencies throughout your application.

### Requirements

- Swift 5.7 or later

- iOS 16.0 or later
- watchOS 9.0 or later
- macOS 13.0 or later
Expand Down Expand Up @@ -114,6 +119,111 @@ struct ContentView: View {
}
```

### Defining Dependencies

`Dependency` is a feature provided by AppState, allowing you to define shared resources or services in your application.

To define a dependency, you should extend the `Application` object. Here's an example of defining a `networkService` dependency:

```swift
extension Application {
var networkService: Dependency<NetworkServiceType> {
dependency(NetworkService())
}
}
```

In this example, `Dependency<NetworkServiceType>` represents a type safe container for `NetworkService`.

### Reading and Using Dependencies

Once you've defined a dependency, you can access it anywhere in your app:

```swift
let networkService = Application.dependency(\.networkService)
```

This approach allows you to work with dependencies in a type-safe manner, avoiding the need to manually handle errors related to incorrect types.

### AppDependency Property Wrapper

AppState provides the `@AppDependency` property wrapper that simplifies access to dependencies. When you annotate a property with `@AppDependency`, it fetches the appropriate dependency from the `Application` object directly.

```swift
struct ContentView: View {
@AppDependency(\.networkService) var networkService

// Your view code
}
```

In this case, ContentView has access to the networkService dependency and can use it within its code.

### Using Dependency with ObservableObject

When your dependency is an `ObservableObject`, any changes to it will automatically update your SwiftUI views. Make sure your service conforms to the `ObservableObject` protocol. To do this, you should not use the `@AppDependency` property wrapper, but instead use the `@ObservedObject` property wrapper.

Here's an example:

```swift
class DataService: ObservableObject {
@AppState(\.someData) var data: [String]
}

extension Application {
var dataService: Dependency<DataService> {
dependency(DataService())
}
}

struct ContentView: View {
@ObservedObject var dataService = Application.dependency(\.dataService)

var body: some View {
List(dataService.data, id: \.self) { item in
Text(item)
}
.onAppear {
dataService.fetchData()
}
}
}
```

In this example, whenever data in `DataService` changes, SwiftUI automatically updates the `ContentView`.

### Testing with Mock Dependencies

One of the great advantages of using `Dependency` in AppState is the capability to replace dependencies with mock versions during testing. This is incredibly useful for isolating parts of your application for unit testing.

You can replace a dependency by calling the `override(_:_:)` function. This function returns a `DependencyOverrideToken`, you'll want to hold onto this token for as long as you want the mock dependency to be effective. When the token is deallocated, the dependency reverts back to its original condition.

Here's an example:

```swift
// Real network service
extension Application {
var networkService: Dependency<NetworkServiceType> {
dependency(initial: NetworkService())
}
}

// Mock network service
class MockNetworkService: NetworkServiceType {
// Your mock implementation
}

func testNetworkService() {
// Keep hold of the `DependencyOverride` for the duration of your test.
let networkOverride = Application.override(\.networkService, with: MockNetworkService())

let mockNetworkService = Application.dependency(\.networkService)

// Once done, you can allow the `tokDependencyOverrideen` to be deallocated
// or call `override.cancel()` to revert back to the original service.
}
```

## License

AppState is released under the MIT License. See [LICENSE](https://github.com/0xLeif/AppState/blob/main/LICENSE) for more information.
Expand Down
24 changes: 24 additions & 0 deletions Sources/AppState/AppDependency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Combine
import SwiftUI

/// The `@AppDependency` property wrapper is a feature provided by AppState, intended to simplify dependency handling throughout your application. It makes it easy to access, share, and manage dependencies in a neat and Swift idiomatic way.
@propertyWrapper public struct AppDependency<Value> {
/// Path for accessing `Dependency` from Application.
private let keyPath: KeyPath<Application, Application.Dependency<Value>>

/// Represents the current value of the `Dependency`.
public var wrappedValue: Value {
Application.dependency(keyPath)
}

/**
Initializes the AppDependency with a `keyPath` for accessing `Dependency` in Application.

- Parameter keyPath: The `KeyPath` for accessing `Dependency` in Application.
*/
public init(
_ keyPath: KeyPath<Application, Application.Dependency<Value>>
) {
self.keyPath = keyPath
}
}
9 changes: 6 additions & 3 deletions Sources/AppState/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@ import SwiftUI
app.value(keyPath: keyPath).value
}
nonmutating set {
let key = app.value(keyPath: keyPath).scope.key
let scope = app.value(keyPath: keyPath).scope

app.cache.set(
value: newValue,
forKey: key
value: Application.State(
initial: newValue,
scope: scope
),
forKey: scope.key
)
}
}
Expand Down
55 changes: 55 additions & 0 deletions Sources/AppState/Application+Dependency.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
extension Application {
/// `Dependency` struct encapsulates dependencies used throughout the app.
public struct Dependency<Value>: CustomStringConvertible {
/// The dependency value.
let value: Value

/// The scope in which this state exists.
let scope: Scope

/**
Initializes a new dependency within a given scope with an initial value.

A Dependency allows for defining services or shared objects across the app. It is designed to be read-only and can only be changed by re-initializing it, ensuring thread-safety in your app.

- Parameters:
- value: The initial value of the dependency.
- scope: The scope in which the dependency exists.
*/
init(
_ value: Value,
scope: Scope
) {
self.value = value
self.scope = scope
}

public var description: String {
"Dependency<\(Value.self)>(\(value)) (\(scope.key))"
}
}

/// `DependencyOverride` provides a handle to revert a dependency override operation.
public class DependencyOverride {
/// Closure to be invoked when the dependency override is cancelled. This closure typically contains logic to revert the overrides on the dependency.
private let cancelOverride: () -> Void

/**
Initializes a `DependencyOverride` instance.

- Parameter cancelOverride: The closure to be invoked when the
dependency override is cancelled.
*/
init(cancelOverride: @escaping () -> Void) {
self.cancelOverride = cancelOverride
}

/// Automatically cancels the override when `DependencyOverride` instance is deallocated.
deinit { cancel() }

/// Cancels the override and resets the Dependency back to its value before the override.
public func cancel() {
cancelOverride()
}
}
}
17 changes: 12 additions & 5 deletions Sources/AppState/Application+State.swift
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
extension Application {
/// `State` encapsulates the value within the application's scope and allows any changes to be propagated throughout the scoped area.
public struct State<Value> {
public struct State<Value>: CustomStringConvertible {
/// A private backing storage for the value.
private var _value: Value

/// The current state value.
public var value: Value {
get {
guard
let value = shared.cache.get(
let state = shared.cache.get(
scope.key,
as: Value.self
as: State<Value>.self
)
else { return _value }

return value
return state._value
}
set {
_value = newValue
shared.cache.set(
value: newValue,
value: Application.State(
initial: newValue,
scope: scope
),
forKey: scope.key
)
}
Expand All @@ -42,5 +45,9 @@ extension Application {
self._value = value
self.scope = scope
}

public var description: String {
"State<\(Value.self)>(\(value)) (\(scope.key))"
}
}
}
18 changes: 0 additions & 18 deletions Sources/AppState/Application+internal.swift

This file was deleted.

Loading

0 comments on commit 8008c52

Please sign in to comment.