Skip to content

Commit 9957b43

Browse files
committed
Initial commit
0 parents  commit 9957b43

34 files changed

Lines changed: 1531 additions & 0 deletions
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Documentation
2+
3+
on:
4+
push:
5+
tags:
6+
- '*'
7+
workflow_dispatch:
8+
9+
# Set permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages.
10+
permissions:
11+
contents: read
12+
id-token: write
13+
pages: write
14+
15+
# Allow one concurrent deployment. Do not cancel in-flight deployments because we don't want assets to be in a
16+
# a semi-deployed state.
17+
concurrency:
18+
group: "deploy-documentation"
19+
cancel-in-progress: false
20+
21+
jobs:
22+
deploy-documentation:
23+
name: Deploy Documentation
24+
environment:
25+
name: github-pages
26+
url: ${{ steps.deployment.outputs.page_url }}
27+
runs-on: macos-15
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v3
31+
- name: Select Xcode
32+
run: sudo xcode-select -s /Applications/Xcode_16.3.app
33+
- name: Set Up GitHub Pages
34+
uses: actions/configure-pages@v3
35+
- name: Build Documentation
36+
run: |
37+
xcodebuild docbuild \
38+
-scheme Weakify \
39+
-derivedDataPath /tmp/DerivedData \
40+
-destination 'generic/platform=iOS';
41+
mkdir _site;
42+
$(xcrun --find docc) process-archive \
43+
transform-for-static-hosting /tmp/DerivedData/Build/Products/Debug-iphoneos/Weakify.doccarchive \
44+
--hosting-base-path Weakify \
45+
--output-path _site;
46+
- name: Create index.html
47+
run: |
48+
echo "<script>window.location.href += \"/documentation/weakify\"</script>" > _site/index.html;
49+
- name: Upload Documentation Artifact to GitHub Pages
50+
uses: actions/upload-pages-artifact@v3
51+
with:
52+
path: _site
53+
- name: Deploy to GitHub Pages
54+
id: deployment
55+
uses: actions/deploy-pages@v4

.github/workflows/test.yml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.ref_name }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
get-environment-details:
16+
strategy:
17+
matrix:
18+
include:
19+
- os: macos-15
20+
xcode: '16.3'
21+
name: Get Environment Details (Xcode ${{ matrix.xcode }})
22+
runs-on: ${{ matrix.os }}
23+
steps:
24+
- name: Select Xcode
25+
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
26+
- name: Print OS SDKs
27+
run: xcodebuild -version -sdk
28+
- name: Print simulators
29+
run: |
30+
xcrun simctl delete unavailable
31+
xcrun simctl list
32+
test:
33+
needs: get-environment-details
34+
strategy:
35+
matrix:
36+
include:
37+
- os: macos-15
38+
xcode: '16.3'
39+
platform: iOS
40+
destination: "name=iPhone 16 Pro"
41+
sdk: iphonesimulator
42+
- os: macos-15
43+
xcode: '16.3'
44+
platform: tvOS
45+
destination: "name=Apple TV 4K (3rd generation)"
46+
sdk: appletvsimulator
47+
- os: macos-15
48+
xcode: '16.3'
49+
platform: visionOS
50+
destination: "name=Apple Vision Pro"
51+
sdk: xrsimulator
52+
- os: macos-15
53+
xcode: '16.3'
54+
platform: watchOS
55+
destination: "name=Apple Watch Ultra 2 (49mm)"
56+
sdk: watchsimulator
57+
- os: macos-15
58+
xcode: '16.3'
59+
platform: macOS
60+
name: Test ${{ matrix.platform }} (Xcode ${{ matrix.xcode }})
61+
runs-on: ${{ matrix.os }}
62+
steps:
63+
- name: Checkout project
64+
uses: actions/checkout@master
65+
- name: Select Xcode
66+
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
67+
- name: Run tests (Xcode)
68+
if: matrix.platform != 'macOS'
69+
run: |
70+
set -o pipefail
71+
xcodebuild clean test -scheme Weakify -sdk ${{ matrix.sdk }} -destination "${{ matrix.destination }}" -configuration Debug -enableCodeCoverage YES | xcpretty -c
72+
- name: Run tests (Swift)
73+
if: matrix.platform == 'macOS'
74+
run: swift test

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>FILEHEADER</key>
6+
<string>
7+
// Copyright © ___YEAR___ Kyle Hughes. All rights reserved.
8+
// SPDX-License-Identifier: MIT
9+
//</string>
10+
</dict>
11+
</plist>

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2025 Kyle Hughes
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Package.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// swift-tools-version: 6.1
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "Weakify",
7+
products: [
8+
.library(
9+
name: "Weakify",
10+
targets: [
11+
"Weakify",
12+
]
13+
),
14+
],
15+
targets: [
16+
.target(
17+
name: "Weakify"
18+
),
19+
.testTarget(
20+
name: "WeakifyTests",
21+
dependencies: [
22+
"Weakify",
23+
]
24+
),
25+
]
26+
)

README.md

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Weakify
2+
3+
[![Platform Versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fkylehughes%2FWeakify%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/kylehughes/Weakify)
4+
[![Swift Versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fkylehughes%2FWeakify%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/kylehughes/Weakify)
5+
[![Test](https://github.com/kylehughes/Weakify/actions/workflows/test.yml/badge.svg)](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

Comments
 (0)