⚛️ A Reactive Data-Binding and Dependency Injection Library for SwiftUI x Concurrency.

Overview

SwiftUI Atom Properties

A Reactive Data-Binding and Dependency Injection Library
for SwiftUI x Concurrency

📔 API Reference

build release swift platform license



Introduction

Reactive Data Binding Effective Data Caching Compile Safe
Dependency Injection
Piece of app data that can be accessed from anywhere propagates changes reactively. Recompute atom data and views only when truly need, otherwise it caches data until no longer used. Successful compilation guarantees that dependency injection is ready.

SwiftUI Atom Properties offers practical capabilities to manage the complexity of modern apps. It effectively integrates the solution for both data-binding and dependency injection while allowing us to rapidly building an application.

Motivation

SwiftUI offers a simple and understandable data-binding solution with built-in property wrappers, but is a little uneasiness for building middle to large scale production apps. As a typical example, view data can only be shared by pushing it up to a common ancestor.
Software development is not all set in advance; it evolves over time to meet business and customer needs. Therefore, you may need to radically redesign it so that local data used only in one part of the view-tree can be shared elsewhere, as the app grows.
EnvironmentObject was hoped to be a solution to the problem, but it ended up with let us to create a huge state-holder object - Big Ball of Mud being provided from the root of an app, so it could not be an ideal.
Ultimately, pure SwiftUI needs state-drilling from the root to descendants in anyway, which not only makes code-splitting difficult, but also causes gradual performance degradation due to the huge view-tree computation as the app grow up.

This library solves these problems by defining application data as distributed pieces called atom, allowing data to be shared throughout the app as the source of truth. That said, atom itself doesn't have internal state, but rather retrieves the associated state from the context in which they are used, and ensures that the app is testable.
Furthermore, it manages a directed graph of atoms and propagates data changes transitively from upstream to downstream, such that it updates only the views truly need update while preventing expensive data recomputation, resulting in effortlessly high performance and efficient memory use.

This approach guarantees the following principles:

  • Reactively reflects data changes into views.
  • Boilerplate-free interface where shared data has the same simple interface as SwiftUI built-ins.
  • Compatible with any other libraries (e.g. TCA) of your choice if needed.
  • Accelerates code-splitting by distributed & incremental state definition.
  • Ensures testable code over time with capabilities of dependency injection.
  • Provides simplified interfaces for asynchronous process.
  • Swift Concurrency based thread-safety.

Quick Overview

To get a feel for this library, let's first look at the state management for a tiny counter app.

The CounterAtom in the example below represents the shared data of a mutable count value.

struct CounterAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> Int {
        0
    }
}

Bind the atom to the view using @WatchState property wrapper so that it can obtain the value and write new values.

struct CountStepper: View {
    @WatchState(CounterAtom())
    var count

    var body: some View {
        Stepper(value: $count) {}
    }
}

@Watch property wrapper obtains the atom value read-only.
Now that the app can share the state among multiple views without passing it down through initializer.

struct CounterView: View {
    @Watch(CounterAtom())
    var count

    var body: some View {
        VStack {
            Text("Count: \(count)")
            CountStepper()
        }
    }
}

If you like the principles, see the sample apps and the basic tutorial to learn more about this library.


Examples

Counter Todo TMDB Map Voice Memo Time Travel
  • Counter
    Demonstrates the minimum app using this library.
  • Todo
    A simple todo app that has user interactions, showing how multiple atoms interact with each other.
  • The Movie DB
    Demonstrates practical usage which close to a real-world app, using TMDB API for asynchronous networking.
  • Map
    A simple but effective app that demonstrates how to wrap a framework in this library.
  • Voice Memo
    Demonstrates how to manage complex state with multiple dependencies using MVVM pattern on an atom. Created with imitate TCA's example.
  • Time Travel
    A simple demo that demonstrates how to do time travel debugging with this library.

Each example has test target to show how to test your atoms with dependency injection as well.
Open Examples/App.xcodeproj and play around with it!


Getting Started

Requirements

Minimum Version
Swift 5.6
Xcode 13.3
iOS 14.0
macOS 11.0
tvOS 14.0
watchOS 7.0

Installation

The module name of the package is Atoms. Choose one of the instructions below to install and add the following import statement to your source code.

import Atoms

Xcode Package Dependency

From Xcode menu: File > Swift Packages > Add Package Dependency

https://github.com/ra1028/swiftui-atom-properties

Swift Package Manager

In your Package.swift file, first add the following to the package dependencies:

.package(url: "https://github.com/ra1028/swiftui-atom-properties"),

And then, include "Atoms" as a dependency for your target:

.target(name: "<target>", dependencies: [
    .product(name: "Atoms", package: "swiftui-atom-properties"),
]),

Documentation


Basic Tutorial

In this tutorial, we will create a simple todo app as an example. This app will support:

  • Create todo items
  • Edit todo items
  • Filter todo items

Every view that uses atom must have an AtomRoot somewhere in the ancestor. In SwiftUI lifecycle apps, it's recommended to put it right under WindowGroup.

@main
struct TodoApp: App {
    var body: some Scene {
        WindowGroup {
            AtomRoot {
                TodoList()
            }
        }
    }
}

First, define a todo structure and an enum to filter todo list, and declare state with StateAtom that represents a mutable value.

struct Todo {
    var id: UUID
    var text: String
    var isCompleted: Bool
}

enum Filter: CaseIterable, Hashable {
    case all, completed, uncompleted
}

struct TodosAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> [Todo] {
        []
    }
}

struct FilterAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> Filter {
        .all
    }
}

The FilteredTodosAtom below represents the derived data that combines the above two atoms. You can think of derived data as the output of passing values to a pure function that derives a new value from the depending values.

When dependent data changes, the derived data reactively updates, and the output value is cached until it truly needs to be updated, so don't need to worry about low performance due to the filter function being called each time the view recomputes.

struct FilteredTodosAtom: ValueAtom, Hashable {
    func value(context: Context) -> [Todo] {
        let filter = context.watch(FilterAtom())
        let todos = context.watch(TodosAtom())

        switch filter {
        case .all:         return todos
        case .completed:   return todos.filter(\.isCompleted)
        case .uncompleted: return todos.filter { !$0.isCompleted }
        }
    }
}

To create a new todo item, you need to access to a writable value that update the value of TodosAtom we defined previously. We can use @WatchState property wrapper to obtain a read-write access to it.

struct TodoCreator: View {
    @WatchState(TodosAtom())
    var todos

    @State
    var text = ""

    var body: some View {
        HStack {
            TextField("Enter your todo", text: $text)
            Button("Add") {
                todos.append(Todo(id: UUID(), text: text, isCompleted: false))
                text = ""
            }
        }
    }
}

Similarly, build a view to switch the value of FilterAtom. Get a Binding to the value exposed by @WatchState using $ prefix.

struct TodoFilters: View {
    @WatchState(FilterAtom())
    var current

    var body: some View {
        Picker("Filter", selection: $current) {
            ForEach(Filter.allCases, id: \.self) { filter in
                switch filter {
                case .all:         Text("All")
                case .completed:   Text("Completed")
                case .uncompleted: Text("Uncompleted")
                }
            }
        }
        .pickerStyle(.segmented)
    }
}

Next, create a view to display and edit individual todo items.

struct TodoItem: View {
    @WatchState(TodosAtom())
    var allTodos

    @State
    var text: String

    @State
    var isCompleted: Bool

    let todo: Todo

    init(todo: Todo) {
        self.todo = todo
        self._text = State(initialValue: todo.text)
        self._isCompleted = State(initialValue: todo.isCompleted)
    }

    var index: Int {
        allTodos.firstIndex { $0.id == todo.id }!
    }

    var body: some View {
        Toggle(isOn: $isCompleted) {
            TextField("Todo", text: $text) {
                allTodos[index].text = text
            }
        }
        .onChange(of: isCompleted) { isCompleted in
            allTodos[index].isCompleted = isCompleted
        }
    }
}

Use @Watch to obtain the value of FilteredTodosAtom read-only. Updates to any of the dependent atoms are propagated to this view, and it re-render the todo list.
Finally, assemble the views we've created so far and complete.

struct TodoList: View {
    @Watch(FilteredTodosAtom())
    var filteredTodos

    var body: some View {
        List {
            TodoCreator()
            TodoFilters()

            ForEach(filteredTodos, id: \.id) { todo in
                TodoItem(todo: todo)
            }
        }
    }
}

That is the basics for building apps using SwiftUI Atom Properties, but even asynchronous processes and more complex state management can be settled according to the same steps.
See Guides section for more detail. Also, the Examples directory has several projects to explore concrete usage.


Guides

This section introduces the available APIs and their uses.
To look into the APIs in more detail, visit the API referrence.


AtomRoot

Provides the internal store which provides atoms to view-tree through environment values.
It must be the root of any views to manage atoms used throughout the application.

@main
struct ExampleApp: App {
    var body: some Scene {
        WindowGroup {
            AtomRoot {
                ExampleView()
            }
        }
    }
}

Atoms

An atom represents a piece of state and is the source of truth for your app. It can also represent a derived data by combining and transforming one or more other atoms.
Each atom does not actually have a global data inside, and retrieve values from the internal store provided by the AtomRoot. That's why they can be accessed from anywhere, but never lose testability.

An atom and its value are associated using a unique key which is automatically defined if the atom conforms to Hashable, but you can also define it explicitly without Hashable.

struct UserNameAtom: StateAtom {
   let userID: Int

   var key: Int {
       userID
   }

   func defaultValue(context: Context) -> String {
       "Robert"
   }
}

In order to provide the best interface and effective data-binding for the type of the resulting values, there are several variants of atoms as following.

ValueAtom

📖 Click to expand example code
struct LocaleAtom: ValueAtom, Hashable {
    func value(context: Context) -> Locale {
        .current
    }
}

struct LocaleView: View {
    @Watch(LocaleAtom())
    var locale

    var body: some View {
        Text(locale.identifier)
    }
}
Description
Summary Provides a read-only value.
Output T
Use Case Computed property, Derived data, Dependency injection

StateAtom

📖 Click to expand example code
struct CounterAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> Int {
        0
    }

    // Does nothing by default.
    func willSet(newValue: Int, oldValue: Int, context: Context) {
        print("Will change")
    }

    // Does nothing by default.
    func didSet(newValue: Int, oldValue: Int, context: Context) {
        print("Did change")
    }
}

struct CounterView: View {
    @WatchState(CounterAtom())
    var count

    var body: some View {
        Stepper("Count: \(count)", value: $count)
    }
}
Description
Summary Provides a read-write data.
Output T
Use Case Mutable data, Derived data

TaskAtom

📖 Click to expand example code
struct FetchUserAtom: TaskAtom, Hashable {
    func value(context: Context) async -> User? {
        await fetchUser()
    }
}

struct UserView: View {
    @Watch(FetchUserAtom())
    var userTask

    var body: some View {
        Suspense(userTask) { user in
            Text(user?.name ?? "Unknown")
        }
    }
}
Description
Summary Initiates a non-throwing Task from the given async function.
Output Task<T, Never>
Use Case Non-throwing asynchronous operation e.g. Expensive calculation

ThrowingTaskAtom

📖 Click to expand example code
struct FetchMoviesAtom: ThrowingTaskAtom, Hashable {
    func value(context: Context) async throws -> [Movie] {
        try await fetchMovies()
    }
}

struct MoviesView: View {
    @Watch(FetchMoviesAtom())
    var moviesTask

    var body: some View {
        List {
            Suspense(moviesTask) { movies in
                ForEach(movies, id: \.id) { movie in
                    Text(movie.title)
                }
            } catch: { error in
                Text(error.localizedDescription)
            }
        }
    }
}
Description
Summary Initiates a throwing Task from the given async throws function.
Output Task<T, Error>
Use Case Throwing asynchronous operation e.g. API call

AsyncSequenceAtom

📖 Click to expand example code
struct NotificationAtom: AsyncSequenceAtom, Hashable {
    let name: Notification.Name

    func sequence(context: Context) -> NotificationCenter.Notifications {
        NotificationCenter.default.notifications(named: name)
    }
}

struct NotificationView: View {
    @Watch(NotificationAtom(name: UIApplication.didBecomeActiveNotification))
    var notificationPhase

    var body: some View {
        switch notificationPhase {
        case .suspending, .failure:
            Text("Unknown")

        case .success:
            Text("Active")
        }
    }
}
Description
Summary Provides a AsyncPhase value that represents asynchronous, sequential elements of the given AsyncSequence.
Output AsyncPhase<T, Error>
Use Case Handle multiple asynchronous values e.g. web-sockets

PublisherAtom

📖 Click to expand example code
struct TimerAtom: PublisherAtom, Hashable {
    func publisher(context: Context) -> AnyPublisher<Date, Never> {
        Timer.publish(every: 1, on: .main, in: .default)
            .autoconnect()
            .eraseToAnyPublisher()
    }
}

struct TimerView: View {
    @Watch(TimerAtom())
    var timerPhase

