|
| 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 weakly and unownedly capturing methods 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] Unowned capture for non-optional scenarios. |
| 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 | +* Xcode 16.3+ |
| 38 | + |
| 39 | +## Installation |
| 40 | + |
| 41 | +### Swift Package Manager |
| 42 | + |
| 43 | +```swift |
| 44 | +dependencies: [ |
| 45 | + .package(url: "https://github.com/kylehughes/Weakify.git", .upToNextMajor(from: "1.0.0")), |
| 46 | +] |
| 47 | +``` |
| 48 | + |
| 49 | +## Quick Start |
| 50 | + |
| 51 | +Weakly capturing a method reference: |
| 52 | + |
| 53 | +```swift |
| 54 | +import Weakify |
| 55 | + |
| 56 | +class MyViewController: UIViewController { |
| 57 | + private lazy var button: UIButton = { |
| 58 | + let button = UIButton() |
| 59 | + button.addAction( |
| 60 | + UIAction(handler: weakify(MyViewController.buttonTapped, on: self)), |
| 61 | + for: .primaryActionTriggered |
| 62 | + ) |
| 63 | + return button |
| 64 | + }() |
| 65 | + |
| 66 | + private func buttonTapped(_ action: UIAction) { |
| 67 | + print("Button tapped") |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +Unownedly capturing a method reference: |
| 73 | + |
| 74 | +```swift |
| 75 | +import Weakify |
| 76 | + |
| 77 | +class MyViewController: UIViewController { |
| 78 | + func observe(notificationCenter: NotificationCenter) { |
| 79 | + notificationCenter.addObserver( |
| 80 | + forName: .myNotification, |
| 81 | + object: nil, |
| 82 | + queue: .main, |
| 83 | + using: disown(MyViewController.handleNotification, on: self) |
| 84 | + ) |
| 85 | + } |
| 86 | + |
| 87 | + private func handleNotification(_ notification: Notification) { |
| 88 | + print("Notification received") |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +## Usage |
| 94 | + |
| 95 | +### Weak Capture with Default |
| 96 | + |
| 97 | +Use `weakify` to safely capture `self` without retaining it. A default fallback value can be provided for when `self` has been deallocated: |
| 98 | + |
| 99 | +```swift |
| 100 | +let weakHandler = weakify(MyViewController.formatMessage(_:count:), on: self, default: "N/A") |
| 101 | +``` |
| 102 | + |
| 103 | +### Unowned Capture |
| 104 | + |
| 105 | +Use `disown` when the target object will definitely outlive the closure: |
| 106 | + |
| 107 | +```swift |
| 108 | +let unownedHandler = disown(MyViewController.updateStatus(_:), on: self) |
| 109 | +``` |
| 110 | + |
| 111 | +### Heterogeneous Arguments |
| 112 | + |
| 113 | +Weakify supports methods with heterogeneous argument lists: |
| 114 | + |
| 115 | +```swift |
| 116 | +func formatMessage(_ prefix: String, count: Int) -> String { |
| 117 | + "\(prefix): \(count)" |
| 118 | +} |
| 119 | + |
| 120 | +let formatter = weakify(MyViewController.formatMessage, on: self, default: "default") |
| 121 | +formatter("Count", 5) |
| 122 | +``` |
| 123 | + |
| 124 | +## Important Behavior |
| 125 | + |
| 126 | +* `weakify` closures evaluate the provided default when the target is deallocated. |
| 127 | +* `disown` closures will crash if called after the target is deallocated; ensure the target outlives the closure. |
| 128 | + |
| 129 | +## Contributions |
| 130 | + |
| 131 | +Weakify is not accepting source contributions at this time. Bug reports will be considered. |
| 132 | + |
| 133 | +## Author |
| 134 | + |
| 135 | +[Kyle Hughes](https://kylehugh.es) |
| 136 | + |
| 137 | +[![Bluesky][bluesky_image]][bluesky_url] |
| 138 | +[![LinkedIn][linkedin_image]][linkedin_url] |
| 139 | +[![Mastodon][mastodon_image]][mastodon_url] |
| 140 | + |
| 141 | +[bluesky_image]: https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff |
| 142 | +[bluesky_url]: https://bsky.app/profile/kylehugh.es |
| 143 | +[linkedin_image]: https://img.shields.io/badge/LinkedIn-0A66C2?logo=linkedin&logoColor=fff |
| 144 | +[linkedin_url]: https://www.linkedin.com/in/kyle-hughes |
| 145 | +[mastodon_image]: https://img.shields.io/mastodon/follow/109356914477272810?domain=https%3A%2F%2Fmister.computer&style=social |
| 146 | +[mastodon_url]: https://mister.computer/@kyle |
| 147 | + |
| 148 | +## License |
| 149 | + |
| 150 | +Weakify is available under the MIT license. |
| 151 | + |
| 152 | +See `LICENSE` for details. |
0 commit comments