Large Swift values are wonderfully easy to pass around. That ease can hide a painful performance story: a request pipeline looks value-oriented and clean, yet Instruments shows allocator churn and long stretches inside buffer-copying code.
The right response is not to replace every struct with a class. It is to find the ownership boundary that forces a copy, express the access you actually need, and measure again. Swift’s ownership model gives us a progression of tools for doing that—from borrowing and consuming, which are already established language features, to preview work associated with Swift 6.4 around yielding accessors, unique containers, safe references, and borrowing iteration.
This article follows one realistic problem: an image-processing job carries a large byte buffer through several layers. We will profile it, remove accidental ownership transfers, and evaluate where the proposed Swift 6.4-era tools might eventually earn their complexity.
One important status note: as of September 14, 2026, the Swift Evolution release index lists Swift 6.4 as announced but not released. The examples labeled stable use established ownership features. Everything else is preview material: some proposals are accepted but not implemented, and some are available only in recent development snapshots or behind experimental flags. Do not plan a production migration around them until a released toolchain and your shipping Xcode provide the exact APIs.
Now that the release is available, use the Swift 6.4 migration guide for production projects to verify which ownership APIs shipped and plan a reversible toolchain rollout.
1. Start With a Copy You Can Prove
Imagine a photo editor that decodes a large payload, validates it, calculates a histogram, and finally writes metadata:
struct PixelBuffer {
var bytes: [UInt8]
var width: Int
var height: Int
}
func checksum(of buffer: PixelBuffer) -> UInt64 {
buffer.bytes.reduce(0) { partial, byte in
partial &+ UInt64(byte)
}
}
func normalize(_ buffer: PixelBuffer) -> PixelBuffer {
var result = buffer
guard let maximum = result.bytes.max(), maximum > 0 else {
return result
}
for index in result.bytes.indices {
result.bytes[index] = UInt8(
(UInt16(result.bytes[index]) * 255) / UInt16(maximum)
)
}
return result
}
The source is not proof of a physical copy. Array uses copy-on-write (COW), and the optimizer can eliminate temporary values. But result = buffer creates another logical owner of the array storage. The first mutation of result.bytes must preserve buffer, so it can allocate and copy the entire buffer.
For a 48-megabyte image, that one uniqueness transition matters. For a tiny settings model, it probably does not. This is why ownership work begins in Instruments, not in a style guide.
Create a release-build benchmark that reflects production sizes:
import Foundation
@inline(never)
func benchmark(label: String, iterations: Int, operation: () -> Void) {
let clock = ContinuousClock()
let elapsed = clock.measure {
for _ in 0..<iterations {
autoreleasepool(invoking: operation)
}
}
print("\(label): \(elapsed)")
}
let sample = PixelBuffer(
bytes: Array(repeating: 127, count: 48 * 1_024 * 1_024),
width: 4_000,
height: 3_000
)
benchmark(label: "normalize", iterations: 10) {
_ = normalize(sample)
}
Record wall time, peak memory, allocations, and their call stacks. Keep the result observable so optimization cannot remove the work, and compare variants on the same machine and toolchain.
If you need a refresher on why arrays behave this way, the discussion of value semantics and copy-on-write in Swift is the right foundation. Ownership controls when access is shared, transferred, or exclusive; it does not repeal COW.
2. Express Read-Only Access With borrowing
The checksum function reads its argument and does not retain it. Make that contract explicit:
func checksum(of buffer: borrowing PixelBuffer) -> UInt64 {
buffer.bytes.reduce(0) { partial, byte in
partial &+ UInt64(byte)
}
}
This stable syntax comes from SE-0377. A borrowing parameter receives shared access for the duration of the call. The callee cannot consume or mutate it, and the caller can continue using it after the function returns.
For ordinary copyable parameters, Swift often borrows without being told. Explicit borrowing can document a hot API boundary and prevent the function body from introducing an implicit copy. It does not promise that no machine bytes move; optimizer choices remain implementation details.
Nor can a borrow always be extended. Swift’s exclusivity rule prevents overlapping a shared read with mutation of the same storage. Global variables, escaped closure captures, and class properties may require defensive copies because the compiler cannot prove that the callee will not reach back and mutate the original. If adding borrowing produces an exclusivity diagnostic, treat it as design information instead of fighting the compiler.
Borrowing is not retention
Do not store a borrowed parameter in an escaping closure or return a reference to its internals. The access ends with the call. If downstream work must outlive that scope, it needs ownership: copy a copyable value deliberately, transfer it with consuming, or redesign the lifetime.
That distinction also matters across tasks. A borrow is not a shortcut around Sendable or actor isolation. For concurrent API design, pair this article with Swift 6 concurrency-safe choices.
3. Transfer Ownership With consuming
Our normalizer returns a replacement and has no reason to preserve the input. Say so:
func normalize(_ buffer: consuming PixelBuffer) -> PixelBuffer {
var result = buffer
guard let maximum = result.bytes.max(), maximum > 0 else {
return result
}
for index in result.bytes.indices {
result.bytes[index] = UInt8(
(UInt16(result.bytes[index]) * 255) / UInt16(maximum)
)
}
return result
}
var frame = PixelBuffer(
bytes: Array(repeating: 127, count: 48 * 1_024 * 1_024),
width: 4_000,
height: 3_000
)
frame = normalize(frame)
consuming is also stable SE-0377 syntax. The function takes ownership of the argument. In an optimized build, the uniquely owned array buffer can often continue into result and mutate without a COW allocation.
The key word is often. If another live value shares the same array storage, mutation still needs uniqueness:
let archived = frame
frame = normalize(frame) // archived must keep its original bytes
This explains the limit of COW: it makes copies lazy, not impossible. The copy happens precisely when value semantics require two independently mutable histories.
consuming is excellent for pipeline stages, builders, encoders, and terminal operations. It is poor for a general helper whose callers reasonably expect to reuse their argument. Ownership is part of the API contract; choose it according to meaning, then verify the performance consequence.
4. Use inout for a Mutation, Not a Transfer
When the caller owns the same logical buffer before and after the operation, inout is clearer:
func normalizeInPlace(_ buffer: inout PixelBuffer) {
guard let maximum = buffer.bytes.max(), maximum > 0 else {
return
}
for index in buffer.bytes.indices {
buffer.bytes[index] = UInt8(
(UInt16(buffer.bytes[index]) * 255) / UInt16(maximum)
)
}
}
normalizeInPlace(&frame)
inout grants exclusive access for the call and requires the value to be initialized when the call finishes, including throwing paths. This can avoid a needless return-and-reassign shape, but it does not guarantee unique array storage. A previously made copy can still trigger COW at the first mutation.
You cannot read or modify frame while it is passed inout, and overlapping access to the same value can fail exclusivity checking. Prefer small, obvious mutation scopes.
5. Make Illegal Copies Impossible With ~Copyable
For a resource that must have one owner—an open file, a lock token, or a mutable image workspace—performance and correctness may point in the same direction. Noncopyable structs and enums arrived in Swift 5.9 through SE-0390:
struct RenderWorkspace: ~Copyable {
var pixels: [UInt8]
init(pixels: consuming [UInt8]) {
self.pixels = pixels
}
borrowing func checksum() -> UInt64 {
pixels.reduce(0) { $0 &+ UInt64($1) }
}
consuming func finish() -> [UInt8] {
pixels
}
}
var workspace = RenderWorkspace(pixels: [10, 20, 30])
print(workspace.checksum())
let output = workspace.finish()
// workspace is no longer usable.
This is stable ownership syntax, though exact diagnostics and the explicit consume operator depend on the selected language mode. ~Copyable suppresses the implicit Copyable conformance. It does not magically put the value on the stack or eliminate every allocation; the array inside still owns heap storage. What it does is make accidental duplication of the workspace a compile-time error.
Noncopyable values cannot flow through generic APIs that assume Copyable, and ownership must be explicit at boundaries. Adopt them for unique resources or verified hot paths, not as a blanket preference for structs.
6. Preview Yielding Accessors Remove Getter Copies
A computed property traditionally returns an owned value. A get/set pair may therefore fetch a large value, mutate a temporary, and write it back. SE-0474 introduces yielding borrow and yielding mutate accessors that lend access instead. Status: accepted, not part of a released Swift 6.4 toolchain; partially available on recent main snapshots with -enable-experimental-feature CoroutineAccessors.
The following is SE-0474 preview syntax, not production code for a released Xcode:
struct ImagePlane {
private var storage: PixelBuffer
init(storage: consuming PixelBuffer) {
self.storage = storage
}
var pixels: PixelBuffer {
yielding borrow {
yield storage
}
yielding mutate {
yield &storage
}
}
}
var plane = ImagePlane(storage: frame)
print(checksum(of: plane.pixels))
normalizeInPlace(&plane.pixels)
The accessor is a yield-once coroutine. yielding borrow lends shared access; yielding mutate lends exclusive access, suspending the accessor until the caller finishes. The compiler requires every reachable path to yield exactly once.
Use this when the API exposes stored large or noncopyable state. Keep get when the property computes a fresh result: borrowing a temporary adds coroutine overhead and prevents the caller from consuming the result. Also remember that replacing get with yielding borrow is an API and ABI decision, while mutation accessors have different compatibility rules. This is library design, not just micro-optimization.
7. Choose Unique Storage Deliberately
The ownership roadmap includes containers for cases where shared COW storage is the wrong semantic model. These APIs do not share one availability state, so evaluate them individually.
UniqueBox
SE-0517 defines UniqueBox<Value>, a noncopyable smart pointer that uniquely owns a heap allocation. Status: accepted, with a linked implementation pull request, but not marked Implemented by Swift Evolution and unavailable in a released Swift toolchain. This could eventually help when a large inline value benefits from a stable heap address without shared class ownership:
// SE-0517 proposal example; not available in a released toolchain.
var workspace = UniqueBox(PixelBuffer(
bytes: [10, 20, 30],
width: 1,
height: 1
))
workspace.value.bytes[0] = 255
let finished = workspace.consume()
UniqueBox still allocates. Its intended benefit is explicit unique ownership, automatic cleanup, and in-place value access—not “zero allocation.” Because SE-0517 is accepted but not marked Implemented, use this to reason about future API design rather than as a production dependency.
UniqueArray
SE-0527 introduces UniqueArray and RigidArray in the Containers module. Status: implemented for Swift 6.4, which is still unreleased at the time of writing. UniqueArray is a dynamically resizing, contiguous array with unique rather than COW storage, and it can hold noncopyable elements:
// Swift 6.4 development toolchain with the matching Containers module.
import Containers
var bytes = UniqueArray(copying: [10, 20, 30])
bytes.append(40)
bytes[0] = 255
let independentCopy = bytes.clone()
Unlike Array, copying is not implicit. clone() makes the expensive operation visible. That can be a powerful review signal in a rendering or media pipeline, but Array remains the better default when value sharing, ecosystem compatibility, and source stability matter more than strict ownership.
The proposal’s adoption notes require the toolchain that introduces these types. Until Swift 6.4 is released and integrated into a shipping Xcode, treat this as development-snapshot experimentation rather than deployable app code.
8. Use Ref and MutableRef Without Escaping Lifetimes
SE-0519 adds safe first-class references. Ref<T> represents shared access; MutableRef<T> represents exclusive mutable access. Both are non-escapable, so the compiler prevents them from outliving the target. Status: implemented for Swift 6.4, currently available only in recent main development snapshots according to the proposal.
// Swift 6.4 development toolchain.
func accumulate(_ values: [Int], into totals: inout [String: Int]) {
var total = MutableRef(&totals["samples", default: 0])
for value in values {
total.value += value
}
}
This performs the dictionary lookup once and repeatedly mutates the projected entry. A Ref can similarly name a borrowed nested value without transferring ownership.
These are not safer drop-in pointers that can be stored anywhere. The reference carries a lifetime dependency, and MutableRef holds exclusive access to its target. While it is alive, direct use of that target is restricted. References projected through nontrivial get/set or coroutine accessors can be limited to the immediate caller because teardown must still occur.
SE-0519’s implementation status is not a production availability guarantee. Isolate experiments, compile them in CI with the same development snapshot, and do not claim deployment support until a released shipping Xcode confirms it.
9. Borrow-Based Iteration Is About Bulk Access
Traditional Sequence iteration produces owned elements. That model does not fit a container of noncopyable values and can be inefficient when each element should only be inspected. SE-0516 proposes Iterable, a borrowing iteration model that lends spans of elements for bulk processing. Status: accepted, with development implementation pull requests and a proposal-linked downloadable snapshot, but not part of a released Swift toolchain.
This is proposal-level code that requires the development snapshot linked from SE-0516 and has not been validated against a released toolchain. It includes the proposal’s Element and typed-throws Failure primary associated types and uses its supported for-in surface:
// Requires the development snapshot linked from SE-0516.
func totalLuminance<C: Iterable<UInt8, Never>>(
_ pixels: borrowing C
) -> UInt64 {
var total: UInt64 = 0
for byte in pixels {
total &+= UInt64(byte)
}
return total
}
The important idea is that iteration can borrow contiguous chunks instead of producing a separately owned value for every step. This helps vectorization, reduces iterator overhead, and enables traversal of noncopyable elements. It does not mean every loop gets faster; Array iteration is already highly optimized, and abstraction or coroutine costs can dominate small workloads.
SE-0516’s design also documents lifetime limits: an element borrow generally cannot escape the iterator that provides it. Do not design an API that returns a borrowed element from a helper unless the lifetime model explicitly supports that result.
10. Benchmark Each Ownership Boundary
Re-run the original workload after each change, not only after the final rewrite. A useful experiment has four variants:
- The original by-value pipeline.
- Explicit
borrowingfor read-only stages. - A
consumingorinoutmutation stage. - A development-snapshot experiment for an implemented Swift 6.4 feature, kept behind a dedicated target.
Measure an optimized build with identical input and iteration counts. Use Allocations to count large buffer allocations, Time Profiler to locate retains, releases, and memory moves, and XCTest or Swift Benchmark for repeatable statistics. Report distributions rather than one lucky run. If the ownership-heavy version is not materially faster or uses no less memory, keep the simpler API.
After deploying the optimization, track production CPU, memory, and disk regressions with MetricKit so device-wide results can challenge your laboratory benchmark.
Also inspect code size and call-site ergonomics. A UniqueBox may reduce movement of a large inline aggregate while adding heap allocation. A yielding accessor may avoid a copy while introducing coroutine overhead. A noncopyable container may clarify ownership while forcing adapters at Foundation or UIKit boundaries. Performance is a system property.
For a broader view of heap traffic, retain/release behavior, and Instruments, see iOS memory management from ARC to retain cycles. For packaging experimental ownership code away from the app target, the SPM modularization guide provides a practical boundary.
11. A Production Adoption Checklist
Before merging ownership-oriented code, ask:
- Did a release-build profile identify a meaningful copy or allocation?
- Does
borrowing,consuming, orinoutaccurately describe the API semantics? - Can callers still use the API without surprising lifetime gymnastics?
- Are exclusivity scopes short and obvious?
- Is a noncopyable type modeling genuine unique ownership?
- Is the feature present in a released shipping toolchain, rather than merely accepted or implemented on
main? - Are preview APIs isolated so they can change without infecting the whole codebase?
- Did benchmarks improve on representative devices, not just a developer Mac?
The best ownership optimization often looks modest: one read-only boundary becomes borrowing, one transform becomes consuming, and an unnecessary alias disappears. Reach for unique containers and first-class references only after the simpler moves have earned their keep.
12. Key Takeaways
- Copy-on-write postpones copying; it cannot avoid a copy when two values must diverge.
borrowingexpresses temporary shared access, whileconsumingtransfers ownership andinoutgrants temporary exclusive access.~Copyableprevents implicit copies and is best for genuinely unique resources or measured hot paths.- SE-0474 yielding accessors are accepted preview work that can expose stored values without getter/setter copies; partial support requires a development snapshot and experimental flag.
UniqueBox,UniqueArray,Ref,MutableRef, and borrowing iteration solve different ownership problems; none is a universal faster replacement.- Swift 6.4 remains announced but unreleased, and proposal status is not production availability. Validate against the compiler you actually ship.
- The benchmark—not the annotation count—is the final authority.
13. Primary Sources
- SE-0377: Parameter ownership modifiers
- SE-0390: Noncopyable structs and enums
- SE-0474: Yielding accessors
- SE-0516: Borrowing iteration
- SE-0517: UniqueBox
- SE-0519: Ref and MutableRef
- SE-0527: RigidArray and UniqueArray
14. Conclusion
Ownership features let Swift remain a high-level, safe language while giving performance-sensitive code a more precise vocabulary. Use the stable vocabulary—borrowing, consuming, inout, and carefully chosen noncopyable types—to describe reality today. Treat the Swift 6.4 proposals as a preview of where that model is heading, not as a production checklist.
When an API’s ownership contract matches the work it performs, the optimizer gets better information, reviewers can see expensive transfers, and value-oriented code can avoid copies it never needed. Profile first, make the smallest semantic change, and measure again. That discipline will outlast any individual preview API.