    var body: some View {
        if let date = timerPhase.value {
            Text(date.formatted(date: .numeric, time: .shortened))
        }
    }
}
Description
Summary Provides a AsyncPhase value that represents sequence of values of the given Publisher.
Output AsyncPhase<T, E: Error>
Use Case Handle single or multiple asynchronous value(s) e.g. API call

ObservableObjectAtom

📖 Click to expand example code
class Contact: ObservableObject {
    @Published var name = ""
    @Published var age = 20

    func haveBirthday() {
        age += 1
    }
}

struct ContactAtom: ObservableObjectAtom, Hashable {
    func object(context: Context) -> Contact {
        Contact()
    }
}

struct ContactView: View {
    @WatchStateObject(ContactAtom())
    var contact

    var body: some View {
        VStack {
            TextField("Enter your name", text: $contact.name)
            Text("Age: \(contact.age)")
            Button("Celebrate your birthday!") {
                contact.haveBirthday()
            }
        }
    }
}
Description
Summary Instantiates an observable object.
Output T: ObservableObject
Use Case Mutable complex state object

Modifiers

Modifiers can be applied to an atom to produce a different versions of the original atom to make it more coding friendly or to reduce view re-computation for performance optimization.

select(_:)

📖 Click to expand example code
struct CountAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> Int {
        12345
    }
}

struct CountDisplayView: View {
    @Watch(CountAtom().select(\.description))
    var description  // : String

    var body: some View {
        Text(description)
    }
}
Description
Summary Selects a partial property with the specified key path from the original atom. The selected property doesn't notify updates if the new value is equivalent to the old value.
Output T: Equatable
Compatible All atoms types. The selected property must be Equatable compliant.
Use Case Performance optimization, Property scope restriction

phase

📖 Click to expand example code
struct FetchWeatherAtom: ThrowingTaskAtom, Hashable {
    func value(context: Context) async throws -> Weather {
        try await fetchWeather()
    }
}

struct WeatherReportView: View {
    @Watch(FetchWeatherAtom().phase)
    var weatherPhase  // : AsyncPhase<Weather, Error>

    var body: some View {
        switch weatherPhase {
        case .suspending:
            Text("Loading.")

        case .success(let weather):
            Text("It's \(weather.description) now!")

        case .failure:
            Text("Failed to get weather data.")
        }
    }
}
Description
Summary Converts the Task that the original atom provides into AsyncPhase.
Output AsyncPhase<T, E: Error>
Compatible TaskAtom, ThrowingTaskAtom
Use Case Consume asynchronous result as AsyncPhase

Property Wrappers

The following property wrappers are used to bind atoms to view and recompute the view with data changes.
By retrieving the atom through these property wrappers, the internal system marks the atom as in-use and the values are cached until that view is dismantled.

@Watch

📖 Click to expand example code
struct UserNameAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> String {
        "John"
    }
}

struct UserNameDisplayView: View {
    @Watch(UserNameAtom())
    var name

    var body: some View {
        Text("User name: \(name)")
    }
}
Description
Summary This property wrapper is similar to @State or @Environment, but is always read-only. It recomputes the view with value changes.
Compatible All atom types

@WatchState

📖 Click to expand example code
struct UserNameAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> String {
        "Jim"
    }
}

struct UserNameInputView: View {
    @WatchState(UserNameAtom())
    var name

    var body: some View {
        VStack {
            TextField("User name", text: $name)
            Button("Clear") {
                name = ""
            }
        }
    }
}
Description
Summary This property wrapper is read-write as the same interface as @State. It recomputes the view with data changes. You can get a Binding to the value using $ prefix.
Compatible StateAtom

@WatchStateObject

📖 Click to expand example code
class Counter: ObservableObject {
    @Published var count = 0

    func plus(_ value: Int) {
        count += value
    }
}

struct CounterAtom: ObservableObjectAtom, Hashable {
    func object(context: Context) -> Counter {
        Counter()
    }
}

struct CounterView: View {
    @WatchStateObject(CounterObjectAtom())
    var counter

    var body: some View {
        VStack {
            Text("Count: \(counter.count)")
            Stepper(value: $counter.count) {}
            Button("+100") {
                counter.plus(100)
            }
        }
    }
}
Description
Summary This property wrapper has the same interface as @StateObject and @ObservedObject. It recomputes the view when the observable object updates. You can get a Binding to one of the observable object's properties using $ prefix.
Compatible ObservableObjectAtom

@ViewContext

📖 Click to expand example code
struct FetchBookAtom: ThrowingTaskAtom, Hashable {
    let id: Int

    func value(context: Context) async throws -> Book {
        try await fetchBook(id: id)
    }
}

struct BookView: View {
    @ViewContext
    var context

    let id: Int

    var body: some View {
        let task = context.watch(FetchBookAtom(id: id))

        Suspense(task) { book in
            Text(book.content)
        } suspending: {
            ProgressView()
        }
    }
}

Unlike the property wrappers described the above, this property wrapper is not intended to bind single atom. It provides an AtomViewContext to the view, allowing for more functional control of atoms.
For instance, the following controls can only be done through the context.

  • refresh(_:) operator that to reset an asynchronous atom value and wait for its completion.
await context.refresh(FetchMoviesAtom())
  • reset(_:) operator that to clear the current atom value.
context.reset(CounterAtom())

The context also provides a flexible solution for passing dynamic parameters to atom's initializer. See Contexts section for more detail.


Contexts

Contexts are context structure for using and interacting with the data of other atoms from a view or an another atom. The basic API common to all contexts is as follows:

API Use
watch(_:) Obtains an atom value and starts watching its update.
read(_:) Obtains an atom value but does not watch its update.
set(_:for:) Sets a new value to the atom.
[:_] subscript Read-write access for applying mutating methods.
state(_:) Gets a binding to the atom state.
refresh(_:) Reset an atom and await until asynchronous operation is complete.
reset(_:) Reset an atom to the default value or a first output.

There are the following types context as different contextual environments, and they have some specific APIs for each.

AtomViewContext

📖 Click to expand example code
struct SearchQueryAtom: StateAtom, Hashable {
    func defaultValue(context: Context) -> String {
        ""
    }
}

struct FetchBooksAtom: ThrowingTaskAtom, Hashable {
    func value(context: Context) async throws -> [Book] {
        let query = context.watch(SearchQueryAtom())
        return try await fetchBooks(query: query)
    }
}

struct BooksView: View {
    @ViewContext
    var context: AtomViewContext

