Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
085f689
Add core classes
ra1028 Aug 10, 2022
edb703d
Add function placeholders
ra1028 Aug 10, 2022
ccb4856
Implement Store functions
ra1028 Aug 10, 2022
fab6a80
Rename AtomOverrides to Overrides
ra1028 Aug 11, 2022
a53a9f7
Migrate core functionalities
ra1028 Aug 11, 2022
0da699b
Fix RefreshableAtomValue doesn't finish refreshing
ra1028 Aug 11, 2022
97d9b65
Reuse existing function for Snapshot.restore
ra1028 Aug 11, 2022
ea5ce87
Update downstream after refresh is finished
ra1028 Aug 11, 2022
c7b73f5
Gardening
ra1028 Aug 11, 2022
b2a4623
Ensure that the dependency release task happen after view update
ra1028 Aug 11, 2022
e4090bb
Clear current subscriptions when an atom is updated
ra1028 Aug 11, 2022
86f103d
Cleanup subscription container before view updates
ra1028 Aug 11, 2022
7526efd
Drop support for subscriptions to be cleanup before every update
ra1028 Aug 12, 2022
e153c46
Gardening
ra1028 Aug 12, 2022
f3307ea
Fix refresh issue on example app
ra1028 Aug 12, 2022
8c171fa
Refactoring
ra1028 Aug 12, 2022
fdc6229
Scheduled dependency release
ra1028 Aug 12, 2022
d4882f4
Support KeepAlive
ra1028 Aug 12, 2022
7001bcc
Support atomAssigned observer
ra1028 Aug 12, 2022
f32cb99
Support atomUnassigned observer
ra1028 Aug 12, 2022
23ab3c4
Remvoe unneeded code
ra1028 Aug 12, 2022
2e5b9cf
Support override with ObservableObject
ra1028 Aug 12, 2022
7bf2aaa
Support refresh atoms correctly
ra1028 Aug 12, 2022
42c84b9
Partially support shouldNotifyUpdate
ra1028 Aug 12, 2022
23f4031
Commented out or removed uncompilable tests
ra1028 Aug 12, 2022
b99ab23
Rename startUpdating to handleUpdates
ra1028 Aug 12, 2022
a6816c1
Fix for tests
ra1028 Aug 12, 2022
106c26f
Do not release an atom when the dependencies are updated
ra1028 Aug 12, 2022
3ec563a
Updates ObservableObject in a next run loop
ra1028 Aug 12, 2022
8982bf5
Fix temporary named classes
ra1028 Aug 12, 2022
4731363
Tidyup
ra1028 Aug 12, 2022
195cd4e
Cleanup value when reset
ra1028 Aug 12, 2022
8789cd1
Change overriding method from handleUpdate(context:with:) to lookup(c…
ra1028 Aug 13, 2022
d68e81a
Refactoring
ra1028 Aug 13, 2022
ad967f6
Terminate before refresh
ra1028 Aug 13, 2022
683bf92
Update dependents on next run loop
ra1028 Aug 13, 2022
5689584
Refactoring
ra1028 Aug 13, 2022
3993117
Ensures that the observable object is updated before notifying updates
ra1028 Aug 13, 2022
3571c05
Refactoring
ra1028 Aug 13, 2022
80347fa
Refactor
ra1028 Aug 13, 2022
042837c
Change request configuration of example app
ra1028 Aug 14, 2022
18807f8
Rename graph property
ra1028 Aug 14, 2022
1b49f9a
Refactor Graph
ra1028 Aug 14, 2022
af00863
Refactor StoreState
ra1028 Aug 14, 2022
9e0ab2d
Gardening
ra1028 Aug 14, 2022
8e43743
Add test cases for dependencies release of (Throwing)TaskAtom
ra1028 Aug 15, 2022
3577f17
Release dependencies after finishing to get atom value
ra1028 Aug 15, 2022
b244955
Rename core classes
ra1028 Aug 15, 2022
e95c559
Add more test cases for complex dependencies structure
ra1028 Aug 15, 2022
ebca7dd
Fix for concurrent transaction
ra1028 Aug 16, 2022
50e78cb
Gardening
ra1028 Aug 16, 2022
e88807a
More strict atom process termination
ra1028 Aug 17, 2022
0a26513
Refactoring
ra1028 Aug 17, 2022
511a69a
Make atom state immutable
ra1028 Aug 19, 2022
661f705
Move terminations to Transaction
ra1028 Aug 19, 2022
96f75d8
Refactoring
ra1028 Aug 19, 2022
129bcf9
Refactoring
ra1028 Aug 20, 2022
e626128
Update test
ra1028 Aug 21, 2022
f75b602
Refactor comments
ra1028 Aug 21, 2022
a529ee6
Update API documentation
ra1028 Aug 22, 2022
c1ccd99
Gardening
ra1028 Aug 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ final class MovieLoader: ObservableObject {

func refresh() async {
do {
pages = .suspending

let page = try await api.getMovies(filter: filter, page: 1)
pages = .success([page])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ protocol APIClientProtocol {
}

struct APIClient: APIClientProtocol {
private let session = URLSession.shared
private let session: URLSession = URLSession(configuration: .ephemeral)
private let baseURL = URL(string: "https://api.themoviedb.org/3")!
private let imageBaseURL = URL(string: "https://image.tmdb.org/t/p")!
private let apiKey = "3de15b0402484d3d089399ea0b8d98f1"
Expand All @@ -30,8 +30,8 @@ struct APIClient: APIClientProtocol {
imageBaseURL
.appendingPathComponent(size.rawValue)
.appendingPathComponent(path)

let (data, _) = try await session.data(from: url)
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
let (data, _) = try await session.data(for: request)
return UIImage(data: data) ?? UIImage()
}

Expand Down Expand Up @@ -63,7 +63,8 @@ struct APIClient: APIClientProtocol {
Future { fulfill in
Task {
do {
let response: PagedResponse<Movie> = try await get(
let response = try await get(
PagedResponse<Movie>.self,
path: "search/movie",
parameters: ["query": query]
)
Expand Down Expand Up @@ -93,7 +94,7 @@ private extension APIClient {

urlComponents.queryItems = queryItems

var urlRequest = URLRequest(url: urlComponents.url!)
var urlRequest = URLRequest(url: urlComponents.url!, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 10)
urlRequest.httpMethod = "GET"

do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ struct MoviesScreen: View {
FilterPicker()

switch loader.pages {
case .suspending:
ProgressRow()

case .failure:
CaveatRow(text: "Failed to get the data.")

case .success(let pages):
ForEach(pages, id: \.page) { response in
pageIndex(current: response.page, total: response.totalPages)
Expand All @@ -43,12 +49,6 @@ struct MoviesScreen: View {
await loader.loadNext()
}
}

case .failure:
CaveatRow(text: "Failed to get the data.")

case .suspending:
ProgressRow()
}
}
}
Expand All @@ -69,8 +69,11 @@ struct MoviesScreen: View {
.task(id: loader.filter) {
await loader.refresh()
}
.refreshable { [loader] in
await loader.refresh()
.refreshable { [context] in
// NB: Implicitly capturing `self` causes memory leak with `refreshable`,
// and also capturing `loader` makes refresh doesn't work, so here reads
// `MovieLoader` via context.
await context.read(MovieLoaderAtom()).refresh()
}
.background {
NavigationLink(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct CastList: View {
CaveatRow(text: "Failed to get casts data.")

case .success(let casts) where casts.isEmpty:
CaveatRow(text: "There are no casts.")
CaveatRow(text: "No cast information is available.")

case .success(let casts):
ScrollView(.horizontal, showsIndicators: false) {
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ struct BooksView: View {

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

#### [AtomRelationContext](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomrelationcontext)
#### [AtomTransactionContext](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext)

<details><summary><code>📖 Click to expand example code</code></summary>

Expand Down Expand Up @@ -1028,8 +1028,8 @@ Context passed as a parameter to the primary function of each atom type.

|API|Use|
|:--|:--|
|[addTermination(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomrelationcontext/addtermination(_:))|Calls the passed closure when the atom is updated or is no longer used.|
|[keepUntilTermination(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomrelationcontext/keepuntiltermination(_:))|Retains the given object instance until the atom is updated or is no longer used.|
|[addTermination(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext/addtermination(_:))|Calls the passed closure when the atom is updated or is no longer used.|
|[keepUntilTermination(_:)](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtransactioncontext/keepuntiltermination(_:))|Retains the given object instance until the atom is updated or is no longer used.|

#### [AtomTestContext](https://ra1028.github.io/swiftui-atom-properties/documentation/atoms/atomtestcontext)

Expand Down
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/AsyncSequenceAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
/// }
/// ```
///
public protocol AsyncSequenceAtom: Atom where State == AsyncSequenceAtomState<Sequence> {
public protocol AsyncSequenceAtom: Atom where Loader == AsyncSequenceAtomLoader<Sequence> {
/// The type of asynchronous sequence that this atom manages.
associatedtype Sequence: AsyncSequence

Expand All @@ -64,7 +64,7 @@ public protocol AsyncSequenceAtom: Atom where State == AsyncSequenceAtomState<Se

public extension AsyncSequenceAtom {
@MainActor
func makeState() -> State {
State(makeSequence: sequence)
var _loader: Loader {
Loader(makeSequence: sequence)
}
}
33 changes: 5 additions & 28 deletions Sources/Atoms/Atom/Atom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@ public protocol Atom {
/// A type representing the stable identity of this atom.
associatedtype Key: Hashable

/// A type of state that is an actual implementation of this atom.
associatedtype State: AtomState
/// A loader type that represents an actual implementation of the corresponding atom.
associatedtype Loader: AtomLoader

/// A type of the context structure that to read, watch, and otherwise interacting
/// with other atoms.
typealias Context = AtomRelationContext
typealias Context = AtomTransactionContext

/// A boolean value indicating whether the atom value should be preserved even if
/// no longer watched to.
///
/// It's recommended to conform the ``KeepAlive`` to this atom, instead of overriding
/// this property to return `true`.
/// The default is `false`.
@MainActor
static var shouldKeepAlive: Bool { get }

/// A unique value used to identify the atom internally.
Expand All @@ -32,34 +31,12 @@ public protocol Atom {
/// If this atom conforms to `Hashable`, it will adopt itself as the `key` by default.
var key: Key { get }

/// Creates a new state that is an actual implementation of this atom.
///
/// - Returns: A state object that handles internal process and a value.
@MainActor
func makeState() -> State

/// Returns a boolean value that determines whether it should notify the value update to
/// watchers with comparing the given old value and the new value.
///
/// - Parameters:
/// - newValue: The new value after update.
/// - oldValue: The old value before update.
///
/// - Returns: A boolean value that determines whether it should notify the value update
/// to watchers.
@MainActor
func shouldNotifyUpdate(newValue: State.Value, oldValue: State.Value) -> Bool
}

public extension Atom {
/// A loader that represents an actual implementation of the corresponding atom.
@MainActor
func shouldNotifyUpdate(newValue: State.Value, oldValue: State.Value) -> Bool {
true
}
var _loader: Loader { get }
}

public extension Atom {
@MainActor
static var shouldKeepAlive: Bool {
false
}
Expand Down
16 changes: 4 additions & 12 deletions Sources/Atoms/Atom/ModifiedAtom.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// An atom type that applies a modifier to an atom.
///
/// Use ``Atom/modifier(_:)`` instead of using this atom directly.
public struct ModifiedAtom<Node: Atom, Modifier: AtomModifier>: Atom where Node.State.Value == Modifier.Value {
public struct ModifiedAtom<Node: Atom, Modifier: AtomModifier>: Atom where Node.Loader.Value == Modifier.Value {
/// A type representing the stable identity of this atom.
public struct Key: Hashable {
private let atomKey: Node.Key
Expand Down Expand Up @@ -29,16 +29,8 @@ public struct ModifiedAtom<Node: Atom, Modifier: AtomModifier>: Atom where Node.
Key(atomKey: atom.key, modifierKey: modifier.key)
}

/// Creates a new state that is an actual implementation of this atom.
///
/// - Returns: A state object that handles internal process and a value.
public func makeState() -> ModifiedAtomState<Node, Modifier> {
State(atom: atom, modifier: modifier)
}

/// Returns a boolean value that determines whether it should notify the value update to
/// watchers with comparing the given old value and the new value.
public func shouldNotifyUpdate(newValue: Modifier.ModifiedValue, oldValue: Modifier.ModifiedValue) -> Bool {
modifier.shouldNotifyUpdate(newValue: newValue, oldValue: oldValue)
/// A loader that represents an actual implementation of this atom.
public var _loader: ModifiedAtomLoader<Node, Modifier> {
Loader(atom: atom, modifier: modifier)
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/ObservableObjectAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import Combine
/// }
/// ```
///
public protocol ObservableObjectAtom: Atom where State == ObservableObjectAtomState<ObjectType> {
public protocol ObservableObjectAtom: Atom where Loader == ObservableObjectAtomLoader<ObjectType> {
/// The type of observable object that this atom produces.
associatedtype ObjectType: ObservableObject

Expand All @@ -67,7 +67,7 @@ public protocol ObservableObjectAtom: Atom where State == ObservableObjectAtomSt

public extension ObservableObjectAtom {
@MainActor
func makeState() -> State {
State(makeObject: object)
var _loader: Loader {
Loader(makeObject: object)
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/PublisherAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import Combine
/// }
/// ```
///
public protocol PublisherAtom: Atom where State == PublisherAtomState<Publisher> {
public protocol PublisherAtom: Atom where Loader == PublisherAtomLoader<Publisher> {
/// The type of publisher that this atom manages.
associatedtype Publisher: Combine.Publisher

Expand All @@ -57,7 +57,7 @@ public protocol PublisherAtom: Atom where State == PublisherAtomState<Publisher>

public extension PublisherAtom {
@MainActor
func makeState() -> State {
State(makePublisher: publisher)
var _loader: Loader {
Loader(makePublisher: publisher)
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/StateAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/// }
/// ```
///
public protocol StateAtom: Atom where State == StateAtomState<Value> {
public protocol StateAtom: Atom where Loader == ValueAtomLoader<Value> {
/// The type of state value that this atom produces.
associatedtype Value

Expand Down Expand Up @@ -77,8 +77,8 @@ public protocol StateAtom: Atom where State == StateAtomState<Value> {

public extension StateAtom {
@MainActor
func makeState() -> State {
State(getDefaultValue: defaultValue)
var _loader: Loader {
Loader(getValue: defaultValue)
}

@MainActor
Expand Down
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/TaskAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
/// }
/// ```
///
public protocol TaskAtom: Atom where State == TaskAtomState<Value> {
public protocol TaskAtom: Atom where Loader == TaskAtomLoader<Value> {
/// The type of value that this atom produces.
associatedtype Value

Expand All @@ -53,7 +53,7 @@ public protocol TaskAtom: Atom where State == TaskAtomState<Value> {

public extension TaskAtom {
@MainActor
func makeState() -> State {
State(getValue: value)
var _loader: Loader {
Loader(getValue: value)
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/ThrowingTaskAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
/// }
/// ```
///
public protocol ThrowingTaskAtom: Atom where State == ThrowingTaskState<Value> {
public protocol ThrowingTaskAtom: Atom where Loader == ThrowingTaskAtomLoader<Value> {
/// The type of value that this atom produces.
associatedtype Value

Expand All @@ -57,7 +57,7 @@ public protocol ThrowingTaskAtom: Atom where State == ThrowingTaskState<Value> {

public extension ThrowingTaskAtom {
@MainActor
func makeState() -> State {
State(getValue: value)
var _loader: Loader {
Loader(getValue: value)
}
}
6 changes: 3 additions & 3 deletions Sources/Atoms/Atom/ValueAtom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
/// }
/// ```
///
public protocol ValueAtom: Atom where State == ValueAtomState<Value> {
public protocol ValueAtom: Atom where Loader == ValueAtomLoader<Value> {
/// The type of value that this atom produces.
associatedtype Value

Expand All @@ -48,7 +48,7 @@ public protocol ValueAtom: Atom where State == ValueAtomState<Value> {

public extension ValueAtom {
@MainActor
func makeState() -> State {
State(getValue: value)
var _loader: Loader {
Loader(getValue: value)
}
}
9 changes: 3 additions & 6 deletions Sources/Atoms/AtomRelay.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public struct AtomRelay<Content: View>: View {
private let content: Content
private var observers = [AtomObserver]()

@Environment(\.atomStore)
@Environment(\.store)
private var inheritedStore

/// Creates an atom relay with the specified content that will be allowed to use atoms by
Expand All @@ -60,11 +60,8 @@ public struct AtomRelay<Content: View>: View {
/// The content and behavior of the view.
public var body: some View {
content.environment(
\.atomStore,
Store(
parent: context?._store ?? inheritedStore,
observers: observers
)
\.store,
(context?._store ?? inheritedStore).relay(observers: observers)
)
}

Expand Down
Loading