diff --git a/Sources/FueledUtils/Combine/AnyCurrentValuePublisher.swift b/Sources/FueledUtils/Combine/AnyCurrentValuePublisher.swift index 0dca8656..cd2db9c5 100644 --- a/Sources/FueledUtils/Combine/AnyCurrentValuePublisher.swift +++ b/Sources/FueledUtils/Combine/AnyCurrentValuePublisher.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import Combine /// Use an `AnyCurrentValuePublisher` to wrap an existing current value publisher whose details you don’t want to expose. /// For example, this is useful if you want to use a `CurrentValueSubject` internally, but don't want to expose the setter/its send() method /// -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct AnyCurrentValuePublisher: CurrentValuePublisher { private let valueGetter: () -> Output private let receiveSubscriberClosure: (AnySubscriber) -> Void @@ -44,7 +43,6 @@ public struct AnyCurrentValuePublisher: CurrentVal } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension CurrentValuePublisher { public func eraseToAnyCurrentValuePublisher() -> AnyCurrentValuePublisher { AnyCurrentValuePublisher(self) @@ -54,11 +52,9 @@ extension CurrentValuePublisher { /// /// A publisher that also stores the last value it sent /// -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public protocol CurrentValuePublisher: Publisher { var value: Output { get } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension CurrentValueSubject: CurrentValuePublisher { } diff --git a/Sources/FueledUtils/Combine/ObservableObjectExtensions.swift b/Sources/FueledUtils/Combine/Extensions/ObservableObject+Extensions.swift similarity index 93% rename from Sources/FueledUtils/Combine/ObservableObjectExtensions.swift rename to Sources/FueledUtils/Combine/Extensions/ObservableObject+Extensions.swift index a29ad4ca..5eaa9206 100644 --- a/Sources/FueledUtils/Combine/ObservableObjectExtensions.swift +++ b/Sources/FueledUtils/Combine/Extensions/ObservableObject+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ import Combine import Foundation import FueledUtilsCore -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObjectPublisher { // Perform a one-way link, where the receiver will listen for changes on the object and automatically trigger its `objectWillChange` publisher public func link(to object: Object) { @@ -77,7 +76,6 @@ extension ObservableObject where Self.ObjectWillChangePublisher == ObservableObj } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension ObservableObject { public var objectDidChange: AnyPublisher { // The delay of 0.0 allows the will to transform into a Did, by waiting for exactly one run loop cycle @@ -89,7 +87,6 @@ extension ObservableObject { } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Publisher where Output: Collection, Failure == Never, Output.Element: ObservableObject { public func onAnyChanges() -> AnyPublisher<[Output.Element], Never> { self.flatMap { Publishers.CombineLatestMany($0.map { $0.publisher }) }.eraseToAnyPublisher() diff --git a/Sources/FueledUtils/Combine/Extensions/Publisher+CombinePrevious.swift b/Sources/FueledUtils/Combine/Extensions/Publisher+CombinePrevious.swift new file mode 100644 index 00000000..246c5e40 --- /dev/null +++ b/Sources/FueledUtils/Combine/Extensions/Publisher+CombinePrevious.swift @@ -0,0 +1,84 @@ +// Copyright © 2024 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Combine + + +public extension Publisher { + /// A tuple type that holds the previous and current output values of a publisher. + typealias CombinePreviousOutput = (previous: Output, current: Output) + + /// + /// Combines the previous and current output values of the publisher, starting with a nil initial value. + /// + /// This method emits a tuple containing the previous and current values + /// whenever the publisher emits a new value. If no previous value exists, + /// the publisher will complete without emitting any output. + /// + /// - Returns: An `AnyPublisher` that emits + /// tuples of previous and current output values. + /// + func combinePrevious() -> AnyPublisher { + combinePreviousImplementation(nil) + } + + /// + /// Combines the previous and current output values of the publisher, starting with a specified initial value. + /// + /// This method allows you to track values from the very beginning, + /// emitting a tuple of previous and current values whenever the publisher emits a new value. + /// + /// - Parameter initial: The initial value to use as the previous output + /// for the first emission. + /// - Returns: An `AnyPublisher` that emits + /// tuples of previous and current output values. + /// + func combinePrevious(_ initial: Output) -> AnyPublisher { + combinePreviousImplementation(initial) + } +} + +private extension Publisher { + /// + /// The implementation for combining previous and current output values. + /// + /// This method uses the `scan` operator to keep track of the previous + /// and current values, returning a publisher that emits a tuple of both + /// values whenever a new value is emitted. If the initial value is nil + /// and there is no previous value, the publisher completes without emitting. + /// + /// - Parameter initial: The initial value to use as the previous output. + /// - Returns: An `AnyPublisher` that emits + /// tuples containing the previous and current output values or completes + /// without emitting if there is no previous value available. + /// + func combinePreviousImplementation(_ initial: Output?) -> AnyPublisher { + scan((Output?.none, initial)) { current, newValue in + (current.1, newValue) + } + .map { previous, current -> AnyPublisher in + if let previous { + let output = CombinePreviousOutput(previous, current!) + return Just(output) + .setFailureType(to: Failure.self) + .eraseToAnyPublisher() + } else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + } + .switchToLatest() + .eraseToAnyPublisher() + } +} diff --git a/Sources/FueledUtils/Combine/Extensions/Publisher+Extensions.swift b/Sources/FueledUtils/Combine/Extensions/Publisher+Extensions.swift new file mode 100644 index 00000000..a88198e5 --- /dev/null +++ b/Sources/FueledUtils/Combine/Extensions/Publisher+Extensions.swift @@ -0,0 +1,293 @@ +// Copyright © 2024 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Combine +import FueledUtilsCore + +// MARK: - Helpers Functions +public extension Publisher { + /// + /// Ignores all errors received from this publisher. + /// This function will catch any error from an upstream publisher and replace it with an `Empty` publisher. + /// + func ignoreErrors() -> AnyPublisher { + self.catch { _ in + Empty() + } + .eraseToAnyPublisher() + } + + /// + /// Transforms the output of the publisher into an optional value. + /// This function maps each emitted value to an `Optional`, effectively wrapping the output in an optional. + /// If the original publisher emits a value, it will be wrapped in `Optional.some(value)`. + /// + func promoteOptional() -> AnyPublisher { + map { value in + Optional.some(value) + } + .eraseToAnyPublisher() + } +} + +// MARK: - Sink +public extension Publisher { + /// + /// Creates a subscriber that receives values from the publisher, + /// but does not take any action on the emitted values or completion events. + /// The `receiveCompletion` closure is provided but does not perform any operations + /// when the publisher completes, and the `receiveValue` closure ignores + /// the emitted values entirely. + /// + /// Returns an `AnyCancellable` that can be used to cancel the subscription. + /// + func sink() -> AnyCancellable { + sink( + receiveCompletion: { _ in }, + receiveValue: { _ in } + ) + } + + /// + /// Subscribes to the publisher and stores the cancellable in the specified object's cancellables collection. + /// This function sets up a subscription that lasts for the lifetime of the provided object. + /// The cancellable is stored in `object.combineExtensions.cancellables`, allowing the subscription + /// to be automatically cancelled when the object is deallocated. + /// + /// - Parameter object: An instance conforming to `CombineExtensionsProvider` that provides a storage + /// for cancellable subscriptions. + /// + func sinkForLifetimeOf(_ object: Object) { + sink() + .store(in: &object.combineExtensions.cancellables) + } + + /// + /// Subscribes to the publisher and forwards emitted values to the specified closure while managing the subscription's lifetime. + /// This function sets up a subscription that calls the provided `receiveValue` closure whenever the publisher emits a value. + /// The cancellable is stored in `object.combineExtensions.cancellables`, ensuring that the subscription + /// is automatically cancelled when the provided object is deallocated. + /// + /// - Parameter object: An instance conforming to `CombineExtensionsProvider` that provides a storage + /// for cancellable subscriptions. + /// - Parameter receiveValue: A closure that is called with each value emitted by the publisher. + /// + /// This function is limited to publishers that cannot fail (`Failure == Never`). + /// + func sinkForLifetimeOf( + _ object: Object, + receiveValue: @escaping (Self.Output) -> Void + ) where Failure == Never { + sink(receiveValue: receiveValue) + .store(in: &object.combineExtensions.cancellables) + } + + /// + /// Subscribes to the publisher and forwards emitted values and completion events to the specified closures while managing the subscription's lifetime. + /// This function sets up a subscription that calls the provided `receiveValue` closure whenever the publisher emits a value, + /// and the `receiveCompletion` closure when the publisher finishes or encounters an error. + /// The cancellable is stored in `object.combineExtensions.cancellables`, ensuring that the subscription + /// is automatically cancelled when the provided object is deallocated. + /// + /// - Parameter object: An instance conforming to `CombineExtensionsProvider` that provides a storage + /// for cancellable subscriptions. + /// - Parameter receiveCompletion: A closure that is called with the completion event of the publisher, + /// which can either be `.finished` or `.failure(error)`. + /// - Parameter receiveValue: A closure that is called with each value emitted by the publisher. + /// + func sinkForLifetimeOf( + _ object: Object, + receiveCompletion: @escaping (Subscribers.Completion) -> Void, + receiveValue: @escaping ((Self.Output) -> Void) + ) { + sink( + receiveCompletion: receiveCompletion, + receiveValue: receiveValue + ) + .store(in: &object.combineExtensions.cancellables) + } +} + +// MARK: - Then +public extension Publisher { + /// + /// Subscribes to the publisher and forwards success or failure events to the specified closure. + /// This function sets up a subscription that calls the provided `receiveResult` closure + /// with a `Result` whenever the publisher emits a value or encounters an error. + /// + /// - Parameter receiveResult: A closure that is called with the result of the publisher's output. + /// It receives a `.success(value)` if the publisher emits a value, or a `.failure(error)` if the publisher fails. + /// + /// Returns an `AnyCancellable` that can be used to cancel the subscription. + /// + func then(receiveResult: @escaping (Result) -> Void) -> AnyCancellable { + sink( + receiveCompletion: { completion in + if case .failure(let error) = completion { + receiveResult(.failure(error)) + } + }, + receiveValue: { value in + receiveResult(.success(value)) + } + ) + } + + /// + /// Subscribes to the publisher and forwards success or failure events to the specified closure while managing the subscription's lifetime. + /// This function sets up a subscription that calls the provided `receiveResult` closure with a `Result` + /// whenever the publisher emits a value or encounters an error. + /// The cancellable is stored in `object.combineExtensions.cancellables`, ensuring that the subscription + /// is automatically cancelled when the provided object is deallocated. + /// + /// - Parameter object: An instance conforming to `CombineExtensionsProvider` that provides a storage + /// for cancellable subscriptions. + /// - Parameter receiveResult: A closure that is called with the result of the publisher's output. + /// It receives a `.success(value)` if the publisher emits a value, or a `.failure(error)` if the publisher fails. + /// + func thenForLifetimeOf( + _ object: Object, + receiveResult: @escaping (Result) -> Void + ) { + then(receiveResult: receiveResult) + .store(in: &object.combineExtensions.cancellables) + } +} + +// MARK: - Perform During Life Time +public extension Publisher { + /// + /// Subscribes to the publisher and performs a specified action with the emitted values during the lifetime of the provided object. + /// This function ignores any errors emitted by the publisher, and for each value emitted, it calls the provided `action` + /// closure with the object and the emitted value. The subscription is automatically cancelled when the provided object is deallocated. + /// + /// - Parameter object: An instance conforming to both `CombineExtensionsProvider` and `AnyObject`, + /// which provides storage for cancellable subscriptions and allows for weak references. + /// - Parameter action: A closure that is called with the object and each emitted value from the publisher. + /// + func performDuringLifetimeOf( + _ object: Object, + action: @escaping (Object, Output) -> Void + ) { + ignoreErrors() + .sinkForLifetimeOf(object) { [weak object] value in + guard let object else { + return + } + action(object, value) + } + } + + /// + /// Subscribes to the publisher and performs a specified action with the emitted values during the lifetime of the provided object. + /// This function allows the action to be a curried closure, meaning it can first take the object as an argument + /// and return a closure that takes the emitted value as its argument. + /// The subscription is automatically cancelled when the provided object is deallocated. + /// + /// - Parameter object: An instance conforming to both `CombineExtensionsProvider` and `AnyObject`, + /// which provides storage for cancellable subscriptions and allows for weak references. + /// - Parameter action: A curried closure that first takes the object and returns another closure, + /// which is then called with each emitted value from the publisher. + /// + func performDuringLifetimeOf( + _ object: Object, + action: @escaping (Object) -> (Output) -> Void + ) { + performDuringLifetimeOf(object) { object, output in + action(object)(output) + } + } +} + +// MARK: - Assign +public extension Publisher where Failure == Never { + /// + /// Subscribes to the publisher and assigns emitted values to the specified key path of the provided object + /// without retaining the object strongly. This function uses a weak reference to the object, ensuring that + /// it does not extend the lifetime of the object unnecessarily. + /// + /// When a new value is emitted by the publisher, it is assigned to the property specified by the given + /// `keyPath` on the object, as long as the object still exists. + /// + /// - Parameter keyPath: A reference writable key path that specifies the property of the object + /// to which the emitted values will be assigned. + /// - Parameter object: An instance of the object to which the emitted values will be assigned. + /// This object is captured weakly to prevent strong reference cycles. + /// + /// - Returns: An `AnyCancellable` that represents the ongoing subscription, allowing it to be cancelled + /// when no longer needed. + /// + func assign( + to keyPath: ReferenceWritableKeyPath, + withoutRetaining object: Object + ) -> AnyCancellable { + sink { [weak object] in + object?[keyPath: keyPath] = $0 + } + } + + /// + /// Subscribes to the publisher and assigns emitted values to the specified key path of the provided object + /// for the lifetime of the object. This function uses a weak reference to the object to avoid strong + /// reference cycles while ensuring that the emitted values are assigned to the property specified by the + /// given `keyPath` as long as the object exists. The subscription is automatically cancelled when the + /// object is deallocated, preventing memory leaks. + /// + /// - Parameter keyPath: A reference writable key path that specifies the property of the object + /// to which the emitted values will be assigned. + /// - Parameter object: An instance of the object to which the emitted values will be assigned. + /// This object is captured weakly to prevent strong reference cycles, but its cancellables are stored + /// to maintain the subscription for its lifetime. + /// + /// - Returns: This function does not return a value, as the assignment is performed directly on the object. + /// + func assign( + to keyPath: ReferenceWritableKeyPath, + forLifetimeOf object: Object + ) -> Void { + sink { [weak object] in + object?[keyPath: keyPath] = $0 + } + .store(in: &object.combineExtensions.cancellables) + } +} + +// MARK: - Ignore Nils +public extension Publisher where Output: OptionalProtocol { + /// + /// Transforms an optional publisher into a non-optional publisher by ignoring nil values. + /// If the upstream publisher emits a non-nil value, it wraps that value in a `Just` publisher. + /// If a nil value is emitted, it replaces it with an `Empty` publisher, ensuring that the + /// downstream subscriber only receives non-optional values. + /// + /// This function effectively filters out any nil values from the output stream, allowing only + /// valid (non-nil) values to be processed downstream. It maintains the original failure type + /// of the publisher. + /// + /// - Returns: An `AnyPublisher` that emits non-optional values + /// (if available) or completes without emitting any values if nil is encountered. + /// + func ignoreNils() -> AnyPublisher { + flatMap { optionalPublisher in + let wrappedPublisher = optionalPublisher.wrapped.map { + Just($0).eraseToAnyPublisher() + } + + let finalPublisher = wrappedPublisher ?? Empty().eraseToAnyPublisher() + return finalPublisher + .setFailureType(to: Failure.self) + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/FueledUtils/Combine/Extensions/Publishers+CombineLatestMany.swift b/Sources/FueledUtils/Combine/Extensions/Publishers+CombineLatestMany.swift new file mode 100644 index 00000000..aefbcac7 --- /dev/null +++ b/Sources/FueledUtils/Combine/Extensions/Publishers+CombineLatestMany.swift @@ -0,0 +1,33 @@ +// Copyright © 2024 Fueled Digital Media, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Combine + +extension Publishers { + /// Combine many publishers into a single publisher + static func CombineLatestMany(_ publishers: [AnyPublisher]) -> AnyPublisher<[Output], Failure> { + guard !publishers.isEmpty else { + return Just([]) + .setFailureType(to: Failure.self) + .eraseToAnyPublisher() + } + + let firstPublisherArray = publishers[0].map { [$0] }.eraseToAnyPublisher() + return publishers + .dropFirst() + .reduce(firstPublisherArray) { combined, publisher in + combined.combineLatest(publisher) { $0 + [$1] }.eraseToAnyPublisher() + } + } +} diff --git a/Sources/FueledUtils/Combine/Subject+SendResult.swift b/Sources/FueledUtils/Combine/Extensions/Subject+SendResult.swift similarity index 77% rename from Sources/FueledUtils/Combine/Subject+SendResult.swift rename to Sources/FueledUtils/Combine/Extensions/Subject+SendResult.swift index 66e0fa7b..f6033b6b 100644 --- a/Sources/FueledUtils/Combine/Subject+SendResult.swift +++ b/Sources/FueledUtils/Combine/Extensions/Subject+SendResult.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,15 +14,14 @@ import Combine -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Subject { public func send(result: Result) { switch result { case .failure(let error): - self.send(completion: .failure(error)) + send(completion: .failure(error)) case .success(let value): - self.send(value) - self.send(completion: .finished) + send(value) + send(completion: .finished) } } } diff --git a/Sources/FueledUtils/Combine/Subscriber+EraseToAnySubscriber.swift b/Sources/FueledUtils/Combine/Extensions/Subscriber+EraseToAnySubscriber.swift similarity index 86% rename from Sources/FueledUtils/Combine/Subscriber+EraseToAnySubscriber.swift rename to Sources/FueledUtils/Combine/Extensions/Subscriber+EraseToAnySubscriber.swift index 1873ee92..2f37a07a 100644 --- a/Sources/FueledUtils/Combine/Subscriber+EraseToAnySubscriber.swift +++ b/Sources/FueledUtils/Combine/Extensions/Subscriber+EraseToAnySubscriber.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ import Combine -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension Subscriber { public func eraseToAnySubscriber() -> AnySubscriber { AnySubscriber(self) diff --git a/Sources/FueledUtils/Combine/CombineExtensions+Cancellables.swift b/Sources/FueledUtils/Combine/ExtensionsProvider/CombineExtensions+Cancellables.swift similarity index 68% rename from Sources/FueledUtils/Combine/CombineExtensions+Cancellables.swift rename to Sources/FueledUtils/Combine/ExtensionsProvider/CombineExtensions+Cancellables.swift index 3e0487a0..4444868d 100644 --- a/Sources/FueledUtils/Combine/CombineExtensions+Cancellables.swift +++ b/Sources/FueledUtils/Combine/ExtensionsProvider/CombineExtensions+Cancellables.swift @@ -1,4 +1,4 @@ -// Copyright © 2020 Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,22 +14,20 @@ import Combine import Foundation -import FueledUtilsCore -private var cancellablesKey: UInt8 = 0 +nonisolated(unsafe) private var cancellablesKey: UInt8 = 0 -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension CombineExtensions { public var cancellables: Set { get { - self.cancellablesHelper.map { Set($0.map(\.cancellable)) } ?? { + cancellablesHelper.map { Set($0.map(\.cancellable)) } ?? { let cancellables = Set() self.cancellables = cancellables return cancellables }() } set { - self.cancellablesHelper = newValue.map { CancellableHolder($0) } + cancellablesHelper = newValue.map(CancellableHolder.init) } } @@ -43,10 +41,10 @@ extension CombineExtensions { private var cancellablesHelper: [CancellableHolder]? { get { - objc_getAssociatedObject(self.base, &cancellablesKey) as? [CancellableHolder] + objc_getAssociatedObject(base, &cancellablesKey) as? [CancellableHolder] } set { - objc_setAssociatedObject(self.base, &cancellablesKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY) + objc_setAssociatedObject(base, &cancellablesKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY) } } } diff --git a/Sources/FueledUtils/Combine/CombineExtensions.swift b/Sources/FueledUtils/Combine/ExtensionsProvider/CombineExtensions.swift similarity index 74% rename from Sources/FueledUtils/Combine/CombineExtensions.swift rename to Sources/FueledUtils/Combine/ExtensionsProvider/CombineExtensions.swift index 4bcc183e..e271c294 100644 --- a/Sources/FueledUtils/Combine/CombineExtensions.swift +++ b/Sources/FueledUtils/Combine/ExtensionsProvider/CombineExtensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020 Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,22 +14,19 @@ import Foundation -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public protocol CombineExtensionsProvider { } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public final class CombineExtensions { - public var base: Base + public let base: Base fileprivate init(_ base: Base) { self.base = base } } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension CombineExtensionsProvider { public var combineExtensions: CombineExtensions { - return CombineExtensions(self) + CombineExtensions(self) } } diff --git a/Sources/FueledUtils/Combine/NSObject+CombineExtensions.swift b/Sources/FueledUtils/Combine/ExtensionsProvider/NSObject+CombineExtensions.swift similarity index 91% rename from Sources/FueledUtils/Combine/NSObject+CombineExtensions.swift rename to Sources/FueledUtils/Combine/ExtensionsProvider/NSObject+CombineExtensions.swift index 2ab14c14..fbf89850 100644 --- a/Sources/FueledUtils/Combine/NSObject+CombineExtensions.swift +++ b/Sources/FueledUtils/Combine/ExtensionsProvider/NSObject+CombineExtensions.swift @@ -14,6 +14,5 @@ import Foundation -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) extension NSObject: CombineExtensionsProvider { } diff --git a/Sources/FueledUtils/Combine/CombineOperators/Combine+Operators.swift b/Sources/FueledUtils/Combine/Operators/Combine+Operators.swift similarity index 83% rename from Sources/FueledUtils/Combine/CombineOperators/Combine+Operators.swift rename to Sources/FueledUtils/Combine/Operators/Combine+Operators.swift index 5f5952ba..fecc38ae 100644 --- a/Sources/FueledUtils/Combine/CombineOperators/Combine+Operators.swift +++ b/Sources/FueledUtils/Combine/Operators/Combine+Operators.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,18 +43,15 @@ precedencegroup InsertCancellablePrecedence { infix operator >>>: InsertCancellablePrecedence -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct ObjectKeyPathReference { public let object: Root public let keyPath: ReferenceWritableKeyPath } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func ~ (lhs: Object, rhs: ReferenceWritableKeyPath) -> ObjectKeyPathReference { ObjectKeyPathReference(object: lhs, keyPath: rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func <~ ( lhs: ObjectKeyPathReference, rhs: Publisher @@ -62,7 +59,6 @@ public func <~ ( rhs.assign(to: lhs.keyPath, withoutRetaining: lhs.object) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func <~ ( lhs: ObservingObject, rhs: ObservedObject @@ -70,7 +66,6 @@ public func <~ ( lhs: ObservingObject, rhs: ObservedObjectCollection @@ -78,7 +73,6 @@ public func <~ ( lhs: ObservingObject, rhs: Publisher @@ -86,7 +80,6 @@ public func <~ lhs.link(to: rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func <~ ( lhs: ObservingObject, rhs: Publisher @@ -94,7 +87,6 @@ public func <~ lhs.link(to: rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func <~ ( lhs: ObservingObject, rhs: Publisher @@ -102,7 +94,6 @@ public func <~ lhs.link(to: rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func <~ ( lhs: ObservingObject, rhs: ReferenceWritableKeyPath @@ -110,7 +101,6 @@ public func <~ ( lhs: ObservingObject, rhs: ReferenceWritableKeyPath @@ -118,12 +108,10 @@ public func <~ >> (lhs: AnyCancellable, rhs: inout CancellableCollection) where CancellableCollection.Element == AnyCancellable { lhs.store(in: &rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable, rhs: inout Set) { lhs.store(in: &rhs) } diff --git a/Sources/FueledUtils/Combine/CombineOperators/CombineOperators+Optional.swift b/Sources/FueledUtils/Combine/Operators/CombineOperators+Optional.swift similarity index 78% rename from Sources/FueledUtils/Combine/CombineOperators/CombineOperators+Optional.swift rename to Sources/FueledUtils/Combine/Operators/CombineOperators+Optional.swift index b1a053be..e847c625 100644 --- a/Sources/FueledUtils/Combine/CombineOperators/CombineOperators+Optional.swift +++ b/Sources/FueledUtils/Combine/Operators/CombineOperators+Optional.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,27 +14,22 @@ import Combine -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable?, rhs: inout CancellableCollection) where CancellableCollection.Element == AnyCancellable { lhs?.store(in: &rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable?, rhs: inout Set) { lhs?.store(in: &rhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable, rhs: inout CancellableCollection?) where CancellableCollection.Element == AnyCancellable { rhs?.append(lhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable, rhs: inout Set?) { rhs?.insert(lhs) } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable?, rhs: inout CancellableCollection?) where CancellableCollection.Element == AnyCancellable { guard let lhs = lhs else { return @@ -42,7 +37,6 @@ public func >>> (lhs: AnyCanc lhs >>> rhs } -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public func >>> (lhs: AnyCancellable?, rhs: inout Set?) { guard let lhs = lhs else { return diff --git a/Sources/FueledUtils/Combine/Published+PublisherInit.swift b/Sources/FueledUtils/Combine/Published+PublisherInit.swift index b3c45579..d32dd335 100644 --- a/Sources/FueledUtils/Combine/Published+PublisherInit.swift +++ b/Sources/FueledUtils/Combine/Published+PublisherInit.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ import Combine -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + extension Published { /// This method exists as it's currently impossible to use the projectedValue of a `@Published` property without having initialize the whole object, /// which is obviously not ideal if the projectedValue is required to initialize the whole object (basically a chicken and egg problem) diff --git a/Sources/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift b/Sources/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift index 36826b9b..fd2f037f 100644 --- a/Sources/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift +++ b/Sources/FueledUtils/Combine/Publisher+AdditionalHandleEvents.swift @@ -7,15 +7,14 @@ import Combine -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publisher { +public extension Publisher { /// - Parameters: /// - receiveTermination: Sent when the publisher either a completion event or is cancelled. /// - receiveResult: Sent when the publisher send values, or an error. /// - Please refer to the documentation for /// `Publisher.self.handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)` /// for more information about the other parameters. - public func handleEvents( + func handleEvents( receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, @@ -24,7 +23,7 @@ extension Publisher { receiveResult: ((Result) -> Void)? = nil, receiveRequest: ((Subscribers.Demand) -> Void)? = nil ) -> Publishers.HandleEvents { - self.extendedHandleEvents( + extendedHandleEvents( receiveSubscription: receiveSubscription, receiveOutput: receiveOutput, receiveCompletion: receiveCompletion, @@ -41,7 +40,7 @@ extension Publisher { /// - Please refer to the documentation for /// `Publisher.self.handleEvents(receiveSubscription:receiveOutput:receiveCompletion:receiveCancel:receiveRequest:)` /// for more information about the other parameters. - public func handleEvents( + func handleEvents( receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, @@ -60,8 +59,10 @@ extension Publisher { receiveRequest: receiveRequest ) } +} - private func extendedHandleEvents( +private extension Publisher { + func extendedHandleEvents( receiveSubscription: ((Subscription) -> Void)? = nil, receiveOutput: ((Output) -> Void)? = nil, receiveCompletion: ((Subscribers.Completion) -> Void)? = nil, @@ -80,7 +81,7 @@ extension Publisher { receiveTermination() } } - return self.handleEvents( + return handleEvents( receiveSubscription: receiveSubscription, receiveOutput: { receiveOutput?($0) diff --git a/Sources/FueledUtils/Combine/Publisher+CombinePrevious.swift b/Sources/FueledUtils/Combine/Publisher+CombinePrevious.swift deleted file mode 100644 index 315e10c3..00000000 --- a/Sources/FueledUtils/Combine/Publisher+CombinePrevious.swift +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright © 2020, Fueled Digital Media, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Combine - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publisher { - public func combinePrevious() -> AnyPublisher<(previous: Output, current: Output), Failure> { - self.combinePreviousImplementation(nil) - } - - public func combinePrevious(_ initial: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> { - self.combinePreviousImplementation(initial) - } - - private func combinePreviousImplementation(_ initial: Output?) -> AnyPublisher<(previous: Output, current: Output), Failure> { - return self - .scan((Output?.none, initial)) { current, newValue in - (current.1, newValue) - } - .map { previous, current -> AnyPublisher<(previous: Output, current: Output), Failure> in - if let previous = previous { - return Just((previous, current!)) - .setFailureType(to: Failure.self) - .eraseToAnyPublisher() - } else { - return Empty(completeImmediately: false).eraseToAnyPublisher() - } - } - .switchToLatest() - .eraseToAnyPublisher() - } -} diff --git a/Sources/FueledUtils/Combine/PublisherExtensions.swift b/Sources/FueledUtils/Combine/PublisherExtensions.swift deleted file mode 100644 index 60421739..00000000 --- a/Sources/FueledUtils/Combine/PublisherExtensions.swift +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright © 2020, Fueled Digital Media, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Combine -import FueledUtilsCore - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publisher { - public func ignoreError() -> AnyPublisher { - self.catch { _ in Empty() }.eraseToAnyPublisher() - } - - public func promoteOptional() -> AnyPublisher { - self.map { Optional.some($0) }.eraseToAnyPublisher() - } - - public func sink() -> AnyCancellable { - self.sink(receiveCompletion: { _ in }, receiveValue: { _ in }) - } - - public func then(receiveResult: @escaping ((Result) -> Void)) -> AnyCancellable { - self.sink( - receiveCompletion: { completion in - if case .failure(let error) = completion { - receiveResult(.failure(error)) - } - }, - receiveValue: { value in - receiveResult(.success(value)) - } - ) - } - - public func sinkForLifetimeOf(_ object: Object) { - self.sink() - .store(in: &object.combineExtensions.cancellables) - } - - public func sinkForLifetimeOf(_ object: Object, receiveValue: @escaping ((Self.Output) -> Void)) where Failure == Never { - self.sink(receiveValue: receiveValue) - .store(in: &object.combineExtensions.cancellables) - } - - public func sinkForLifetimeOf(_ object: Object, receiveCompletion: @escaping ((Subscribers.Completion) -> Void), receiveValue: @escaping ((Self.Output) -> Void)) { - self.sink(receiveCompletion: receiveCompletion, receiveValue: receiveValue) - .store(in: &object.combineExtensions.cancellables) - } - - public func thenForLifetimeOf(_ object: Object, receiveResult: @escaping ((Result) -> Void)) { - self.then(receiveResult: receiveResult) - .store(in: &object.combineExtensions.cancellables) - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publisher { - public func performDuringLifetimeOf(_ object: Object, action: @escaping (Object, Output) -> Void) { - self - .ignoreError() - .sinkForLifetimeOf(object) { [weak object] value in - guard let object = object else { - return - } - action(object, value) - } - } - - public func performDuringLifetimeOf(_ object: Object, action: @escaping (Object) -> (Output) -> Void) { - self.performDuringLifetimeOf(object) { object, output in - action(object)(output) - } - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publisher where Failure == Never { - public func assign(to keyPath: ReferenceWritableKeyPath, withoutRetaining object: Object) -> AnyCancellable { - self.sink { [weak object] in - object?[keyPath: keyPath] = $0 - } - } - - public func assign(to keyPath: ReferenceWritableKeyPath, forLifetimeOf object: Object) -> Void { - self.sink { [weak object] in - object?[keyPath: keyPath] = $0 - } - .store(in: &object.combineExtensions.cancellables) - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publisher where Output: OptionalProtocol { - public func ignoreNil() -> AnyPublisher { - self.flatMap { ($0.wrapped.map { Just($0).eraseToAnyPublisher() } ?? Empty().eraseToAnyPublisher()).setFailureType(to: Failure.self) }.eraseToAnyPublisher() - } -} diff --git a/Sources/FueledUtils/Combine/PublishersExtensions.swift b/Sources/FueledUtils/Combine/PublishersExtensions.swift deleted file mode 100644 index 2d397034..00000000 --- a/Sources/FueledUtils/Combine/PublishersExtensions.swift +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright © 2020, Fueled Digital Media, LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Combine -import FueledUtilsCore - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -extension Publishers { - public struct CombineLatestMany: Publisher where PublisherCollection.Element: Combine.Publisher { - public typealias Output = [PublisherCollection.Element.Output] - - public typealias Failure = PublisherCollection.Element.Failure - - public let publishers: PublisherCollection - - public init(_ publishers: PublisherCollection) { - self.publishers = publishers - } - - public func receive(subscriber: Subscriber) where PublisherCollection.Element.Failure == Subscriber.Failure, Subscriber.Input == Output { - let subscription = CombineLatestManySubscription(subscriber: subscriber, publishers: self.publishers) - subscription.startReceiving() - } - } -} - -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) -private final class CombineLatestManySubscription< - Subscriber: Combine.Subscriber, - PublisherCollection: Swift.Collection ->: Subscription where - PublisherCollection.Element: Combine.Publisher, - PublisherCollection.Element.Failure == Subscriber.Failure, - Subscriber.Input == [PublisherCollection.Element.Output] -{ - @Atomic private var demandsState = DemandsState() - private let subscriber: Subscriber - private let publishers: PublisherCollection - - private struct DemandsState { - var currentDemand: Subscribers.Demand! - var pendingValuesBuffer: [Subscriber.Input] = [] - var cancellables: [AnyCancellable] = [] - - mutating func cancel() { - self.currentDemand = Subscribers.Demand.none - self.pendingValuesBuffer = [] - self.cancellables.forEach { $0.cancel() } - } - } - - init(subscriber: Subscriber, publishers: PublisherCollection) { - self.subscriber = subscriber - self.publishers = publishers - } - - func startReceiving() { - self.subscriber.receive(subscription: self) - } - - func request(_ demand: Subscribers.Demand) { - if self.publishers.isEmpty { - if demand > 0 { - _ = self.subscriber.receive([]) - } - self.subscriber.receive(completion: .finished) - return - } - - func sendPendingValuesIfPossible(demand: Subscribers.Demand, demandsState: inout DemandsState) -> Int? { - if demandsState.currentDemand != nil { - demandsState.currentDemand += demand - var valuesSent = 0 - while let firstValue = demandsState.pendingValuesBuffer.first, demandsState.currentDemand > 0 { - demandsState.pendingValuesBuffer.removeFirst() - sendValueIfPossible(firstValue, demandsState: &demandsState) - valuesSent += 1 - } - return valuesSent - } - return nil - } - - func sendValueIfPossible(_ value: Subscriber.Input, demandsState: inout DemandsState) { - if demandsState.currentDemand == 0 { - demandsState.pendingValuesBuffer.append(value) - return - } - - let demandedValues = self.subscriber.receive(value) - let sentValues = sendPendingValuesIfPossible( - demand: demandedValues, - demandsState: &demandsState - ) ?? 0 - let remainingDemand = demandedValues - sentValues - demandsState.currentDemand += remainingDemand - demandsState.currentDemand -= 1 - } - - let shouldReturn = self.$demandsState.modify { demandsState -> Bool in - let sentValues = sendPendingValuesIfPossible(demand: demand, demandsState: &demandsState) - demandsState.currentDemand = demand - return sentValues != nil - } - - if shouldReturn { - return - } - - let publishers = Array(self.publishers) - let cancellables = AtomicValue( - [ - ( - cancellable: AnyCancellable?, - latestValue: PublisherCollection.Element.Output?, - hasCompleted: Bool - ), - ](repeating: (nil, nil, false), count: self.publishers.count) - ) - publishers.enumerated().forEach { index, publisher in - let cancellable = publisher.sink( - receiveCompletion: { completion in - cancellables.modify { - switch completion { - case .failure(let error): - self.subscriber.receive(completion: .failure(error)) - for i in $0.indices { - $0[i].cancellable = nil - } - case .finished: - $0[index].cancellable = nil - $0[index].hasCompleted = true - if $0.allSatisfy({ $0.hasCompleted }) { - self.subscriber.receive(completion: .finished) - } - } - } - }, - receiveValue: { value in - cancellables.modify { - $0[index].latestValue = value - let allLatestValues = $0.compactMap { $0.latestValue } - if allLatestValues.count == publishers.count { - self.$demandsState.modify { - sendValueIfPossible(allLatestValues, demandsState: &$0) - } - } - } - } - ) - cancellables.modify { cancellables in - if !cancellables[index].hasCompleted { - cancellables[index].cancellable = cancellable - } - self.$demandsState.modify { - $0.cancellables = cancellables.compactMap { $0.cancellable } - } - } - } - } - - func cancel() { - self.$demandsState.modify { $0.cancel() } - } -} diff --git a/Sources/FueledUtils/Core/AnyIdentifiable.swift b/Sources/FueledUtils/Core/AnyIdentifiable.swift index f36516b1..08c37f22 100644 --- a/Sources/FueledUtils/Core/AnyIdentifiable.swift +++ b/Sources/FueledUtils/Core/AnyIdentifiable.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ /// /// A type-erased `Identifiable` object. /// -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) struct AnyIdentifiable: Identifiable { private let hashValueClosure: () -> AnyHashable diff --git a/Sources/FueledUtils/Core/CollectionExtensions.swift b/Sources/FueledUtils/Core/Extensions/Collection+Extensions.swift similarity index 58% rename from Sources/FueledUtils/Core/CollectionExtensions.swift rename to Sources/FueledUtils/Core/Extensions/Collection+Extensions.swift index cc1018de..345d0416 100644 --- a/Sources/FueledUtils/Core/CollectionExtensions.swift +++ b/Sources/FueledUtils/Core/Extensions/Collection+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,34 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -extension Collection { - /// - /// **Unavailable**: Please use `getSafely(at:)` instead. - /// - /// Refer to the documentation for `getSafely(at:)` for more info. - /// - @available(*, unavailable, renamed: "getSafely(at:)") - public func getSafely(_ index: Self.Index) -> Self.Iterator.Element? { - fatalError() - } - +public extension Collection { /// /// Try to get the item at index `index`. If the index is out of bounds, `nil` is returned. /// /// Parameter index: The index of the item to tentatively get. /// Returns: The element as a wrapped optional if the `index` is in the `indices` of the collection, `nil` otherwise /// - public func getSafely(at index: Self.Index) -> Self.Iterator.Element? { - if !self.indices.contains(index) { - return nil - } - return self[index] + func getSafely(at index: Self.Index) -> Self.Iterator.Element? { + indices.contains(index) ? self[index] : nil } /// /// Returns a collection with same element, and information as to whether the element is the first or the last, or both. /// - public func withPositionInformation() -> [(element: Self.Element, isFirstElement: Bool, isLastElement: Bool)] { - return self.enumerated().map { ($0.element, $0.offset == 0, $0.offset == self.count - 1) } + func withPositionInformation() -> [(element: Self.Element, isFirstElement: Bool, isLastElement: Bool)] { + enumerated().map { ($0.element, $0.offset == 0, $0.offset == count - 1) } } } diff --git a/Sources/FueledUtils/Core/SequenceExtensions.swift b/Sources/FueledUtils/Core/Extensions/Sequence+Extensions.swift similarity index 98% rename from Sources/FueledUtils/Core/SequenceExtensions.swift rename to Sources/FueledUtils/Core/Extensions/Sequence+Extensions.swift index 3cd29bbe..993bc7cd 100644 --- a/Sources/FueledUtils/Core/SequenceExtensions.swift +++ b/Sources/FueledUtils/Core/Extensions/Sequence+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/FueledUtils/Core/StringExtensions.swift b/Sources/FueledUtils/Core/Extensions/String+Extensions.swift similarity index 98% rename from Sources/FueledUtils/Core/StringExtensions.swift rename to Sources/FueledUtils/Core/Extensions/String+Extensions.swift index 46447fdd..94d03fbb 100644 --- a/Sources/FueledUtils/Core/StringExtensions.swift +++ b/Sources/FueledUtils/Core/Extensions/String+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Sources/FueledUtils/Core/SwiftExtensions.swift b/Sources/FueledUtils/Core/Extensions/Swift+Extensions.swift similarity index 89% rename from Sources/FueledUtils/Core/SwiftExtensions.swift rename to Sources/FueledUtils/Core/Extensions/Swift+Extensions.swift index 723bfdc6..7264fcc2 100644 --- a/Sources/FueledUtils/Core/SwiftExtensions.swift +++ b/Sources/FueledUtils/Core/Extensions/Swift+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -extension FloatingPoint { +public extension FloatingPoint { func rounded(decimalPlaces: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self { var this = self this.round(decimalPlaces: decimalPlaces, rule: rule) return this } +} +private extension FloatingPoint { mutating func round(decimalPlaces: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) { var offset = Self(1) for _ in (0.. Bool { - return implementation.numberOfMatches(in: string, options: options, range: string.nsRange) != 0 + implementation.numberOfMatches(in: string, options: options, range: string.nsRange) != 0 } /// Match all the captured groups if any. @@ -89,5 +89,5 @@ public struct Regex { /// - Returns: `true` if `pattern` matches `string`, `false` otherwise. /// public func ~= (pattern: Regex, string: String) -> Bool { - return pattern.match(string) + pattern.match(string) } diff --git a/Sources/FueledUtils/SwiftUI/Binding+KeyPath.swift b/Sources/FueledUtils/SwiftUI/Extensions/Binding+Extensions.swift similarity index 75% rename from Sources/FueledUtils/SwiftUI/Binding+KeyPath.swift rename to Sources/FueledUtils/SwiftUI/Extensions/Binding+Extensions.swift index 47725305..fab2facb 100644 --- a/Sources/FueledUtils/SwiftUI/Binding+KeyPath.swift +++ b/Sources/FueledUtils/SwiftUI/Extensions/Binding+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,9 +14,12 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + extension Binding { - public init(_ object: Type, to keyPath: ReferenceWritableKeyPath) { + public init( + _ object: Type, + to keyPath: ReferenceWritableKeyPath + ) { self.init( get: { object[keyPath: keyPath] @@ -27,3 +30,5 @@ extension Binding { ) } } + +extension ReferenceWritableKeyPath: @retroactive @unchecked Sendable {} diff --git a/Sources/FueledUtils/SwiftUI/EdgeInsets+Helpers.swift b/Sources/FueledUtils/SwiftUI/Extensions/EdgeInsets+Extensions.swift similarity index 68% rename from Sources/FueledUtils/SwiftUI/EdgeInsets+Helpers.swift rename to Sources/FueledUtils/SwiftUI/Extensions/EdgeInsets+Extensions.swift index bdb0098d..4f517525 100644 --- a/Sources/FueledUtils/SwiftUI/EdgeInsets+Helpers.swift +++ b/Sources/FueledUtils/SwiftUI/Extensions/EdgeInsets+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + extension EdgeInsets { public static var zero: EdgeInsets { - EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0) + EdgeInsets(top: .zero, leading: .zero, bottom: .zero, trailing: .zero) } public init(_ length: CGFloat) { @@ -26,10 +26,10 @@ extension EdgeInsets { public init(_ edges: Edge.Set, _ length: CGFloat) { self.init( - top: edges.contains(.top) ? length : 0.0, - leading: edges.contains(.leading) ? length : 0.0, - bottom: edges.contains(.bottom) ? length : 0.0, - trailing: edges.contains(.trailing) ? length : 0.0 + top: edges.contains(.top) ? length : .zero, + leading: edges.contains(.leading) ? length : .zero, + bottom: edges.contains(.bottom) ? length : .zero, + trailing: edges.contains(.trailing) ? length : .zero ) } } diff --git a/Sources/FueledUtils/SwiftUI/View+AnyView.swift b/Sources/FueledUtils/SwiftUI/Extensions/View+Extensions.swift similarity index 85% rename from Sources/FueledUtils/SwiftUI/View+AnyView.swift rename to Sources/FueledUtils/SwiftUI/Extensions/View+Extensions.swift index cc403cac..553ab276 100644 --- a/Sources/FueledUtils/SwiftUI/View+AnyView.swift +++ b/Sources/FueledUtils/SwiftUI/Extensions/View+Extensions.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ import SwiftUI -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + extension View { public func eraseToAnyView() -> AnyView { AnyView(self) diff --git a/Sources/FueledUtils/SwiftUI/FramePreferenceKey.swift b/Sources/FueledUtils/SwiftUI/FramePreferenceKey.swift index f185c098..ab40c476 100644 --- a/Sources/FueledUtils/SwiftUI/FramePreferenceKey.swift +++ b/Sources/FueledUtils/SwiftUI/FramePreferenceKey.swift @@ -1,4 +1,4 @@ -// Copyright © 2020, Fueled Digital Media, LLC +// Copyright © 2024 Fueled Digital Media, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import SwiftUI /// Used to retrieve the frame of a view through a preference key. /// `TagType` is used to uniquely identify the view using the preference key. /// -@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public struct FramePreferenceKey: PreferenceKey { public static var defaultValue: CGRect { .zero