    var body: some View {
        // watch
        let booksTask = context.watch(FetchBooksAtom())     // Task<[Book], Error>
        // state
        let searchQuery = context.state(SearchQueryAtom())  // Binding<String>

        List {
            Suspense(booksTask) { books in
                ForEach(books, id: \.isbn) { book in
                    Text("\(book.title): \(book.isbn)")
                }
            }
        }
        .searchable(text: searchQuery)
        .refreshable { [context] in  // NB: Unfortunately, SwiftUI has a memory leak when capturing `self` implicitly inside a `refreshable` modifier.
            // refresh
            await context.refresh(FetchBooksAtom())
        }
        .toolbar {
            ToolbarItem(placement: .bottomBar) {
                HStack {
                    Button("Reset") {
                        // reset
                        context.reset(SearchQueryAtom())
                    }
                    Button("All") {
                        // set
                        context.set("All", for: SearchQueryAtom())
                    }
                    Button("Space") {
                        // subscript
                        context[SearchQueryAtom()].append(" ")
                    }
                    Button("Print") {
                        // read
                        let query = context.read(SearchQueryAtom())
                        print(query)
                    }
                }
            }
        }
    }
}

Context available through the @ViewContext property wrapper when using atoms from a view. There is no specific API for this context.

AtomRelationContext

📖 Click to expand example code
class LocationManagerDelegate: NSObject, CLLocationManagerDelegate { ... }

struct LocationManagerAtom: ValueAtom, Hashable {
    func value(context: Context) -> LocationManagerProtocol {
        let manager = CLLocationManager()
        let delegate = LocationManagerDelegate()

        manager.delegate = delegate
        context.addTermination(manager.stopUpdatingLocation)
        context.keepUntilTermination(delegate)

        return manager
    }
}

struct CoordinateAtom: ValueAtom, Hashable {
    func value(context: Context) -> CLLocationCoordinate2D? {
        let manager = context.watch(LocationManagerAtom())
        return manager.location?.coordinate
    }
}

Context passed as a parameter to the primary function of each atom type.

API Use
addTermination(_:) Calls the passed closure when the atom is updated or is no longer used.
keepUntilTermination(_:) Retains the given object instance until the atom is updated or is no longer used.

AtomTestContext

📖 Click to expand example code
protocol APIClientProtocol {
    func fetchMusics() async throws -> [Music]
}

struct APIClient: APIClientProtocol { ... }
struct MockAPIClient: APIClientProtocol { ... }

struct APIClientAtom: ValueAtom, Hashable {
    func value(context: Context) -> APIClientProtocol {
        APIClient()
    }
}

struct FetchMusicsAtom: ThrowingTaskAtom, Hashable {
    func value(context: Context) async throws -> [Music] {
        let api = context.watch(APIClientAtom())
        return try await api.fetchMusics()
    }
}

@MainActor
class FetchMusicsTests: XCTestCase {
    func testFetchMusicsAtom() async throws {
        let context = AtomTestContext()

        context.override(APIClientAtom()) { _ in
            MockAPIClient()
        }

        let musics = try await context.watch(FetchMusicsAtom()).value

        XCTAssertTrue(musics.isEmpty)
    }
}

Context that can simulate any scenarios in which atoms are used from a view or another atom and provides a comprehensive means of testing.

API Use
unwatch(_:) Simulates a scenario in which the atom is no longer watched.
override(_:with:) Overwrites the output of a specific atom or all atoms of the given type with the fixed value.
observe(_:) Observes changes in any atom values and its lifecycles.
onUpdate Sets a closure that notifies there has been an update to one of the atoms.

KeepAlive

KeepAlive allows the atom to preserve its data even if it's no longer watched to from anywhere.
In the example case below, once master data is obtained from the server, it can be cached in memory until the app process terminates.

struct FetchMasterDataAtom: ThrowingTaskAtom, KeepAlive, Hashable {
    func value(context: Context) async throws -> MasterData {
        try await fetchMasterData()
    }
}

Suspense

Suspense awaits the resulting value of the given Task and displays the content depending on its phase.
Optionally, you can pass suspending content to be displayed until the task completes, and pass catch content to be displayed if the task fails.

struct NewsView: View {
    @Watch(LatestNewsAtom())
    var newsTask: Task<News, Error>

    var body: some View {
        Suspense(newsTask) { news in
            Text(news.content)
        } suspending: {
            ProgressView()
        } catch: { error in
            Text(error.localizedDescription)
        }
    }
}

Testing

This library naturally integrates dependency injection and data-binding to provide a comprehensive means of testing. It allows you to test per small atom such that you can keep writing simple test cases per smallest unit of state without compose all states into a huge object and supposing complex integration test scenarios.
In order to fully test your app, this library guarantees the following principles:

  • Hermetic environment that no data is shared between test cases.
  • Dependencies are replaceable with any of mock/stub/fake/spy per test case.
  • Test cases can reproduce any possible scenarios at the view-layer.

In the test case, you first create an AtomTestContext instance that behaves similarly to other context types. The context allows for flexible reproduction of expected scenarios for testing using the control functions described in the Contexts section.
In addition, it's able to replace the atom value with test-friendly dependencies with override function. It helps you to write a reproducible & stable testing.
Since atom needs to be used from the main actor to guarantee thread-safety, XCTestCase class that to test atoms should have @MainActor attribute.

Click to expand the classes to be tested
struct Book: Equatable {
    var title: String
    var isbn: String
}

protocol APIClientProtocol {
    func fetchBook(isbn: String) async throws -> Book
}

struct APIClient: APIClientProtocol {
    func fetchBook(isbn: String) async throws -> Book {
        ... // Networking logic.
    }
}

class MockAPIClient: APIClientProtocol {
    var response: Book?

    func fetchBook(isbn: String) async throws -> Book {
        guard let response = response else {
            throw URLError(.unknown)
        }
        return response
    }
}

struct APIClientAtom: ValueAtom, Hashable {
    func value(context: Context) -> APIClientProtocol {
        APIClient()
    }
}

struct FetchBookAtom: ThrowingTaskAtom, Hashable {
    let isbn: String

    func value(context: Context) async throws -> Book {
        let api = context.watch(APIClientAtom())
        return try await api.fetchBook(isbn: isbn)
    }
}
@MainActor
class FetchBookTests: XCTestCase {
    func testFetch() async throws {
        let context = AtomTestContext()
        let api = MockAPIClient()

        // Override the atom value with the mock instance.
        context.override(APIClientAtom()) { _ in
            api
        }

        let expected = Book(title: "A book", isbn: "ISBN000–0–0000–0000–0")

        // Inject the expected response to the mock.
        api.response = expected

        let book = try await context.watch(FetchBookAtom(isbn: "ISBN000–0–0000–0000–0")).value

        XCTAssertEqual(book, expected)
    }
}

