From bd20449b31b686edc25291660570fabe853600a8 Mon Sep 17 00:00:00 2001 From: maximkrouk Date: Sun, 29 Oct 2023 17:31:23 +0100 Subject: [PATCH] feat: Use variadic generics --- .github/workflows/Test.yml | 22 + .gitignore | 2 + .spi.yml | 5 + LICENSE | 2 +- Makefile | 5 + Package.swift | 8 +- README.md | 33 +- Sources/Capture/Capture.swift | 88 ++++ .../OptionalReferenceContainerProtocol.swift | 325 ++++++++++++ Sources/Capture/Weak.swift | 463 ++++-------------- Sources/Capture/Weakifiable.swift | 440 +++++++++++------ Tests/CaptureTests/CaptureTests.swift | 66 +-- 12 files changed, 855 insertions(+), 604 deletions(-) create mode 100644 .github/workflows/Test.yml create mode 100644 .spi.yml create mode 100644 Makefile create mode 100644 Sources/Capture/Capture.swift create mode 100644 Sources/Capture/OptionalReferenceContainerProtocol.swift diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml new file mode 100644 index 0000000..dea0bd6 --- /dev/null +++ b/.github/workflows/Test.yml @@ -0,0 +1,22 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test_macos: + if: | + !contains(github.event.head_commit.message, '[ci skip]') && + !contains(github.event.head_commit.message, '[ci skip test]') && + !contains(github.event.head_commit.message, '[ci skip test_macos]') + runs-on: macOS-13 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - name: Select Xcode 15.0.0 + run: sudo xcode-select -s /Applications/Xcode_15.0.app + - name: Run tests + run: make test diff --git a/.gitignore b/.gitignore index 95c4320..0e81d70 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /Packages /*.xcodeproj xcuserdata/ +DerivedData +Package.resolved diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..6887a67 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [Capture] + swift_version: 5.9 diff --git a/LICENSE b/LICENSE index 8ad2927..b17c1d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 CaptureContext +Copyright (c) 2023 CaptureContext Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c7fb4d7 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +test: + @swift test + +preview_docs: + @swift package --disable-sandbox preview-documentation --product Capture diff --git a/Package.swift b/Package.swift index 140f9a1..0ab916a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 import PackageDescription @@ -10,6 +10,12 @@ let package = Package( targets: ["Capture"] ) ], + dependencies: [ + .package( + url: "https://github.com/apple/swift-docc-plugin", + from: "1.3.0" + ), + ], targets: [ .target(name: "Capture"), .testTarget( diff --git a/README.md b/README.md index 2b193d0..f214180 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # swift-capture -[![SwiftPM 5.3](https://img.shields.io/badge/📦_swiftpm-5.3-ED523F.svg?style=flat)](https://swift.org/download/) [![@maximkrouk](https://img.shields.io/badge/contact-@capturecontext-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context) +[![Test](https://github.com/capturecontext/swift-capture/actions/workflows/Test.yml/badge.svg) [![SwiftPM 5.9](https://img.shields.io/badge/📦_swiftpm-5.9-ED523F.svg?style=flat)](https://github.com/CaptureContext/swift-declarative-configuration/actions/workflows/Test.yml) ![Platforms](https://img.shields.io/badge/platforms-iOS_|_macOS_|_tvOS_|_watchOS_|_Catalyst-ED523F.svg?style=flat) [![@capture_context](https://img.shields.io/badge/contact-@capture__context-1DA1F2.svg?style=flat&logo=twitter)](https://twitter.com/capture_context) -A mechanism for safe capturing & weakifying objects in Swift. +A mechanism for ergonomic and safe capturing & weakifying objects in Swift. ## Usage Examples @@ -19,7 +19,7 @@ With Capture Default ```swift { [weak self] in - guard let self = self else { return } + guard let self else { return } /// ... } ``` @@ -35,7 +35,7 @@ capture { _self in Multiple parameters ```swift { [weak self] a, b, c in - guard let self = self else { return } + guard let self else { return } /// ... } ``` @@ -58,7 +58,7 @@ Methods ``` ```swift -capture(Self.someMethod) +capture(in: <#Type#>.someMethod) ``` ---- @@ -73,29 +73,14 @@ let object.dataSource = { [weak self] in ``` ```swift -let object.dataSource = capture(or: [], in: \.data) -``` - ----- - -Weak assign - -```swift -{ [weak self] value in - self?.value = value -} -``` - -```swift -captureAssign(to: \.value) -captureAssign(to: \.value, removeDuplicates: ==) +let object.dataSource = capture(orReturn: [], in: \.data) ``` ## Installation ### Basic -You can add `weak` to an Xcode project by adding it as a package dependency. +You can add `swift-capture` to an Xcode project by adding it as a package dependency. 1. From the **File** menu, select **Swift Packages › Add Package Dependency…** 2. Enter [`"https://github.com/capturecontext/swift-capture"`](https://github.com/capturecontext/swift-capture) into the package repository URL text field @@ -107,9 +92,8 @@ If you use SwiftPM for your project, you can add `weak` to your package file. Al ```swift .package( - name: "weak", url: "git@github.com:capturecontext/swift-capture.git", - .upToNextMajor("2.0.0") + .upToNextMajor("3.0.0") ) ``` @@ -117,7 +101,6 @@ Do not forget about target dependencies: ```swift .product( - name: "swift-capture", name: "Capture", package: "swift-capture" ) diff --git a/Sources/Capture/Capture.swift b/Sources/Capture/Capture.swift new file mode 100644 index 0000000..605bedf --- /dev/null +++ b/Sources/Capture/Capture.swift @@ -0,0 +1,88 @@ +import Foundation + +// MARK: - Void result closures + +/// Weakly captures an object in non-parametrized void result closure. +@inlinable +public func capture( + _ object: Object, + in closure: @escaping (Object) -> Void +) -> () -> Void { + return Weak(object).capture(in: closure) +} + +/// Weakly captures an object in non-parametrized lazy void result closure. +@inlinable +public func capture( + _ object: Object, + in closure: @escaping (Object) -> () -> Void +) -> () -> Void { + Weak(object).capture(in: closure) +} + +/// Weakly captures an object in parametrized void result closure. +public func capture( + _ object: Object, + in closure: @escaping (Object, repeat each Arg) -> Void +) -> (repeat each Arg) -> Void { + Weak(object).capture(in: closure) +} + +// MARK: - Non-void result closures + +/// Weakly captures an object in non-parametrized non-void result closure. +@inlinable +public func capture( + _ object: Object, + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Object) -> Output +) -> () -> Output { + Weak(object).capture(orReturn: defaultValue(), in: closure) +} + +/// Weakly captures an object in non-parametrized lazy non-void result closure. +@inlinable +public func capture( + _ object: Object, + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Object) -> () -> Output +) -> () -> Output { + Weak(object).capture(orReturn: defaultValue(), in: closure) +} + +/// Weakly captures an object in parametrized non-void result closure. +public func capture( + _ object: Object, + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Object, repeat each Arg) -> Output +) -> (repeat each Arg) -> Output { + Weak(object).capture(orReturn: defaultValue(), in: closure) +} + +// MARK: - Non-void optional result closures + +/// Weakly captures an object in non-parametrized non-void optional result closure. +@inlinable +public func capture( + _ object: Object, + in closure: @escaping (Object) -> Output? +) -> () -> Output? { + Weak(object).capture(in: closure) +} + +/// Weakly captures an object in non-parametrized lazy non-void optional result closure. +@inlinable +public func capture( + _ object: Object, + in closure: @escaping (Object) -> () -> Output? +) -> () -> Output? { + Weak(object).capture(in: closure) +} + +/// Weakly captures an object in parametrized non-void optional result closure. +public func capture( + _ object: Object, + in closure: @escaping (Object, repeat each Arg) -> Output? +) -> (repeat each Arg) -> Output? { + Weak(object).capture(in: closure) +} diff --git a/Sources/Capture/OptionalReferenceContainerProtocol.swift b/Sources/Capture/OptionalReferenceContainerProtocol.swift new file mode 100644 index 0000000..4a46fde --- /dev/null +++ b/Sources/Capture/OptionalReferenceContainerProtocol.swift @@ -0,0 +1,325 @@ +import Foundation + +/// Protocol to generalize Weak and Weak.Box APIs +public protocol OptionalReferenceContainerProtocol { + associatedtype Object: AnyObject + var object: Object? { get } +} + +// MARK: - Void result closures + +extension OptionalReferenceContainerProtocol { + /// Weakly captures an object in non-parametrized void result closure. + /// + /// Creates `() -> Void` handler from `(Object) -> Void` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Unwrapping happens on the call of returned closure. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func observe( + /// _ publisher: some Publisher + /// ) -> AnyCancellable { + /// publisher.sink( + /// receiveValue: capture { _self in + /// // ... + /// } + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> Void`, where Object is unwrapped weakly captured object. + /// - Returns: `() -> Void` handler, created from `(Object) -> Void` closure by weakly capturing object. + @inlinable + public func capture( + in closure: @escaping (Object) -> Void + ) -> () -> Void { + return { [weak object] in + guard let object else { return } + closure(object) + } + } + + /// Weakly captures an object in non-parametrized lazy void result closure. + /// + /// Primary idea behind this method is to be able to pass methods without referring to an object. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func handleCompletion() { + /// // ... + /// } + /// + /// func observe( + /// _ publisher: some Publisher + /// ) -> AnyCancellable { + /// publisher.sink( + /// receiveValue: capture(in: MyClass.handleCompletion) + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> () -> Void`, most likely it's `ObjectType.someInstanceMethod` + /// - Returns: `() -> Void` handler, created from `(Object) -> () -> Void` closure by weakly capturing object + @inlinable + public func capture( + in closure: @escaping (Object) -> () -> Void + ) -> () -> Void { + return { [weak object] in + guard let object else { return } + closure(object)() + } + } + + /// Weakly captures an object in parametrized void result closure. + /// + /// > In context of this doc "`A, B, C...`" is a shortcut for 1 or more types. + /// + /// Creates `(A, B, C...) -> Void` handler from `(Object, A, B, C...) -> Void` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func observe( + /// _ publisher: some Publisher + /// ) -> AnyCancellable { + /// publisher.sink( + /// // number of params comes from publisher, if it was + /// // some completion with multiple args, all the args + /// // would've been passed to the capture method + /// receiveValue: capture { _self, value in + /// // ... + /// } + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object, A, B, C...) -> Void`, where Object is unwrapped weakly captured object. + /// - Returns: `(A, B, C...) -> Void` handler, created from `(Object, A, B, C) -> Void` closure by weakly capturing object. + public func capture( + in closure: @escaping (Object, repeat each Arg) -> Void + ) -> (repeat each Arg) -> Void { + return { [weak object] (arg: repeat each Arg) in + guard let object else { return } + closure(object, repeat each arg) + } + } +} + +// MARK: - Non-void result closures + +extension OptionalReferenceContainerProtocol { + /// Weakly captures an object in non-parametrized non-void result closure. + /// + /// Creates `() -> Output` handler from `(Object) -> Output` closure. + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Unwrapping happens on the call of returned closure. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// var value: Int = 0 + /// + /// func createZeroDataSource() -> () -> Int { + /// return capture(orReturn: -1) { _self in + /// return 0 + /// } + /// } + /// + /// func createValueDataSource() -> () -> Int { + /// return capture(orReturn: -1, in: \.value) + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - defaultValue: Default value to be returned if object was deinitialized when returned handler was called. + /// - closure: `(Object) -> Output`, where Object is unwrapped weakly captured object. + /// - Returns: `() -> Output` handler, created from `(Object) -> Output` closure by weakly capturing object. + @inlinable + public func capture( + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Object) -> Output + ) -> () -> Output { + return { [weak object] in + guard let object else { return defaultValue() } + return closure(object) + } + } + + /// Weakly captures an object in non-parametrized lazy non-void result closure. + /// + /// Primary idea behind this method is to be able to pass methods without referring to an object. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// // `MyClass.getData` signature is `(MyClass) -> () -> Int` + /// func getData() -> Int { + /// return 0 + /// } + /// + /// func createDataSource() -> () -> Int { + /// return capture(orReturn: -1, in: MyClass.getData) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> () -> Output`, most likely it's `ObjectType.someInstanceMethod`. + /// - Returns: `() -> Output` handler, created from `(Object) -> () -> Output` closure by weakly capturing object. + @inlinable + public func capture( + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Object) -> () -> Output + ) -> () -> Output { + return { [weak object] in + guard let object else { return defaultValue() } + return closure(object)() + } + } + + /// Weakly captures an object in parametrized non-void result closure. + /// + /// > In context of this doc "`A, B, C...`" is a shortcut for 1 or more types. + /// + /// Creates `(A, B, C...) -> Output` handler from `(Object, A, B, C...) -> Output` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func createDataSource() -> (Bool) -> Int { + /// // number of params comes from publisher, if it was + /// // some completion with multiple args, all the args + /// // would've been passed to the capture method + /// return capture(orReturn: -1) { _self, isZero in + /// return isZero ? 0 : 1 + /// } + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object, A, B, C...) -> Output`, where Object is unwrapped weakly captured object. + /// - Returns: `(A, B, C...) -> Output` handler, created from `(Object, A, B, C) -> Output` closure by weakly capturing object. + public func capture( + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Object, repeat each Arg) -> Output + ) -> (repeat each Arg) -> Output { + return { [weak object] (arg: repeat each Arg) in + guard let object else { return defaultValue() } + return closure(object, repeat each arg) + } + } +} + +// MARK: - Non-void optional result closures + +extension OptionalReferenceContainerProtocol { + /// Weakly captures an object in non-parametrized non-void optional resilt closure. + /// + /// Creates `() -> Output?` handler from `(Object) -> Output?` closure. + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Unwrapping happens on the call of returned closure. + /// Handler will return nil if object was already deinitialized. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// var value: Int = 0 + /// + /// func createZeroDataSource() -> () -> Int? { + /// return capture { _self in + /// return _self.value + /// } + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - closure: `(Object) -> Output?`, where Object is unwrapped weakly captured object. + /// - Returns: `() -> Output?` handler, created from `(Object) -> Output?` closure by weakly capturing object. Handler will return nil if object was already deinitialized. + @inlinable + public func capture( + in closure: @escaping (Object) -> Output? + ) -> () -> Output? { + return { [weak object] in + guard let object else { return nil } + return closure(object) + } + } + + /// Weakly captures an object in non-parametrized lazy non-void optional result closure. + /// + /// Primary idea behind this method is to be able to pass methods without referring to an object. + /// + /// Handler will return nil if object was already deinitialized. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// // `MyClass.getData` signature is `(MyClass) -> () -> Int?` + /// func getData() -> Int? { + /// return 0 + /// } + /// + /// func createDataSource() -> () -> Int? { + /// return capture(orReturn: -1, in: MyClass.getData) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> () -> Output?`, most likely it's `ObjectType.someInstanceMethod`. + /// - Returns: `() -> Output?` handler, created from `(Object) -> () -> Output?` closure by weakly capturing object. + @inlinable + public func capture( + in closure: @escaping (Object) -> () -> Output? + ) -> () -> Output? { + return { [weak object] in + guard let object else { return nil } + return closure(object)() + } + } + + /// Weakly captures an object in parametrized non-void optional result closure. + /// + /// > In context of this doc "`A, B, C...`" is a shortcut for 1 or more types. + /// + /// Creates `(A, B, C...) -> Output?` handler from `(Object, A, B, C...) -> Output?` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Handler will return nil if object was already deinitialized. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func createDataSource() -> (Bool) -> Int? { + /// // number of params comes from publisher, if it was + /// // some completion with multiple args, all the args + /// // would've been passed to the capture method + /// return capture(orReturn: -1) { _self, isZero in + /// return isZero ? 0 : 1 + /// } + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object, A, B, C...) -> Void`, where Object is unwrapped weakly captured object. + /// - Returns: `(A, B, C...) -> Output?` handler, created from `(Object, A, B, C) -> Output?` closure by weakly capturing object. + public func capture( + in closure: @escaping (Object, repeat each Arg) -> Output? + ) -> (repeat each Arg) -> Output? { + return { [weak object] (arg: repeat each Arg) in + guard let object else { return nil } + return closure(object, repeat each arg) + } + } +} diff --git a/Sources/Capture/Weak.swift b/Sources/Capture/Weak.swift index 79ef861..38ffa0d 100644 --- a/Sources/Capture/Weak.swift +++ b/Sources/Capture/Weak.swift @@ -1,393 +1,128 @@ import Foundation +/// Value-type container for capturing objects weakly +/// +/// We believe that most APIs should not be too opinionated and this package provides +/// various ways to capture objects, so feel free to use it as property wrapper or plain property/variable. +/// +/// However opinionated approach removes cognitive load while working with APIs, so we do also provide recomendations. +/// +/// It's recommended to conform your objects to ``Weakifiable`` protocol instead of using this type if possible +/// or use `_capture(...)` methods directly if not. +/// +/// But there is a recommended usecase: workarounds for poorly designed APIs with ``capture(_:)-swift.type.method`` 😅 @propertyWrapper -public struct Weak { +public struct Weak: OptionalReferenceContainerProtocol { + + /// Weak reference to an object public weak var wrappedValue: Object? - - public var projectedValue: Object? { + + /// Convenience property to access wrappedValue with better semantics when used inline + @inlinable + public var object: Object? { get { wrappedValue } set { wrappedValue = newValue } } - - public init(_ object: Object?) { - self.init(wrappedValue: object) + + /// Creates a reference-type box with a weak reference the object + @inlinable + public var projectedValue: Box { + return .init(wrappedValue) } - + + /// Convenience property to access projectedValue with better semantics when used inline + @inlinable + public var box: Box { + return projectedValue + } + public init() {} public init(wrappedValue: Object?) { self.wrappedValue = wrappedValue } -} -extension Weak { - public func capture( - in closure: @escaping (Object) -> Void - ) -> (() -> Void) { - return { [weak wrappedValue] in - guard let object = wrappedValue else { return } - closure(object) - } - } - - public func capture( - in closure: @escaping (Object, T0) -> Void - ) -> ((T0) -> Void) { - return { [weak wrappedValue] params in - guard let object = wrappedValue else { return } - closure(object, params) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1) -> Void - ) -> ((T0, T1) -> Void) { - return { [weak wrappedValue] t0, t1 in - guard let object = wrappedValue else { return } - closure(object, t0, t1) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2) -> Void - ) -> ((T0, T1, T2) -> Void) { - return { [weak wrappedValue] t0, t1, t2 in - guard let object = wrappedValue else { return } - closure(object, t0, t1, t2) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3) -> Void - ) -> ((T0, T1, T2, T3) -> Void) { - return { [weak wrappedValue] t0, t1, t2, t3 in - guard let object = wrappedValue else { return } - closure(object, t0, t1, t2, t3) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4) -> Void - ) -> ((T0, T1, T2, T3, T4) -> Void) { - return { [weak wrappedValue] t0, t1, t2, t3, t4 in - guard let object = wrappedValue else { return } - closure(object, t0, t1, t2, t3, t4) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5) -> Void - ) -> ((T0, T1, T2, T3, T4, T5) -> Void) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5 in - guard let object = wrappedValue else { return } - closure(object, t0, t1, t2, t3, t4, t5) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6) -> Void - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Void) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6 in - guard let object = wrappedValue else { return } - closure(object, t0, t1, t2, t3, t4, t5, t6) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6, T7) -> Void - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Void) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6, t7 in - guard let object = wrappedValue else { return } - closure(object, t0, t1, t2, t3, t4, t5, t6, t7) - } + @inlinable + public init(_ object: Object?) { + self.init(wrappedValue: object) } -} -extension Weak { - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object) -> Value - ) -> (() -> Value) { - return { [weak wrappedValue] in - guard let object = wrappedValue else { return defaultValue() } - return closure(object) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0) -> Value - ) -> ((T0) -> Value) { - return { [weak wrappedValue] params in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, params) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1) -> Value - ) -> ((T0, T1) -> Value) { - return { [weak wrappedValue] t0, t1 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1, T2) -> Value - ) -> ((T0, T1, T2) -> Value) { - return { [weak wrappedValue] t0, t1, t2 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1, t2) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1, T2, T3) -> Value - ) -> ((T0, T1, T2, T3) -> Value) { - return { [weak wrappedValue] t0, t1, t2, t3 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1, t2, t3) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1, T2, T3, T4) -> Value - ) -> ((T0, T1, T2, T3, T4) -> Value) { - return { [weak wrappedValue] t0, t1, t2, t3, t4 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1, t2, t3, t4) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5) -> Value - ) -> ((T0, T1, T2, T3, T4, T5) -> Value) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1, t2, t3, t4, t5) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Value) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1, t2, t3, t4, t5, t6) - } - } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6, T7) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Value) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6, t7 in - guard let object = wrappedValue else { return defaultValue() } - return closure(object, t0, t1, t2, t3, t4, t5, t6, t7) - } + /// Passes capturable box in a closure, box object is set to returned value after return + /// + /// Recommended usecase: workarounds for poorly designed APIs 😅 + /// ```swift + /// return Weak.capture { box in + /// return createFancyBottomSheet(onLinkTapped: { url in + /// guard let controller = box.object else { return } + /// openWebView(url, in: controller) + /// }) + /// } + /// ``` + /// instead of + /// ```swift + /// var controller: UIViewController + /// controller = createFancyBottomSheet(onLinkTapped: { [weak controller] url in + /// guard let controller else { return } + /// openWebView(url, in: controller) + /// }) + /// return controller + /// ``` + @inlinable + public static func capture( + _ initializer: (Box) -> Object + ) -> Object { + let box = Box() + let object = initializer(box) + box.wrappedValue = object + return object } } extension Weak { - public func capture( - in closure: @escaping (Object) -> Value - ) -> (() -> Value?) { - return { [weak wrappedValue] in - guard let object = wrappedValue else { return nil } - return closure(object) - } - } - - public func capture( - in closure: @escaping (Object, T0) -> Value - ) -> ((T0) -> Value?) { - return { [weak wrappedValue] params in - guard let object = wrappedValue else { return nil } - return closure(object, params) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1) -> Value - ) -> ((T0, T1) -> Value?) { - return { [weak wrappedValue] t0, t1 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2) -> Value - ) -> ((T0, T1, T2) -> Value?) { - return { [weak wrappedValue] t0, t1, t2 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3) -> Value - ) -> ((T0, T1, T2, T3) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4) -> Value - ) -> ((T0, T1, T2, T3, T4) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5) -> Value - ) -> ((T0, T1, T2, T3, T4, T5) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4, t5) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4, t5, t6) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6, T7) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6, t7 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4, t5, t6, t7) - } - } -} + /// Value-type container for capturing objects weakly + /// + /// We believe that most APIs should not be too opinionated and this package provides + /// various ways to capture objects, so feel free to use it as property wrapper or plain property/variable. + /// + /// However opinionated approach removes cognitive load while working with APIs, so we do also provide recomendations. + /// + /// It's recommended to conform your objects to ``Weakifiable`` protocol instead of using this type if possible + /// or use `_capture(...)` methods directly if not. + /// + /// But there is a recommended usecase: workarounds for poorly designed APIs with ``capture(_:)-swift.type.method`` 😅 + @propertyWrapper + public final class Box: OptionalReferenceContainerProtocol { + /// Weak reference to an object + public weak var wrappedValue: Object? = nil -extension Weak { - public func capture( - in closure: @escaping (Object) -> Value? - ) -> (() -> Value?) { - return { [weak wrappedValue] in - guard let object = wrappedValue else { return nil } - return closure(object) - } - } - - public func capture( - in closure: @escaping (Object, T0) -> Value? - ) -> ((T0) -> Value?) { - return { [weak wrappedValue] params in - guard let object = wrappedValue else { return nil } - return closure(object, params) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1) -> Value? - ) -> ((T0, T1) -> Value?) { - return { [weak wrappedValue] t0, t1 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2) -> Value? - ) -> ((T0, T1, T2) -> Value?) { - return { [weak wrappedValue] t0, t1, t2 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3) -> Value? - ) -> ((T0, T1, T2, T3) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4) -> Value? - ) -> ((T0, T1, T2, T3, T4) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5) -> Value? - ) -> ((T0, T1, T2, T3, T4, T5) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4, t5) + /// Convenience property to access wrappedValue with better semantics when used inline + @inlinable + public var object: Object? { + get { wrappedValue } + set { wrappedValue = newValue } } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6) -> Value? - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4, t5, t6) - } - } - - public func capture( - in closure: @escaping (Object, T0, T1, T2, T3, T4, T5, T6, T7) -> Value? - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Value?) { - return { [weak wrappedValue] t0, t1, t2, t3, t4, t5, t6, t7 in - guard let object = wrappedValue else { return nil } - return closure(object, t0, t1, t2, t3, t4, t5, t6, t7) + + /// Creates a value-type ``Weak`` container with a weak reference the object + @inlinable + public var projectedValue: Weak { + return .init(wrappedValue) } - } -} -extension Weak { - public func capture( - _ closure: @escaping (Object) -> (() -> Void) - ) -> (() -> Void) { - return { [weak wrappedValue] in - guard let object = wrappedValue else { return } - return closure(object)() + /// Convenience method to access ``projectedValue`` with better semantics when used inline + /// + /// - Returns: Value-type weak reference to an object + @inlinable + public func unboxed() -> Weak { projectedValue } + + public init() {} + + public init(wrappedValue: Object?) { + self.wrappedValue = wrappedValue } - } -} -extension Weak { - public func captureAssign( - to keyPath: ReferenceWritableKeyPath - ) -> (Value) -> Void { - capture { $0[keyPath: keyPath] = $1 } - } - - public func captureAssign( - to keyPath: ReferenceWritableKeyPath, - removeDuplicates isDuplicate: @escaping (Value, Value) -> Bool - ) -> (Value) -> Void { - capture { _self, newValue in - let currentValue = _self[keyPath: keyPath] - if !isDuplicate(currentValue, newValue) { - _self[keyPath: keyPath] = newValue - } + @inlinable + public convenience init(_ object: Object?) { + self.init(wrappedValue: object) } } } diff --git a/Sources/Capture/Weakifiable.swift b/Sources/Capture/Weakifiable.swift index 83430cb..5a9ec96 100644 --- a/Sources/Capture/Weakifiable.swift +++ b/Sources/Capture/Weakifiable.swift @@ -1,179 +1,299 @@ import Foundation +/// Provides a bunch of methods for weakly capturing objects +/// +/// Uses ``Weak`` capturing under the hood public protocol Weakifiable: AnyObject {} + extension NSObject: Weakifiable {} -extension Weakifiable { - public func capture(in closure: @escaping (Self) -> Void) - -> (() -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0) -> Void) - -> ((T0) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1) -> Void) - -> ((T0, T1) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1, T2) -> Void) - -> ((T0, T1, T2) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1, T2, T3) -> Void) - -> ((T0, T1, T2, T3) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1, T2, T3, T4) -> Void) - -> ((T0, T1, T2, T3, T4) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1, T2, T3, T4, T5) -> Void) - -> ((T0, T1, T2, T3, T4, T5) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6) -> Void) - -> ((T0, T1, T2, T3, T4, T5, T6) -> Void) { Weak(self).capture(in: closure) } - - public func capture(in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6, T7) -> Void) - -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Void) { Weak(self).capture(in: closure) } -} +// MARK: - Void result closures extension Weakifiable { - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self) -> Value - ) -> (() -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0) -> Value - ) -> ((T0) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1) -> Value - ) -> ((T0, T1) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1, T2) -> Value - ) -> ((T0, T1, T2) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1, T2, T3) -> Value - ) -> ((T0, T1, T2, T3) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1, T2, T3, T4) -> Value - ) -> ((T0, T1, T2, T3, T4) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5) -> Value - ) -> ((T0, T1, T2, T3, T4, T5) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } - - public func capture( - or defaultValue: @escaping @autoclosure () -> Value, - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6, T7) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Value) { Weak(self).capture(or: defaultValue(), in: closure) } -} + /// Weakly captures an object in non-parametrized void result closure. + /// + /// Creates `() -> Void` handler from `(Object) -> Void` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Unwrapping happens on the call of returned closure. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func observe( + /// _ publisher: some Publisher + /// ) -> AnyCancellable { + /// publisher.sink( + /// receiveValue: capture { _self in + /// // ... + /// } + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> Void`, where Object is unwrapped weakly captured object. + /// - Returns: `() -> Void` handler, created from `(Object) -> Void` closure by weakly capturing object. + @inlinable + public func capture( + in closure: @escaping (Self) -> Void + ) -> () -> Void { + return Weak(self).capture(in: closure) + } -extension Weakifiable { - public func capture( - in closure: @escaping (Self) -> Value - ) -> (() -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0) -> Value - ) -> ((T0) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1) -> Value - ) -> ((T0, T1) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2) -> Value - ) -> ((T0, T1, T2) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3) -> Value - ) -> ((T0, T1, T2, T3) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4) -> Value - ) -> ((T0, T1, T2, T3, T4) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5) -> Value - ) -> ((T0, T1, T2, T3, T4, T5) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6, T7) -> Value - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Value?) { Weak(self).capture(in: closure) } -} + /// Weakly captures an object in non-parametrized lazy void result closure. + /// + /// Primary idea behind this method is to be able to pass methods without referring to an object. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func handleCompletion() { + /// // ... + /// } + /// + /// func observe( + /// _ publisher: some Publisher + /// ) -> AnyCancellable { + /// publisher.sink( + /// receiveValue: capture(in: MyClass.handleCompletion) + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> () -> Void`, most likely it's `ObjectType.someInstanceMethod` + /// - Returns: `() -> Void` handler, created from `(Object) -> () -> Void` closure by weakly capturing object + @inlinable + public func capture( + in closure: @escaping (Self) -> () -> Void + ) -> () -> Void { + return Weak(self).capture(in: closure) + } -extension Weakifiable { - public func capture( - in closure: @escaping (Self) -> Value? - ) -> (() -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0) -> Value? - ) -> ((T0) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1) -> Value? - ) -> ((T0, T1) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2) -> Value? - ) -> ((T0, T1, T2) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3) -> Value? - ) -> ((T0, T1, T2, T3) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4) -> Value? - ) -> ((T0, T1, T2, T3, T4) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5) -> Value? - ) -> ((T0, T1, T2, T3, T4, T5) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6) -> Value? - ) -> ((T0, T1, T2, T3, T4, T5, T6) -> Value?) { Weak(self).capture(in: closure) } - - public func capture( - in closure: @escaping (Self, T0, T1, T2, T3, T4, T5, T6, T7) -> Value? - ) -> ((T0, T1, T2, T3, T4, T5, T6, T7) -> Value?) { Weak(self).capture(in: closure) } + /// Weakly captures an object in parametrized void result closure. + /// + /// > In context of this doc "`A, B, C...`" is a shortcut for 1 or more types. + /// + /// Creates `(A, B, C...) -> Void` handler from `(Object, A, B, C...) -> Void` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func observe( + /// _ publisher: some Publisher + /// ) -> AnyCancellable { + /// publisher.sink( + /// // number of params comes from publisher, if it was + /// // some completion with multiple args, all the args + /// // would've been passed to the capture method + /// receiveValue: capture { _self, value in + /// // ... + /// } + /// ) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object, A, B, C...) -> Void`, where Object is unwrapped weakly captured object. + /// - Returns: `(A, B, C...) -> Void` handler, created from `(Object, A, B, C) -> Void` closure by weakly capturing object. + public func capture( + in closure: @escaping (Self, repeat each Arg) -> Void + ) -> (repeat each Arg) -> Void { + return Weak(self).capture(in: closure) + } } +// MARK: - Non-void result closures + extension Weakifiable { - public func capture( - _ closure: @escaping (Self) -> (() -> Void) - ) -> (() -> Void) { - Weak(self).capture(closure) + /// Weakly captures an object in non-parametrized non-void result closure. + /// + /// Creates `() -> Output` handler from `(Object) -> Output` closure. + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Unwrapping happens on the call of returned closure. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// var value: Int = 0 + /// + /// func createZeroDataSource() -> () -> Int { + /// return capture(orReturn: -1) { _self in + /// return 0 + /// } + /// } + /// + /// func createValueDataSource() -> () -> Int { + /// return capture(orReturn: -1, in: \.value) + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - defaultValue: Default value to be returned if object was deinitialized when returned handler was called. + /// - closure: `(Object) -> Output`, where Object is unwrapped weakly captured object. + /// - Returns: `() -> Output` handler, created from `(Object) -> Output` closure by weakly capturing object. + @inlinable + public func capture( + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Self) -> Output + ) -> () -> Output { + return Weak(self).capture(orReturn: defaultValue(), in: closure) + } + + /// Weakly captures an object in non-parametrized lazy non-void result closure. + /// + /// Primary idea behind this method is to be able to pass methods without referring to an object. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// // `MyClass.getData` signature is `(MyClass) -> () -> Int` + /// func getData() -> Int { + /// return 0 + /// } + /// + /// func createDataSource() -> () -> Int { + /// return capture(orReturn: -1, in: MyClass.getData) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> () -> Output`, most likely it's `ObjectType.someInstanceMethod`. + /// - Returns: `() -> Output` handler, created from `(Object) -> () -> Output` closure by weakly capturing object. + @inlinable + public func capture( + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Self) -> () -> Output + ) -> () -> Output { + return Weak(self).capture(orReturn: defaultValue(), in: closure) + } + + /// Weakly captures an object in parametrized non-void result closure. + /// + /// > In context of this doc "`A, B, C...`" is a shortcut for 1 or more types. + /// + /// Creates `(A, B, C...) -> Output` handler from `(Object, A, B, C...) -> Output` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func createDataSource() -> (Bool) -> Int { + /// // number of params comes from publisher, if it was + /// // some completion with multiple args, all the args + /// // would've been passed to the capture method + /// return capture(orReturn: -1) { _self, isZero in + /// return isZero ? 0 : 1 + /// } + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object, A, B, C...) -> Output`, where Object is unwrapped weakly captured object. + /// - Returns: `(A, B, C...) -> Output` handler, created from `(Object, A, B, C) -> Output` closure by weakly capturing object. + public func capture( + orReturn defaultValue: @escaping @autoclosure () -> Output, + in closure: @escaping (Self, repeat each Arg) -> Output + ) -> (repeat each Arg) -> Output { + return Weak(self).capture(orReturn: defaultValue(), in: closure) } } +// MARK: - Non-void optional result closures + extension Weakifiable { - public func captureAssign( - to keyPath: ReferenceWritableKeyPath - ) -> (Value) -> Void { - Weak(self).captureAssign(to: keyPath) + /// Weakly captures an object in non-parametrized non-void optional resilt closure. + /// + /// Creates `() -> Output?` handler from `(Object) -> Output?` closure. + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Unwrapping happens on the call of returned closure. + /// Handler will return nil if object was already deinitialized. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// var value: Int = 0 + /// + /// func createZeroDataSource() -> () -> Int? { + /// return capture { _self in + /// return _self.value + /// } + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - closure: `(Object) -> Output?`, where Object is unwrapped weakly captured object. + /// - Returns: `() -> Output?` handler, created from `(Object) -> Output?` closure by weakly capturing object. Handler will return nil if object was already deinitialized. + @inlinable + public func capture( + in closure: @escaping (Self) -> Output? + ) -> () -> Output? { + return Weak(self).capture(in: closure) } - - public func captureAssign( - to keyPath: ReferenceWritableKeyPath, - removeDuplicates isDuplicate: @escaping (Value, Value) -> Bool - ) -> (Value) -> Void { - Weak(self).captureAssign(to: keyPath, removeDuplicates: isDuplicate) + + /// Weakly captures an object in non-parametrized lazy non-void optional result closure. + /// + /// Primary idea behind this method is to be able to pass methods without referring to an object. + /// + /// Handler will return nil if object was already deinitialized. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// // `MyClass.getData` signature is `(MyClass) -> () -> Int?` + /// func getData() -> Int? { + /// return 0 + /// } + /// + /// func createDataSource() -> () -> Int? { + /// return capture(orReturn: -1, in: MyClass.getData) + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object) -> () -> Output?`, most likely it's `ObjectType.someInstanceMethod`. + /// - Returns: `() -> Output?` handler, created from `(Object) -> () -> Output?` closure by weakly capturing object. + @inlinable + public func capture( + in closure: @escaping (Self) -> () -> Output? + ) -> () -> Output? { + return Weak(self).capture(in: closure) + } + + /// Weakly captures an object in parametrized non-void optional result closure. + /// + /// > In context of this doc "`A, B, C...`" is a shortcut for 1 or more types. + /// + /// Creates `(A, B, C...) -> Output?` handler from `(Object, A, B, C...) -> Output?` closure + /// so you can access weakly captured Object in your handler if the object is present. + /// + /// Handler will return nil if object was already deinitialized. + /// + /// Example: + /// ```swift + /// class MyClass: Weakifiable { + /// func createDataSource() -> (Bool) -> Int? { + /// // number of params comes from publisher, if it was + /// // some completion with multiple args, all the args + /// // would've been passed to the capture method + /// return capture(orReturn: -1) { _self, isZero in + /// return isZero ? 0 : 1 + /// } + /// } + /// } + /// ``` + /// + /// - Parameter closure: `(Object, A, B, C...) -> Void`, where Object is unwrapped weakly captured object. + /// - Returns: `(A, B, C...) -> Output?` handler, created from `(Object, A, B, C) -> Output?` closure by weakly capturing object. + public func capture( + in closure: @escaping (Self, repeat each Arg) -> Output? + ) -> (repeat each Arg) -> Output? { + return Weak(self).capture(in: closure) } } diff --git a/Tests/CaptureTests/CaptureTests.swift b/Tests/CaptureTests/CaptureTests.swift index f5960f1..056f16b 100644 --- a/Tests/CaptureTests/CaptureTests.swift +++ b/Tests/CaptureTests/CaptureTests.swift @@ -22,11 +22,16 @@ final class WeakTests: XCTestCase { isObjectDeinitialized = true }) - let weakObject = Weak(object) - + var weakObject = Weak(object) + XCTAssert(!isObjectDeinitialized) object = nil XCTAssert(isObjectDeinitialized) + + do { // mute warnings + weakObject = .init() + _ = weakObject + } } func testDeinitWithCapture() { @@ -62,7 +67,7 @@ final class WeakTests: XCTestCase { isObjectDeinitialized = true }) - let closure = object!.capture(or: 0) { object in + let closure = object!.capture(orReturn: 0) { object in numberOfCalls += 1 return 1 } @@ -114,7 +119,7 @@ final class WeakTests: XCTestCase { isObjectDeinitialized = true }) - let closure: () -> Int? = object!.capture(or: 1) { object in + let closure: () -> Int? = object!.capture(orReturn: 1) { object in numberOfCalls += 1 return nil } @@ -151,10 +156,10 @@ final class WeakTests: XCTestCase { isObjectDeinitialized = true }) - let closure1: () -> Void = object!.capture(LocalObject.doSomething) - let closure2: () -> Void = object!.capture(LocalObject.undoSomething) - let closure3: () -> Bool = object!.capture(or: false, in: \.didSomething) - + let closure1: () -> Void = object!.capture(in: LocalObject.doSomething) + let closure2: () -> Void = object!.capture(in: LocalObject.undoSomething) + let closure3: () -> Bool = object!.capture(orReturn: false, in: \.didSomething) + XCTAssertEqual(object?.didSomething, false) XCTAssertEqual(closure3(), false) closure1() @@ -171,49 +176,4 @@ final class WeakTests: XCTestCase { XCTAssertEqual(object?.didSomething, nil) XCTAssertEqual(closure3(), false) } - - func testAssign() { - class LocalObject: Object { - var value: Int = 0 { - didSet { onValueDidSet?(value) } - } - - var onValueDidSet: ((Int) -> Void)? - } - - var isObjectDeinitialized = false - var object: LocalObject! = LocalObject( - onDeinit: { isObjectDeinitialized = true } - ) - - let assignValue = object.captureAssign(to: \.value) - let assignValueNoDuplicate = object.captureAssign(to: \.value, removeDuplicates: ==) - - var values: [Int] = [object.value] // 0 - object.onValueDidSet = { value in - values.append(value) - } - - XCTAssertEqual(object.value, 0) - XCTAssertEqual(values, [0]) - - assignValue(1) - XCTAssertEqual(object.value, 1) - XCTAssertEqual(values, [0, 1]) - - assignValue(1) - XCTAssertEqual(object.value, 1) - XCTAssertEqual(values, [0, 1, 1]) - - assignValueNoDuplicate(2) - XCTAssertEqual(object.value, 2) - XCTAssertEqual(values, [0, 1, 1, 2]) - - assignValueNoDuplicate(2) - XCTAssertEqual(object.value, 2) - XCTAssertEqual(values, [0, 1, 1, 2]) - - object = nil - XCTAssertEqual(isObjectDeinitialized, true) - } }