Skip to content

Commit

Permalink
Merge branch 'navigation-beta' into prerelease/1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
stephencelis committed Mar 21, 2023
2 parents 132cb3b + 553214e commit fc17996
Show file tree
Hide file tree
Showing 45 changed files with 542 additions and 298 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swiftui-navigation",
"state" : {
"revision" : "0a0e1b321d70ee6a464ecfe6b0136d9eff77ebfc",
"version" : "0.7.0"
"revision" : "47dd574b900ba5ba679f56ea00d4d282fc7305a6",
"version" : "0.7.1"
}
},
{
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ let package = Package(
.product(name: "IdentifiedCollections", package: "swift-identified-collections"),
.product(name: "OrderedCollections", package: "swift-collections"),
.product(name: "_SwiftUINavigationState", package: "swiftui-navigation"),
// TODO: should we depend on this or copy some stuff over?
.product(name: "SwiftUINavigation", package: "swiftui-navigation"),
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
]
Expand Down
140 changes: 72 additions & 68 deletions Sources/ComposableArchitecture/Effects/Cancellation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ extension EffectPublisher {
/// - Returns: A new effect that is capable of being canceled by an identifier.
public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Self {
@Dependency(\.navigationIDPath) var navigationIDPath

switch self.operation {
case .none:
return .none
Expand All @@ -47,49 +48,37 @@ extension EffectPublisher {
defer { _cancellablesLock.unlock() }

if cancelInFlight {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID]?.forEach { $0.cancel() }
_cancellationCancellables.cancel(id: id, path: navigationIDPath)
}

let cancellationSubject = PassthroughSubject<Void, Never>()

var cancellationCancellable: AnyCancellable!
cancellationCancellable = AnyCancellable {
var cancellable: AnyCancellable!
cancellable = AnyCancellable {
_cancellablesLock.sync {
cancellationSubject.send(())
cancellationSubject.send(completion: .finished)
for navigationIDPath in navigationIDPath.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID]?.remove(cancellationCancellable)
if _cancellationCancellables[cancelID]?.isEmpty == .some(true) {
_cancellationCancellables[cancelID] = nil
}
}
_cancellationCancellables.remove(cancellable, at: id, path: navigationIDPath)
}
}

return publisher.prefix(untilOutputFrom: cancellationSubject)
.handleEvents(
receiveSubscription: { _ in
_cancellablesLock.sync {
for navigationIDPath in navigationIDPath.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID, default: []].insert(
cancellationCancellable
)
}
_cancellationCancellables.insert(cancellable, at: id, path: navigationIDPath)
}
},
receiveCompletion: { _ in cancellationCancellable.cancel() },
receiveCancel: cancellationCancellable.cancel
receiveCompletion: { _ in cancellable.cancel() },
receiveCancel: cancellable.cancel
)
}
.eraseToAnyPublisher()
)
)
case let .run(priority, operation):
return withEscapedDependencies { continuation in
Self(
return Self(
operation: .run(priority) { send in
await continuation.yield {
await withTaskCancellation(id: id, cancelInFlight: cancelInFlight) {
Expand Down Expand Up @@ -124,11 +113,10 @@ extension EffectPublisher {
public static func cancel(id: AnyHashable) -> Self {
let dependencies = DependencyValues._current
@Dependency(\.navigationIDPath) var navigationIDPath
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
return Deferred { () -> Publishers.CompactMap<Result<Action?, Failure>.Publisher, Action> in
DependencyValues.$_current.withValue(dependencies) {
_cancellablesLock.sync {
_cancellationCancellables[cancelID]?.forEach { $0.cancel() }
_cancellationCancellables.cancel(id: id, path: navigationIDPath)
}
}
return Just<Action?>(nil)
Expand Down Expand Up @@ -222,28 +210,19 @@ extension EffectPublisher {
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
@Dependency(\.navigationIDPath) var navigationIDPath

let (cancellable, task) = _cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
if cancelInFlight {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID]?.forEach { $0.cancel() }
_cancellationCancellables.cancel(id: id, path: navigationIDPath)
}
let task = Task { try await operation() }
let cancellable = AnyCancellable { task.cancel() }
for navigationIDPath in navigationIDPath.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID, default: []].insert(cancellable)
}
_cancellationCancellables.insert(cancellable, at: id, path: navigationIDPath)
return (cancellable, task)
}
defer {
_cancellablesLock.sync {
for navigationIDPath in navigationIDPath.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID]?.remove(cancellable)
if _cancellationCancellables[cancelID]?.isEmpty == .some(true) {
_cancellationCancellables[cancelID] = nil
}
}
_cancellationCancellables.remove(cancellable, at: id, path: navigationIDPath)
}
}
do {
Expand All @@ -259,28 +238,19 @@ extension EffectPublisher {
operation: @Sendable @escaping () async throws -> T
) async rethrows -> T {
@Dependency(\.navigationIDPath) var navigationIDPath

let (cancellable, task) = _cancellablesLock.sync { () -> (AnyCancellable, Task<T, Error>) in
if cancelInFlight {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID]?.forEach { $0.cancel() }
_cancellationCancellables.cancel(id: id, path: navigationIDPath)
}
let task = Task { try await operation() }
let cancellable = AnyCancellable { task.cancel() }
for navigationIDPath in navigationIDPath.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID, default: []].insert(cancellable)
}
_cancellationCancellables.insert(cancellable, at: id, path: navigationIDPath)
return (cancellable, task)
}
defer {
_cancellablesLock.sync {
for navigationIDPath in navigationIDPath.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
_cancellationCancellables[cancelID]?.remove(cancellable)
if _cancellationCancellables[cancelID]?.isEmpty == .some(true) {
_cancellationCancellables[cancelID] = nil
}
}
_cancellationCancellables.remove(cancellable, at: id, path: navigationIDPath)
}
}
do {
Expand Down Expand Up @@ -336,9 +306,9 @@ extension Task where Success == Never, Failure == Never {
/// - Parameter id: An identifier.
public static func cancel<ID: Hashable & Sendable>(id: ID) {
@Dependency(\.navigationIDPath) var navigationIDPath
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)

return _cancellablesLock.sync {
_cancellationCancellables[cancelID]?.forEach { $0.cancel() }
_cancellationCancellables.cancel(id: id, path: navigationIDPath)
}
}

Expand All @@ -354,22 +324,19 @@ extension Task where Success == Never, Failure == Never {
}

@_spi(Internals) public struct _CancelID: Hashable {
let discriminator: ObjectIdentifier
let id: AnyHashable
let navigationIDPath: NavigationIDPath

init(id: AnyHashable, navigationIDPath: NavigationIDPath) {
self.discriminator = ObjectIdentifier(type(of: id.base))
self.id = id
self.navigationIDPath = navigationIDPath
}

public init(_id id: AnyHashable) {
self.id = id
self.navigationIDPath = NavigationIDPath()
}
}

@_spi(Internals) public var _cancellationCancellables: [_CancelID: Set<AnyCancellable>] = [:]
@_spi(Internals) public let _cancellablesLock = NSRecursiveLock()
@_spi(Internals) public var _cancellationCancellables = CancellablesCollection()
private let _cancellablesLock = NSRecursiveLock()

@rethrows
private protocol _ErrorMechanism {
Expand All @@ -390,19 +357,56 @@ extension _ErrorMechanism {

extension Result: _ErrorMechanism {}

@_spi(Internals)
public class CancellablesCollection {
var storage: [_CancelID: Set<AnyCancellable>] = [:]

func insert(
_ cancellable: AnyCancellable,
at id: AnyHashable,
path: NavigationIDPath
) {
for navigationIDPath in path.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
self.storage[cancelID, default: []].insert(cancellable)
}
}

/*
func remove(
_ cancellable: AnyCancellable,
at id: AnyHashable,
path: NavigationIDPath
) {
for navigationIDPath in path.prefixes {
let cancelID = _CancelID(id: id, navigationIDPath: navigationIDPath)
self.storage[cancelID]?.remove(cancellable)
if self.storage[cancelID]?.isEmpty == true {
self.storage[cancelID] = nil
}
}
}

[1, 2, 3], TimerID
[1]
func cancel(
id: AnyHashable,
path: NavigationIDPath
) {
let cancelID = _CancelID(id: id, navigationIDPath: path)
self.storage[cancelID]?.forEach { $0.cancel() }
self.storage[cancelID] = nil
}

func exists(
at id: AnyHashable,
path: NavigationIDPath
) -> Bool {
return self.storage[_CancelID(id: id, navigationIDPath: path)] != nil
}

Trie<AnyID, [AnyHashable: Set<AnyCancellable>]>
.insert(navigationID, [:])
.modify(navigationID, default: [:]) {
$0
}
public var count: Int {
return self.storage.count
}

trie[navigationID, default: [:]][id, default: []].insert(cancellable)
trie[navigationID] = nil
*/
public func removeAll() {
self.storage.removeAll()
}
}
11 changes: 10 additions & 1 deletion Sources/ComposableArchitecture/Effects/Publisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@ extension EffectPublisher where Failure == Never {
public static func publisher<P: Publisher>(_ createPublisher: @escaping () -> P) -> Self
where P.Output == Action, P.Failure == Never {
Self(
operation: .publisher(Deferred(createPublisher: createPublisher).eraseToAnyPublisher())
operation: .publisher(
withEscapedDependencies { continuation in
Deferred {
continuation.yield {
createPublisher()
}
}
}
.eraseToAnyPublisher()
)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extension AlertState: _EphemeralState {}
@available(iOS 13, macOS 12, tvOS 13, watchOS 6, *)
extension ConfirmationDialogState: _EphemeralState {}

@usableFromInline
func isEphemeral<State>(_ state: State) -> Bool {
if State.self is _EphemeralState.Type {
return true
Expand Down
1 change: 1 addition & 0 deletions Sources/ComposableArchitecture/Internal/NavigationID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ private enum NavigationIDPathKey: DependencyKey {
struct NavigationIDPath: Hashable, Identifiable, Sendable {
fileprivate var path: [NavigationID]

@usableFromInline
init(path: [NavigationID] = []) {
self.path = path
}
Expand Down
Loading

0 comments on commit fc17996

Please sign in to comment.