Preview

Even in SwiftUI previews, the view must have an AtomRoot somewhere in the ancestor. However, since This library offers the new solution for dependency injection, you don't need to do painful DI each time you create previews anymore. You can to override the atoms that you really want to inject substitutions.

struct NewsList_Preview: PreviewProvider {
    static var previews: some View {
        AtomRoot {
            NewsList()
        }
        .override(APIClientAtom()) { _ in
            StubAPIClient()
        }
    }
}

Observability

📖 Click to expand example code
struct Logger: AtomObserver {
    func atomAssigned<Node: Atom>(atom: Node) {
        print("\(atom) started to be used somewhere.")
    }

    func atomUnassigned<Node: Atom>(atom: Node) {
        print("\(atom) is no longer used.")
    }

    func atomChanged<Node: Atom>(snapshot: Snapshot<Node>) {
        print("The value of `\(snapshot.atom)` is changed to `\(snapshot.value)`.")
    }
}

@main
struct ExampleApp: App {
    var body: some Scene {
        WindowGroup {
            AtomRoot {
                VStack {
                    NavigationLink("Home") {
                        Home()
                    }

                    NavigationLink("Setting") {
                        AtomRelay {
                            Setting()
                        }
                        .observe(Logger())  // Observes setting related atoms only.
                    }
                }
            }
            .observe(Logger())  // Observes all atoms used in the app.
        }
    }
}

You can monitor the updates and lifecycle of atoms used in your app by registering an AtomObserver compliant instance through the observe(_:) function in AtomRoot or AtomRelay.
Registering an observer in AtomRoot observes all atoms used in the app, but in contrast, using AtomRelay can observe partial atoms that used in the descendant views.
In addition, this observability can be applied to do time travel debugging and is demonstrated in one of the examples.


Advanced Usage

Obtain an atom value without watching to it

📖 Click to expand example code
struct TextAtom: StateAtom, Hashable {
    func value(context: Context) -> String {
        ""
    }
}

struct TextCopyView: View {
    @ViewContext
    var context

    var body: some View {
        Button("Copy") {
            UIPasteboard.general.string = context.read(TextAtom())
        }
    }
}

The read(_:) function is a way to get the data of an atom without having watch to and receiving future updates of it. It's commonly used inside functions triggered by call-to-actions.

Dynamically initiate an atom with external parameters

📖 Click to expand example code
struct FetchUserAtom: ThrowingTaskAtom {
    let id: Int

    // This atom can also conforms to `Hashable` in this case,
    // but this example specifies the key explicitly.
    var key: Int {
        id
    }

    func value(context: Context) async throws -> Value {
        try await fetchUser(id: id)
    }
}

struct UserView: View {
    let id: Int

    @ViewContext
    var context

    var body: some View {
        let task = context.watch(FetchUserAtom(id: id))

        Suspense(task) { user in
            VStack {
                Text("Name: \(user.name)")
                Text("Age: \(user.age)")
            }
        }
    }
}

Each atom must have a unique key to be uniquely associated with its value. As described in the Atoms section, it is automatically synthesized by conforming to Hashable, but with explicitly specifying a key allowing you to pass arbitrary external parameters to the atom. It is commonly used, for example, to retrieve user information associated with a dynamically specified ID from a server.

Pass a context to your object to interact with other atoms

📖 Click to expand example code
@MainActor
class MessageLoader: ObservableObject {
    let context: AtomContext

    @Published
    var phase = AsyncPhase<[Message], Error>.suspending

    init(context: AtomContext) {
        self.context = context
    }

    func load() async {
        do {
            let api = context.read(APIClientAtom())
            let messages = try await api.fetchMessages(offset: 0)
            phase = .success(messages)
        }
        catch {
            phase = .failure(error)
        }
    }

    func loadNext() async {
        guard let messages = phase.value else {
            return
        }

        do {
            let api = context.read(APIClientAtom())
            let next = try await api.fetchMessages(offset: messages.count)
            phase = .success(messages + next)
        }
        catch {
            phase = .failure(error)
        }
    }
}

struct MessageLoaderAtom: ObservableObjectAtom, Hashable {
    func object(context: Context) -> MessageLoader {
        MessageLoader(context: context)
    }
}

You can pass a context to your object and interact with other atoms at any asynchronous timing. However, in that case, when the watch is called, it end up with the object instance itself will be re-created with fresh data. Therefore, you can explicitly prevent the use of the watch by passing it as AtomContext type.


Dealing with Known SwiftUI Bugs

In iOS14, modal presentation causes assertionFailure when dismissing it

💡 Click to expand workaround
struct RootView: View {
    @State
    var isPresented = false

    @ViewContext
    var context

    var body: some View {
        VStack {
            Text("Example View")
        }
        .sheet(isPresented: $isPresented) {
            AtomRelay(context) {
                MailView()
            }
        }
    }
}

Unfortunately, SwiftUI has a bug in iOS14 where the EnvironmentValue is removed from a screen presented with .sheet just before dismissing it. Since this library is designed based on EnvironmentValue, this bug end up triggering the friendly assertionFailure that is added so that developers can easily aware of forgotten AtomRoot implementation.
As a workaround, AtomRelay has the ability to explicitly inherit the internal store through AtomViewContext from the parent view.

Some SwiftUI modifiers cause memory leak

💡 Click to expand workaround
@ViewContext
var context

...

.refreshable { [context] in
    await context.refresh(FetchDataAtom())
}
@State
var isShowingSearchScreen = false

...

.onSubmit { [$isShowingSearchScreen] in
    $isShowingSearchScreen.wrappedValue = true
}

Some modifiers in SwiftUI seem to cause an internal memory leak if it captures self implicitly or explicitly. To avoid that bug, make sure that self is not captured when using those modifiers.
Below are the list of modifiers I found that cause memory leaks:


Contributing

Any type of contribution is welcome! e.g.

  • Give it star & fork this repository.
  • Report bugs with reproducible steps.
  • Propose new features.
  • Add more documentations.
  • Provide repos of sample apps using this library.
  • Become a maintainer after making multiple contributions.
  • Become a sponsor.

Acknowledgements


License

MIT © Ryo Aoyama


Comments
  • [Question]: My watched ObservableObjectAtom isn't updating, am I configuring something wrong?

    [Question]: My watched ObservableObjectAtom isn't updating, am I configuring something wrong?

    Checklist

    • [X] This is not a bug caused by platform.
    • [X] Reviewed the README and documentation.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    What happened?

    (I apologize if this is not a bug, I wasn't sure which category of issue to file this under.) (I have been using Atoms for a few weeks, with zero issues. I've been able to get almost everything working except this one last thing. I got a stuck on it a week ago, left it and moved on, but now I've come back to it and after a day of trying to get it working, I'm stumped.)

    Context:

    1. I have an AccountStore, (most of which was copied from the Apple Passkey Sample Code) that is working correctly (when I use it as an EnvironmentObject. Here is part of it, shortened for brevity:
    public final class AccountStore: NSObject, ObservableObject, ASAuthorizationControllerDelegate {
       @Published public private(set) var currentUser: User? = .default
    
       @MainActor
       public func signIn(username: String) async throws {
          // ... 
       }
    
       @MainActor
       public func signOut() {
          // ... 
       }
    
       // ...
    }
    
    1. Since I've managed to move the rest of my data model over to Atoms, the only thing left is Auth & fetching the User (Player). Here are the Atoms I created in order to try to do this:
    struct AccountStoreAtom: ObservableObjectAtom, Hashable {
    	func object(context: Context) -> AccountStore {
    		AccountStore()
    	}
    }
    
    struct FetchPlayerAtom: ThrowingTaskAtom, Hashable {
    	func defaultValue(context: Context) -> Player? {
    		return nil
    	}
    	
    	func value(context: Context) async throws -> Player {
    		print("FetchPlayerAtom().value")
    		return try await BackendAPI.fetchPlayer()
    	}
    }
    
    struct PlayerAtom: ValueAtom, Hashable {
    	func value(context: Context) -> Player? {
    		let accountStore = context.watch(AccountStoreAtom())
    		print("PlayerAtom().value", accountStore.currentUser as Any)
    		
    		if case let .authenticated(username) = accountStore.currentUser {
    			print("PlayerAtom().value.authenticated()", username)
    			let fetchPlayerPhase = context.watch(FetchPlayerAtom().phase)
    			
    			if case let .success(player) = fetchPlayerPhase {
    				print("PlayerAtom().value.success()", player)
    				return player
    			}
    		}
    		
    		return nil
    	}
    }
    
    struct GameScreenAtom: ValueAtom, Hashable {
    	func value(context: Context) -> GameScreen {
    		let player = context.watch(PlayerAtom())
    		
    		print("GameScreenAtom().value", player as Any)
    
    		if player == nil {
    			return .mainMenu
    		}
    	
    		if player!.isPlaying {
    			return .homeBase
    		}
    	
    		return .setupNew
    	}
    }
    

    Expected Behavior

    After logging in, the AccountStore publishes to currentUser, setting it to authenticated.

    I expect that:

    1. The PlayerAtom(), which is watching AccountStoreAtom(), would notice that currentUser was updated. (Not happening)
    2. Then it would watch the FetchPlayerAtom(), and return the Player after it's fetched.
    3. Finally, the GameScreenAtom(), which is watching the PlayerAtom(), would notice that a Player now exists, and would set the gameScreen accordingly.

    Reproduction Steps

    I don't have a great way to help you reproduce this yet, I'm hoping that you can spot if I'm doing anything incorrectly in my code. If not, then I'll try to put together a reproducible example.

    I'm attaching a Screen Recording of this. If you watch it, please play attention to the orange 🏗 Debug Values at the top of the screen. You will note that when the EnvObject.currentScreen updates (which is computed in the same way, from the AccountStore), the Atom.GameScreen does not.

    Swift Version

    5

    Library Version

    0.2.0

    Platform

    iOS

    Scrrenshot/Video/Gif

    https://user-images.githubusercontent.com/1843672/181870440-1fc9033b-040b-4021-8692-f7875debc164.MP4

    bug 
    opened by felixakiragreen 6
  • Remove all EXCLUDED_ARCHS configs to avoid an error

    Remove all EXCLUDED_ARCHS configs to avoid an error

    Hi! Thank you for sharing the interesting framework! I was browsing the README and wanted to run some sample apps on my Xcode. And then, I found a setting issue in my env. Could you check this when you have time? Thank you 🙏

    Pull Request Type

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Description

    The issue is that M1 Mac without Rosetta cannot run the sample projects.

    Could not find module 'iOSApp' for target 'x86_64-apple-ios-simulator'; found: arm64-apple-ios-simulator, at: /*/Build/Products/Debug-iphonesimulator/iOSApp.swiftmodule

    To fix the issue, I've removed all EXCLUDED_ARCHS because it looks unnecessary in my env. But I'm not sure about how intel Mac behaves.

    Motivation and Context

    I want to run Xcode without Rosetta.

    Impact on Existing Code

    M1 users will need to use Xcode via Rosetta to run the sample projects.

    Screenshot/Video/Gif

    Screen Shot 2022-04-19 at 18 27 29
    opened by yasuradodo 3
  • [Doc Request]: How to keep specific atoms alive if there's no watchers until the `AtomRoot` destroyed.

    [Doc Request]: How to keep specific atoms alive if there's no watchers until the `AtomRoot` destroyed.

    Checklist

    • [X] Reviewed the README and documentation.
    • [X] Confirmed that this is uncovered by existing docs or examples.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    Description

    Currently if there's no view watch a atom, it will auto unassigned, and later when a view watch if again, it will reset to default value. If i want keep the atom value, i need to find a way to cache it when it recreate, or maybe a EmptyView watch at it at any time. I wanna know if there's a better way to approach.

    Motivation & Context

    Maybe a new property to define the life circle behavior of a atom, auto unassigned or alive if the AtomRoot exist.

    documentation 
    opened by strangeliu 2
  • [Bug]: The package product can't be used as a dependency

    [Bug]: The package product can't be used as a dependency

    Checklist

    • [X] This is not a bug caused by platform.
    • [X] Reviewed the README and documentation.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    What happened?

    When you try to use this package as a dependency, Xcode shows the following error.

    The package product 'Atoms' cannot be used as a dependency of this target because it uses unsafe build flags.
    

    Expected Behavior

    The package is usable without any error.

    Reproduction Steps

    1. Add the package as a dependency of your project through Xcode.
    2. Build it.

    Swift Version

    5.6

    Library Version

    0.1.0

    Platform

    iOS, tvOS, macOS, watchOS

    Scrrenshot/Video/Gif

    No response

    bug 
    opened by ra1028 1
  • Add support for Xcode 14

    Add support for Xcode 14

    Pull Request Type

    • [ ] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [x] Chore

    Description

    Xcode 13.3 is still supported even after this PR is merged. I still see some weird warning Result of call to function returning xxx is unused, but it's reported as a bug of Swift5.7 at here.

    opened by ra1028 0
  • Display dependency graph in DOT language

    Display dependency graph in DOT language

    Pull Request Type

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Issue for this PR

    Link:

    Description

    Add a capability to display a string representation of dependency graph in DOT language.

    Impact on Existing Code

    Contains some breaking changes in debugging information, but no impact on normal usage.

    Screenshot/Video/Gif

    digraph {
      node [shape=box]
      "APIClientAtom"
      "APIClientAtom" -> "ImageAtom"
      "APIClientAtom" -> "MovieLoaderAtom"
      "ExampleMovieDB/FiltePicker.swift" [style=filled]
      "ExampleMovieDB/MoviesScreen.swift" [style=filled]
      "ExampleMovieDB/NetworkImage.swift" [style=filled]
      "FilterAtom"
      "FilterAtom" -> "ExampleMovieDB/FiltePicker.swift" [label="line:5"]
      "FilterAtom" -> "MovieLoaderAtom"
      "ImageAtom"
      "ImageAtom" -> "ExampleMovieDB/NetworkImage.swift" [label="line:8"]
      "MovieLoaderAtom"
      "MovieLoaderAtom" -> "ExampleMovieDB/MoviesScreen.swift" [label="line:5"]
      "SearchQueryAtom"
      "SearchQueryAtom" -> "ExampleMovieDB/MoviesScreen.swift" [label="line:8"]
    }
    

    graphviz

    opened by ra1028 0
  • Do not terminate current atom state when an old snapshot is restored

    Do not terminate current atom state when an old snapshot is restored

    Pull Request Type

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Description

    Stop terminating the current atom state when old values are restored via snapshot.

    Motivation and Context

    Terminating a state could cause weird behavior, such as ObservableObject doesn't emits update anymore. Still it's not an ideal solution but we'd like to stop terminating them for now.

    opened by ra1028 0
  • refactor: Remove observe(_:) function from AtomTestContext

    refactor: Remove observe(_:) function from AtomTestContext

    Pull Request Type

    • [ ] Bug fix
    • [ ] New feature
    • [x] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Description

    • Remove AtomTestContext/observe(_:).

    Motivation and Context

    AtomTestContext is a class for testing so nobody won't needs observability in the test context.

    Impact on Existing Code

    • AtomTestContext/observe(_:) is abolished.
    opened by ra1028 0
  • feat: Enable to get Snapshot through AtomViewContext

    feat: Enable to get Snapshot through AtomViewContext

    Pull Request Type

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Description

    • Add snapshot() function to AtomViewContext.
    • Fix minor bugs of restore function.

    Motivation and Context

    Currently, Snapshot can only be obtained through AtomRoot|AtomRelay/observe(_:) so this functionality allows users to get Snapshot on-demand through AtomViewContext.

    opened by ra1028 0
  • feat: New Observability API

    feat: New Observability API

    Pull Request Type

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Issue for this PR

    Link: close #17

    Description

    The current observability API done with AtomObserver has too few functionalities as it just can observe what atom is registered/released/updated and that's it. It would be better to support more powerful functionalities like accessing arbitrary atom values through a Snapshot structure. Also, currently Snapshot.restore() can only reset the current value to be the snapshotted value, but it should reset the whole store state to be the snapshotted one to prevent inconsistency.

    Impact on Existing Code

    The following APIs have been abolished.

    • AtomObserver
    • AtomRoot/observe(_: AtomObserver)
    • AtomRelay/observe(_: AtomObserver)
    opened by ra1028 0
  • Enable store missing assertion only for default store context

    Enable store missing assertion only for default store context

    Pull Request Type

    • [x] Bug fix
    • [ ] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Description

    Add enablesAssertion which is a flag to determine whether the it should assert when a store is missing.

    Motivation and Context

    Unit tests could crash if some atom is updated asynchronously after AtomTestContext is released.

    opened by ra1028 0
  • [Feat Request]: UIKit support

    [Feat Request]: UIKit support

    Checklist

    • [X] Reviewed the README and documentation.
    • [X] Checked existing issues & PRs to ensure not duplicated.

    Description

    Any plan to support in UIKit?

    Example Use Case

    It would be great if we can use it in UIKit app

    Alternative Solution

    No response

    Proposed Solution

    No response

    Motivation & Context

    No response

    enhancement 
    opened by BayramInanc 1
  • Add asynchronous init in AsyncPhase

    Add asynchronous init in AsyncPhase

    Pull Request Type

    • [ ] Bug fix
    • [x] New feature
    • [ ] Refactoring
    • [ ] Documentation update
    • [ ] Chore

    Description

    AsyncPhase now has a utility initializer to handle a result of async throwing functions.

    ⚠️ This PR is still blocked by swift-format's issue that it isn't able to parse async throwing initializer with error: file contains invalid or unrecognized Swift syntax.

    opened by ra1028 0
Releases(0.3.0)
  • 0.3.0(Oct 8, 2022)

    ⚠️ Breaking changes

    • Atom.Context.addTermination(_:) has been obsoleted. Use the new Coordinator API implemented in #24 instead.
    • StateAtom.willSet(newValue:oldValue:context:)andStateAtom.didSet(newValue:oldValue:context)` have been obsoleted. Use the new side-effects management API implemented in #25 instead.
    • AtomObserver, AtomRoot.observe(_:), AtomRelay.observe(_:), and AtomRoot.observe(_:) have been obsoleted. Use the new Observability API implemented in #27 instead.

    What's Changed

    • refactor: Atom modifier by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/11
    • feat: Ensure that ObservableObjectAtom notifies updates after a new @Published value is set by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/10
    • refactor: Use ObservableObject more in examples by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/12
    • feat: Add a new testing interface AtomContext/waitUntilNextUpdate(timeout:) by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/13
    • refactor: Map app example by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/14
    • refactor: Internal system by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/15
    • refactor: DAG algorithm based state management by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/16
    • chore: Install development tools in the root Package.swift by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/21
    • chore: Remove Package.resolved by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/22
    • fix: Lifecycle and unsubscription problem by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/23
    • feat: Atom Coordinator API by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/24
    • feat: Add a new side-effects management API by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/25
    • Enable store missing assertion only for default store context by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/26
    • feat: New Observability API by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/27
    • feat: Enable to get Snapshot through AtomViewContext by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/28
    • refactor: Remove observe(_:) function from AtomTestContext by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/29
    • Do not terminate current atom state when an old snapshot is restored by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/30
    • Display dependency graph in DOT language by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/31
    • Update README by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/32
    • Move AtomReader to under Context directory by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/34
    • Add support for Xcode 14 by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/33

    Full Changelog: https://github.com/ra1028/swiftui-atom-properties/compare/0.2.0...0.3.0

    Source code(tar.gz)
    Source code(zip)
  • 0.2.0(May 26, 2022)

    This version contains a breaking change regarding package/repository name. GitHub would automatically redirect you to the old repo name but please consider to update your Package.swift for the new name.

    What's Changed

    • chore: Prepare for renaming the repository name to be swiftui-atom-properties by @ra1028 in https://github.com/ra1028/swiftui-atom-properties/pull/8

    Full Changelog: https://github.com/ra1028/swiftui-atom-properties/compare/0.1.1...0.2.0

    Source code(tar.gz)
    Source code(zip)
  • 0.1.1(May 9, 2022)

    What's Changed

    • chore: Improve README documentation by @ra1028 in https://github.com/ra1028/swiftui-atomic-architecture/pull/1
    • Fix typos in README by @jordanekay in https://github.com/ra1028/swiftui-atomic-architecture/pull/2
    • Remove all EXCLUDED_ARCHS configs to avoid an error by @yasuradodo in https://github.com/ra1028/swiftui-atomic-architecture/pull/3
    • fix: Remove unsafe flags from the package definition by @ra1028 in https://github.com/ra1028/swiftui-atomic-architecture/pull/6

    New Contributors

    • @ra1028 made their first contribution in https://github.com/ra1028/swiftui-atomic-architecture/pull/1
    • @jordanekay made their first contribution in https://github.com/ra1028/swiftui-atomic-architecture/pull/2
    • @yasuradodo made their first contribution in https://github.com/ra1028/swiftui-atomic-architecture/pull/3

    Full Changelog: https://github.com/ra1028/swiftui-atomic-architecture/compare/0.1.0...0.1.1

    Source code(tar.gz)
    Source code(zip)
  • 0.1.0(Apr 9, 2022)

Owner
Ryo Aoyama
👾 Lead iOS architect at @mercari
Ryo Aoyama
SignalKit is a reactive Swift framework with focus on clean and readable API.

Abstract SignalKit is a lightweight event and binding framework. The core of SignalKit is the Observable protocol. Each implementation of the Observab

Yanko Dimitrov 252 Dec 13, 2022
Mini-application iOS native avec Xcode et Swift exploitant l'architecture MVVM et le framework Combine d'Apple pour la mise en place de la programmation réactive fonctionnelle, le tout avec UIKit.

iOS (Swift 5): Test MVVM avec Combine et UIKit L'architecture MVVM et la programmation réactive fonctionnelle sont très utlisées dans le développement

Koussaïla BEN MAMAR 2 Nov 5, 2022
This to learn such as : Add Target , NSNotification Center Send/Get Data , Observer Override , resize Data By Byte , UIImagePicker Delegate , UIAlert Handle , Top ViewController , Get pickerController

Technicalisto How to Create UIButton Class to Pick Data Image Purpose Learn this topics With exact Task Add Target NSNotification Center Send/Get Data

Aya Baghdadi 1 Feb 20, 2022
Jared Watson 0 Jan 8, 2022
Exemplify a LazyVGrid in SwiftUI in a MVVM pattern with Mock Data and images in assets.

SwiftUI-LazyVGrid Exemplify a LazyVGrid in SwiftUI in a MVVM pattern with Mock Data and images in assets. Screenshots Samples IP & Credits All those b

Ethan Halprin 3 Aug 9, 2022
ClearScore - This application simulates a credit check on a user by making a call to an API and displaying the data

ClearScore Description This application simulates a credit check on a user by ma

Sashen Pillay 0 Jan 11, 2022
SampleProjectMVVM - Sample project using MVVM parsing data from Giphy.com

iOS Take Home Create an iOS app with two views, MainViewController and DetailVie

HG HrachGarabedian 0 Jan 27, 2022
Lightweight Framework for using Core Data with Value Types

Features Uses Swift Reflection to convert value types to NSManagedObjects iOS and Mac OS X support Use with structs Works fine with let and var based

Benedikt Terhechte 456 Nov 6, 2022
SwiftUI module library for adding seasons theme animations to your app

HolidayThemes SwiftUI module library for adding seasons theme animations to your app. Requirements iOS 13.0+ Xcode 12.0+ Installation Swift Package Ma

null 2 Mar 7, 2022
This a simple swiftui app where i used mvvm architecture, coredata swiftui and so on..

SwiftUI MVVM COREDATA NOTE APP This a simple swiftui app where i used mvvm architecture, coredata swiftui and so on... #FEATURES SWIFTUI MVVM COREDATA

Isaias Cuvula 1 Jul 20, 2022
MockChat TDD MVVM SwiftUI - Mockable, Test driven Development, clear Architecture example using SwiftUI

MockChat_TDD_MVVM_SwiftUI Mockable, Test driven Development, clear Architecture

Zahirul Islam 0 Feb 5, 2022
SmartString - A powerful and small library that will allow the creation of complex String Styles

SmartString A powerful and small library that will allow the creation of complex

Valerio 7 Oct 26, 2022
DCL Private Library

DCLPrivateLibrary Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements Installation

null 0 Nov 28, 2021
This library is a UIView that is capable of Picture-in-Picture (PiP) in iOS.

>> 日本語 UIPiPView This library is a UIView that is capable of Picture-in-Picture (PiP) in iOS. Using this library, information that is updated in real

Akihiro Urushihara 128 Jan 4, 2023
Swift library of lightweight interfaces for prototyping, bridged to JS

Prototope Prototope is a lightweight, high-performance prototyping framework. Its goals are: making simple things very easy making complex things poss

Khan Academy 230 Jun 29, 2022
CoreDragon is a drag'n'drop library for iOS applications

CoreDragon is a drag'n'drop library for iOS applications. Instead of using context menus, modal view controllers, share sheets and other "indirect manipulation" ways of moving data around, it's much more intuitive to just grab the thing you want to move, and drop it on the place where you want to move it to.

Nevyn Bengtsson 680 Sep 9, 2022
Swift Client library to interact with Supabase Functions.

functions-swift Swift Client library to interact with Supabase Functions. Usage let client = FunctionsClient( url: URL(string: "https://project-id.s

Supabase Community 3 Dec 1, 2022
A demo demonstrates how to use combine and MVVM in the SwiftUI app

SwiftUI-MVVM-Combine A demo demonstrates how to use combine and MVVM in the Swif

Asa. Ga 7 Jul 5, 2022