|
| 1 | +# Weakify |
| 2 | + |
| 3 | +[](https://swiftpackageindex.com/kylehughes/Weakify) |
| 4 | +[](https://swiftpackageindex.com/kylehughes/Weakify) |
| 5 | +[](https://github.com/kylehughes/Weakify/actions/workflows/test.yml) |
| 6 | + |
| 7 | +*A simple, ergonomic solution for safely capturing method references in Swift.* |
| 8 | + |
| 9 | +## About |
| 10 | + |
| 11 | +Weakify provides convenient and safe mechanisms for capturing method references weakly or unownedly, ensuring closures referencing methods on an object do not inadvertently extend the object's lifetime. It relies on unapplied method references, and uses parameter packs to support heterogeneous argument lists. |
| 12 | + |
| 13 | +This package encourages: |
| 14 | + |
| 15 | +* Single-line syntax for capturing method references. |
| 16 | +* Avoidance of retain cycles through `weak` or `unowned` captures. |
| 17 | + |
| 18 | +Weakify is extremely lightweight and has a robust test suite. |
| 19 | + |
| 20 | +## Capabilities |
| 21 | + |
| 22 | +* [x] Weakly capture method references with automatic fallback values. |
| 23 | +* [x] Unownedly capture method references when you know the target will outlive the closure. |
| 24 | +* [x] Ergonomic handling of heterogenous arguments. |
| 25 | +* [x] Swift 6 language mode support. |
| 26 | + |
| 27 | +## Supported Platforms |
| 28 | + |
| 29 | +* iOS 13.0+ |
| 30 | +* macOS 10.15+ |
| 31 | +* tvOS 13.0+ |
| 32 | +* visionOS 1.0+ |
| 33 | +* watchOS 6.0+ |
| 34 | + |
| 35 | +## Requirements |
| 36 | + |
| 37 | +* Swift 6.1+ |
| 38 | +* Xcode 16.3+ |
| 39 | + |
| 40 | +> [!NOTE] |
| 41 | +> This package ideally would only require Swift 5.9, but compiler versions prior to 6.1 (packaged with Xcode 16.3) |
| 42 | +> have a bug that makes the intended usage incompatible with `@MainActor`-isolated closures. We require Swift language |
| 43 | +> tools version 6.1 as a proxy for this tribal knowledge. |
| 44 | +
|
| 45 | +## Documentation |
| 46 | + |
| 47 | +[Documentation is available on GitHub Pages.](https://kylehughes.github.io/Weakify/) |
| 48 | + |
| 49 | +## Installation |
| 50 | + |
| 51 | +### Swift Package Manager |
| 52 | + |
| 53 | +```swift |
| 54 | +dependencies: [ |
| 55 | + .package(url: "https://github.com/kylehughes/Weakify.git", .upToNextMajor(from: "1.0.0")), |
| 56 | +] |
| 57 | +``` |
| 58 | + |
| 59 | +## Quick Start |
| 60 | + |
| 61 | +Weakly capture a method reference: |
| 62 | + |
| 63 | +```swift |
| 64 | +import Weakify |
| 65 | + |
| 66 | +class MyViewController: UIViewController { |
| 67 | + private lazy var button: UIButton = { |
| 68 | + let button = UIButton() |
| 69 | + button.addAction( |
| 70 | + UIAction(handler: weakify(MyViewController.buttonTapped, on: self)), |
| 71 | + for: .primaryActionTriggered |
| 72 | + ) |
| 73 | + return button |
| 74 | + }() |
| 75 | + |
| 76 | + private func buttonTapped(_ action: UIAction) { |
| 77 | + print("Button tapped") |
| 78 | + } |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +Unownedly capture a method reference: |
| 83 | + |
| 84 | +```swift |
| 85 | +import Weakify |
| 86 | + |
| 87 | +class MyViewController: UIViewController { |
| 88 | + func observe(notificationCenter: NotificationCenter) { |
| 89 | + notificationCenter.addObserver( |
| 90 | + forName: NSNotification.Name("MyNotification"), |
| 91 | + object: nil, |
| 92 | + queue: .main, |
| 93 | + using: disown(MyViewController.handleNotification, on: self) |
| 94 | + ) |
| 95 | + } |
| 96 | + |
| 97 | + private func handleNotification(_ notification: Notification) { |
| 98 | + print("Notification received") |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +## Usage |
| 104 | + |
| 105 | +> [!NOTE] |
| 106 | +> An unapplied method reference (UMR) is the function value produced by writing an instance method on the type |
| 107 | +> instead of an instance, for example `UIView.layoutIfNeeded` or `Array.append`. Its curried shape is |
| 108 | +> `(Self) -> (Args…) -> Result`, so supplying a specific instance—`unappliedMethodReference(target)`—yields the regular |
| 109 | +> `(Args…) -> Result` closure you would normally call. |
| 110 | +
|
| 111 | +### Weak Capture with Fallback |
| 112 | + |
| 113 | +Use `weakify` to safely capture `self` without retaining it. A default fallback value can be provided for when `self` has been deallocated: |
| 114 | + |
| 115 | +```swift |
| 116 | +let weakHandler = weakify(MyViewController.formatMessage, on: self, default: "N/A") |
| 117 | +``` |
| 118 | + |
| 119 | +### Weak Capture without Fallback |
| 120 | + |
| 121 | +Use `weakify` to safely capture `self` without retaining it. If the accepting closure does not need a return value, you can omit the fallback. No side effects will occur if the target is deallocated. |
| 122 | + |
| 123 | +```swift |
| 124 | +let weakHandler = weakify(MyViewController.fireAndForget, on: self) |
| 125 | +``` |
| 126 | + |
| 127 | +### Unowned Capture |
| 128 | + |
| 129 | +Use `disown` to capture `self` when the target object will definitely outlive the closure: |
| 130 | + |
| 131 | +```swift |
| 132 | +let unownedHandler = disown(MyViewController.updateStatus, on: self) |
| 133 | +``` |
| 134 | + |
| 135 | +### Heterogeneous Arguments |
| 136 | + |
| 137 | +Weakify supports methods with heterogeneous argument lists: |
| 138 | + |
| 139 | +```swift |
| 140 | +func printMessage(_ prefix: String, count: Int) { |
| 141 | + print("\(prefix): \(count)") |
| 142 | +} |
| 143 | + |
| 144 | +let printer = weakify(MyViewController.printMessage, on: self) |
| 145 | +printer("Age", 5) |
| 146 | +``` |
| 147 | + |
| 148 | +## Important Behavior |
| 149 | + |
| 150 | +* `weakify` closures evaluate the provided default when the target is deallocated. |
| 151 | +* `disown` closures will crash if called after the target is deallocated; ensure the target outlives the closure. |
| 152 | + |
| 153 | +## Contributions |
| 154 | + |
| 155 | +Weakify is not accepting source contributions at this time. Bug reports will be considered. |
| 156 | + |
| 157 | +## Author |
| 158 | + |
| 159 | +[Kyle Hughes](https://kylehugh.es) |
| 160 | + |
| 161 | +[![Bluesky][bluesky_image]][bluesky_url] |
| 162 | +[![LinkedIn][linkedin_image]][linkedin_url] |
| 163 | +[![Mastodon][mastodon_image]][mastodon_url] |
| 164 | + |
| 165 | +[bluesky_image]: https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff |
| 166 | +[bluesky_url]: https://bsky.app/profile/kylehugh.es |
| 167 | +[linkedin_image]: https://img.shields.io/badge/LinkedIn-0A66C2?logo=linkedin&logoColor=fff |
| 168 | +[linkedin_url]: https://www.linkedin.com/in/kyle-hughes |
| 169 | +[mastodon_image]: https://img.shields.io/mastodon/follow/109356914477272810?domain=https%3A%2F%2Fmister.computer&style=social |
| 170 | +[mastodon_url]: https://mister.computer/@kyle |
| 171 | + |
| 172 | +## License |
| 173 | + |
| 174 | +Weakify is available under the MIT license. |
| 175 | + |
| 176 | +See `LICENSE` for details. |
0 commit comments