Skip to content

Releases: 0xLeif/AppState

1.3.0

07 Dec 23:46
b1639ff
Compare
Choose a tag to compare

What's Changed

Full Changelog: 1.2.0...1.3.0

1.2.0

07 Dec 00:42
2f2b645
Compare
Choose a tag to compare

Great news! I’ve lowered the platform versions needed to use AppState. Now, you only need iOS 15, tvOS 15, macOS 11, and watchOS 8. Special note for watchOS users: You'll need watchOS 9 to use the SyncState feature.

What's Changed

Full Changelog: 1.1.1...1.2.0

1.1.1

30 Nov 00:50
a75d910
Compare
Choose a tag to compare

GitHub DocC Documentation fix

What's Changed

Full Changelog: 1.1.0...1.1.1

1.1.0

30 Nov 00:10
3c5ed85
Compare
Choose a tag to compare

SyncState: Real-Time State Synchronization

SyncState offers real-time synchronization of application state across multiple devices using Apple's NSUbiquitousKeyValueStore. This allows for a consistent application state across various devices in your ecosystem. If your application operates on multiple platforms, SyncState ensures that all instances share the same state in real-time.

NSUbiquitousKeyValueStore provides a lightweight, quick setup solution to store small amounts of data that are available ubiquitously across a user's multiple devices. The data is stored in iCloud and automatically syncs to all devices signed in to the same iCloud account, making it an ideal solution for synchronizing application state.

For more information on synchronizing app preferences with iCloud, you can refer to Apple's official documentation.

What's Changed

Full Changelog: 1.0.0...1.1.0

1.0.0

29 Nov 01:55
a77a3e6
Compare
Choose a tag to compare

AppState

AppState is a Swift Package that simplifies the management of application state in a thread-safe, type-safe, and SwiftUI-friendly way. Featuring dedicated struct types for managing state, AppState provides easy and coordinated access to this state across your application. Added to this, the package incorporates built-in logging mechanisms to aid debugging and error tracking. The AppState package also boasts a cache-based system to persistently store and retrieve any application-wide data at any given time.

Key Features

  • Application: Centralized class housing all application-wide data, equipped with built-in observability for reactive changes.

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

  • StoredState: Dedicated struct type for encapsulating and broadcasting stored value changes within the app's scope. Values are stored using UserDefaults.

  • 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.

  • StoredState (property wrapper): A property wrapper that stores its values to UserDefaults. Works the same as AppState otherwise.

  • 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

  • tvOS 16.0 or later

Getting Started

To add AppState to your Swift project, use the Swift Package Manager. This involves adding a package dependency to your Package.swift file.

dependencies: [
    .package(url: "https://github.com/0xLeif/AppState.git", from: "1.0.0")
]

For App projects, open your project in Xcode and navigate to File > Swift Packages > Add Package Dependency... and enter https://github.com/0xLeif/AppState.git.

Usage

AppState is designed to uphold application state management ease and intuitiveness. Here's how to use it:

Define Application State

Defining an application-wide state requires extending the Application and declaring the variables that retain the state. Each state corresponds to an instance of the generic Application.State struct:

extension Application {
    var isLoading: State<Bool> {
        state(initial: false)
    }

    var username: State<String> {
        state(initial: "Leif")
    }

    var colors: State<[String: CGColor]> {
        state(initial: ["primary": CGColor(red: 1, green: 0, blue: 1, alpha: 1)])
    }
}

Read and Write Application States

Once you define the state, it is straightforward to read and write it within your application:

var appState: Application.State = Application.state(\.username)

// Read the value
print(appState.value) // Output: "Leif"

// Modify the value
appState.value = "0xL"

print(Application.state(\.username).value) // Output: "0xL"

Using the AppState Property Wrapper

The AppState property wrapper can directly bridge State of an Application to SwiftUI:

struct ContentView: View {
    @AppState(\.username) var username

    var body: some View {
        Button(
            action: { username = "Hello!" }.
            label: { Text("Hello, \(username)!") }
        )
    }
}

You can also use AppState in a SwiftUI ObservableObject:

class UserSettings: ObservableObject {
    @AppState(\.username) var username

    func updateUsername(newUsername: String) {
        username = newUsername
    }
}

struct ContentView: View {
    @ObservedObject private var settings = UserSettings()

    var body: some View {
        VStack {
            Text("User name: \(settings.username)")
            Button("Update Username") {
                settings.updateUsername(newUsername: "NewUserName")
            }
        }
    }
}

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:

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:

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.

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:

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

    func fetchData() { ... }
}

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 Application.override function. This function returns a DependencyOverride, 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:

// 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 `DependencyOverrideen` to be deallocated 
    // or call `networkOverride.cancel()` to revert back to the original service.
}

Promoting the Application

In AppState, you have the ability to promote your custom Application subclass to a shared singleton instance. This can be particularly useful when your Application subclass needs to conform to a delegate protocol such as UIApplicationDelegate.

Here's an example of how to use the promote function:

class CustomApplication: Application, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // ... your custom setup code here ...
        return true
    }
}

Application.promote(to: CustomApplication())

By doing this, your custom Application subclass becomes the shared singleton instance that you can use throughout your application. This allows you to extend the functionalities of the Application class and utilize these extensions wherever you need in your application.

License

AppState is released under the MIT License. See LICENSE for more information.

Communication and Contribution

  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, submit a pull request.

0.6.2

28 Nov 01:31
a77a3e6
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.6.1...0.6.2

0.6.1

28 Nov 01:20
b95deee
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.6.0...0.6.1

0.6.0

28 Nov 00:47
fcfc143
Compare
Choose a tag to compare

This release includes a new feature that allows the promotion of a custom Application subclass to a shared singleton instance. The promote function in AppState can be used to extend the functionalities of the Application class. This is especially useful when the Application subclass needs to conform to a delegate protocol like UIApplicationDelegate. This enhancement provides developers with greater flexibility in managing application state.

What's Changed

Full Changelog: 0.5.0...0.6.0

0.5.0

27 Nov 23:53
1dd917f
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.4.1...0.5.0

0.4.1

26 Nov 23:39
b492ae1
Compare
Choose a tag to compare

What's Changed

Full Changelog: 0.4.0...0.4.1