Skip to content

Instantly share code, notes, and snippets.

@ryanlintott
Last active June 18, 2024 17:23
Show Gist options
  • Save ryanlintott/5e065dde4838848f2f6e95323fe26ada to your computer and use it in GitHub Desktop.
Save ryanlintott/5e065dde4838848f2f6e95323fe26ada to your computer and use it in GitHub Desktop.
AnimatablePair is limited to only two values. You can nest AnimatablePair to add more values but theres a lot of repetition and using .first.second.first etc… when accessing the values. Now that we have access to Swift 6 with parameter pack iteration we could have an AnimatablePack. Similar to how view builders are no longer limited to 10 views …
//
// AnimatablePack.swift
// ShapeUp
//
// Created by Ryan Lintott on 2023-08-02.
//
/// AnimatablePack uses parameter pack iteration that is only available in swift 6.0
/// https://forums.swift.org/t/pitch-enable-pack-iteration/66168
#if compiler(>=6.0)
import SwiftUI
/**
A parameter pack implementation of `AnimatablePair`
Conforming to Animatable with AnimatablePair:
```swift
struct MyShape: Animatable {
var animatableData: AnimatablePair<CGFloat, AnimatablePair<RelatableValue, Double>> {
get { AnimatablePair(insetAmount, AnimatablePair(cornerRadius, rotation)) }
set {
insetAmount = newValue.first
cornerRadius = newValue.second.first
rotation = newValue.second.second
}
}
}
```
Conforming to Animatable with AnimatablePack:
```swift
struct MyShape: Animatable {
var animatableData: AnimatablePack<CGFloat, RelatableValue, Double> {
get { AnimatablePack(insetAmount, cornerRadius, rotation) }
set { (insetAmount, cornerRadius, rotation) = newValue() }
}
}
```
*/
@available(iOS 17, macOS 14, watchOS 10, tvOS 17, *)
@dynamicMemberLookup
public struct AnimatablePack<each Item: VectorArithmetic>: VectorArithmetic {
/// Pack of items that conform to `VectorArithmetic`
public var item: (repeat each Item)
/// Creates an `Animatable` pack of items
/// - Parameter item: Pack of items that conform to `VectorArithmetic`
public init(_ item: repeat each Item) {
self.item = (repeat each item)
}
/// Access elements in the same was as a tuple using pack.1, pack.2, etc...
public subscript<V>(dynamicMember keyPath: WritableKeyPath<(repeat each Item), V>) -> V {
get { item[keyPath: keyPath] }
set { item[keyPath: keyPath] = newValue }
}
public func callAsFunction() -> (repeat each Item) {
item
}
}
@available(iOS 17, macOS 14, watchOS 10, tvOS 17, *)
extension AnimatablePack: Sendable where repeat each Item: Sendable { }
@available(iOS 17, macOS 14, watchOS 10, tvOS 17, *)
public extension AnimatablePack {
static var zero: Self {
.init(repeat (each Item).zero)
}
static func + (lhs: Self, rhs: Self) -> Self {
.init(repeat (each lhs.item) + (each rhs.item))
}
static func - (lhs: Self, rhs: Self) -> Self {
.init(repeat (each lhs.item) - (each rhs.item))
}
mutating func scale(by rhs: Double) {
item = (repeat (each item).scaled(by: rhs))
}
static func == (lhs: Self, rhs: Self) -> Bool {
(lhs - rhs).magnitudeSquared == .zero
}
var magnitudeSquared: Double {
var value = 0.0
for item in repeat each item {
value += item.magnitudeSquared
}
return value
}
}
#endif
//
// AnimatablePackExampleView.swift
// AnimatablePackExample
//
// Created by Ryan Lintott on 2024-06-17.
//
import SwiftUI
struct AnimatablePackExampleView: View {
@State private var pointHeight: CGFloat = 100
@State private var topTaper: CGFloat = 0
@State private var bottomTaper: CGFloat = 50
var body: some View {
VStack {
Text("Both pentagons are animatable by all three properties at the same time but one was much easier to write.")
VStack {
Pentagon1(pointHeight: pointHeight, topTaper: topTaper, bottomTaper: bottomTaper)
.fill(.red)
Pentagon2(pointHeight: pointHeight, topTaper: topTaper, bottomTaper: bottomTaper)
.fill(.blue)
}
.animation(.default.speed(0.3), value: pointHeight)
.animation(.default.speed(0.3), value: topTaper)
.animation(.default.speed(0.3), value: bottomTaper)
Stepper("Point Height \(pointHeight.formatted())", value: $pointHeight, in: 0...200, step: 20)
Stepper("Top Taper \(topTaper.formatted())", value: $topTaper, in: 0...200, step: 20)
Stepper("Bottom Taper \(bottomTaper.formatted())", value: $bottomTaper, in: 0...200, step: 20)
}
.padding()
}
}
struct Pentagon1: Shape {
var pointHeight: CGFloat
var topTaper: CGFloat
var bottomTaper: CGFloat
/// Notice how the AnimatablePair becomes difficult to write with more than two arguments and likely extremely difficult with many.
var animatableData: AnimatablePair<CGFloat, AnimatablePair<CGFloat, CGFloat>> {
get {
.init(pointHeight, .init(topTaper, bottomTaper))
}
set {
pointHeight = newValue.first
topTaper = newValue.second.first
bottomTaper = newValue.second.second
}
}
public init(pointHeight: CGFloat, topTaper: CGFloat, bottomTaper: CGFloat) {
self.pointHeight = pointHeight
self.topTaper = topTaper
self.bottomTaper = bottomTaper
}
public nonisolated func path(in rect: CGRect) -> Path {
var path = Path()
path.addLines([
CGPoint(x: rect.minX + bottomTaper, y: rect.maxY),
CGPoint(x: rect.minX + topTaper, y: rect.minY + pointHeight),
CGPoint(x: rect.midX, y: rect.minY),
CGPoint(x: rect.maxX - topTaper, y: rect.minY + pointHeight),
CGPoint(x: rect.maxX - bottomTaper, y: rect.maxY)
])
return path
}
}
struct Pentagon2: Shape {
var pointHeight: CGFloat
var topTaper: CGFloat
var bottomTaper: CGFloat
/// With AnimatablePack you only need to remember the order of the arguements in order to list them.
var animatableData: AnimatablePack<CGFloat, CGFloat, CGFloat> {
get {
.init(pointHeight, topTaper, bottomTaper)
}
set {
(pointHeight, topTaper, bottomTaper) = newValue()
}
}
public init(pointHeight: CGFloat, topTaper: CGFloat, bottomTaper: CGFloat) {
self.pointHeight = pointHeight
self.topTaper = topTaper
self.bottomTaper = bottomTaper
}
public nonisolated func path(in rect: CGRect) -> Path {
var path = Path()
path.addLines([
CGPoint(x: rect.minX + bottomTaper, y: rect.maxY),
CGPoint(x: rect.minX + topTaper, y: rect.minY + pointHeight),
CGPoint(x: rect.midX, y: rect.minY),
CGPoint(x: rect.maxX - topTaper, y: rect.minY + pointHeight),
CGPoint(x: rect.maxX - bottomTaper, y: rect.maxY)
])
return path
}
}
#Preview {
AnimatablePackExampleView()
}
@ole
Copy link

ole commented Jun 18, 2024

Very cool! Suggestion: add a @dynamicMemberLookup subscript. Like this:

@dynamicMemberLookup
public struct AnimatablePack<…> {
    
    public subscript<V> (dynamicMember keyPath: WritableKeyPath<(repeat each Item), V>) -> V {
        get { item[keyPath: keyPath] }
        set { item[keyPath: keyPath] = newValue }
    }
    
}

With this you can access the pack elements like on a normal tuple:

var pack: AnimatablePack<CGFloat, CGFloat, CGFloat> = .init(1, 2, 3)
pack.1 += 100
// pack is now (1, 102, 3)

@ryanlintott
Copy link
Author

That's a great idea, I will! Thanks!

@ole
Copy link

ole commented Jun 18, 2024

Nice, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment