mirror of
https://github.com/EmergeTools/Pow.git
synced 2026-03-25 08:55:50 +00:00
Co-authored-by: Robert Böhnke <robb@robb.is> Co-authored-by: Kasper Lahti <kasper@lahti.email>
263 lines
8.9 KiB
Swift
263 lines
8.9 KiB
Swift
import SwiftUI
|
|
import simd
|
|
|
|
public extension AnyTransition.MovingParts {
|
|
/// A transition that moves the view from the specified edge of the on
|
|
/// insertion and towards it on removal.
|
|
static func move(edge: Edge) -> AnyTransition {
|
|
return .modifier(
|
|
active: Scaled(Move(edge: edge)),
|
|
identity: Scaled(Move())
|
|
)
|
|
}
|
|
|
|
/// A transition that moves the view at the specified angle.
|
|
///
|
|
/// The angle is relative to the current `layoutDirection`, such that 0° represents animating towards the trailing edge on insertion and 90° represents inserting towards the bottom edge.
|
|
///
|
|
/// In this example, the view insertion is animated by moving it towards the top trailing corner and the removal is animated by moving it towards the bottom edge.
|
|
///
|
|
/// ```swift
|
|
/// Text("Hello")
|
|
/// .transition(
|
|
/// .asymmetric(
|
|
/// insertion: .movingParts.move(angle: .degrees(45)),
|
|
/// removal: .movingParts.move(angle: .degrees(90))
|
|
/// )
|
|
/// )
|
|
/// ```
|
|
///
|
|
/// - Parameter angle: The direction of the animation.
|
|
static func move(angle: Angle) -> AnyTransition {
|
|
return .modifier(
|
|
active: Scaled(Move(angle: angle)),
|
|
identity: Scaled(Move())
|
|
)
|
|
}
|
|
}
|
|
|
|
internal struct Move: GeometryEffect, Animatable {
|
|
/// Translation is relative, depth is ignored, anchor is always
|
|
/// `UnitPoint(0.5, 0.5)`.
|
|
var animatableData: TRS = .identity
|
|
|
|
init(edge: Edge) {
|
|
switch edge {
|
|
case .top:
|
|
animatableData.translation.y = -1
|
|
case .leading:
|
|
animatableData.translation.x = -1
|
|
case .bottom:
|
|
animatableData.translation.y = 1
|
|
case .trailing:
|
|
animatableData.translation.x = 1
|
|
}
|
|
}
|
|
|
|
init() {}
|
|
|
|
init(angle: Angle) {
|
|
let u = cos(angle.radians)
|
|
let v = sin(angle.radians)
|
|
|
|
let u_2: Double = pow(u, 2)
|
|
let v_2: Double = pow(v, 2)
|
|
let sq2: Double = sqrt(2.0)
|
|
|
|
let x: Double = 0.5 * sqrt(abs(2.0 + u_2 - v_2 + 2.0 * u * sq2)) - 0.5 * sqrt(abs(2.0 + u_2 - v_2 - 2.0 * u * sq2))
|
|
let y: Double = 0.5 * sqrt(abs(2.0 - u_2 + v_2 + 2.0 * v * sq2)) - 0.5 * sqrt(abs(2.0 - u_2 + v_2 - 2.0 * v * sq2))
|
|
|
|
animatableData.translation.x = -x
|
|
animatableData.translation.y = -y
|
|
}
|
|
|
|
private var trs: TRS {
|
|
get { animatableData }
|
|
set { animatableData = newValue }
|
|
}
|
|
|
|
func effectValue(size: CGSize) -> ProjectionTransform {
|
|
let anchor = UnitPoint.center
|
|
|
|
let offset = simd_double4x4(translationX: size.width * anchor.x, y: size.height * anchor.y)
|
|
|
|
let translation = simd_double4x4(translationX: trs.translation.x * size.width, y: trs.translation.y * size.height, z: 0)
|
|
|
|
let rotation = simd_double4x4(trs.rotation.normalized)
|
|
|
|
let scale = simd_double4x4(scaleX: trs.scale.x, y: trs.scale.y, z: 1)
|
|
|
|
return ProjectionTransform((((offset * translation) * rotation) * scale) * offset.inverse)
|
|
}
|
|
}
|
|
|
|
#if os(iOS) && DEBUG
|
|
@available(iOS 15.0, *)
|
|
struct Move_Previews: PreviewProvider {
|
|
struct Preview: View {
|
|
@State
|
|
var indices: [UUID] = [UUID()]
|
|
|
|
enum DirectionType: String, Hashable, Identifiable, CaseIterable {
|
|
case edge = "Edge"
|
|
case angle = "Angle"
|
|
|
|
var name: String {
|
|
return rawValue
|
|
}
|
|
|
|
var id: Self {
|
|
return self
|
|
}
|
|
}
|
|
|
|
@State
|
|
var directionType: DirectionType = .edge
|
|
|
|
@State
|
|
var edge: Edge = .leading
|
|
|
|
@State
|
|
var angle: Angle = .degrees(0)
|
|
|
|
@State
|
|
var isRightToLeft: Bool = false
|
|
|
|
func makeTransition() -> AnyTransition {
|
|
switch directionType {
|
|
case .edge:
|
|
return .movingParts.move(edge: edge)
|
|
case .angle:
|
|
return .movingParts.move(angle: angle)
|
|
}
|
|
}
|
|
|
|
var resolvedAngle: Angle {
|
|
switch directionType {
|
|
case .edge:
|
|
switch edge {
|
|
case .top:
|
|
return .degrees(90)
|
|
case .leading:
|
|
return .degrees(0)
|
|
case .bottom:
|
|
return .degrees(270)
|
|
case .trailing:
|
|
return .degrees(180)
|
|
}
|
|
case .angle:
|
|
return angle
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Swoosh")
|
|
.bold()
|
|
|
|
Text("myView.transition(**.movingParts.move**)")
|
|
}
|
|
.font(.footnote.monospaced())
|
|
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .leading)
|
|
.padding()
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
|
.fill(.thickMaterial)
|
|
)
|
|
|
|
Stepper {
|
|
Text("View Count ") + Text("(\(indices.count))").foregroundColor(.secondary)
|
|
} onIncrement: {
|
|
withAnimation(.spring()) {
|
|
indices.append(UUID())
|
|
}
|
|
} onDecrement: {
|
|
if !indices.isEmpty {
|
|
let _ = withAnimation {
|
|
indices.removeLast()
|
|
}
|
|
}
|
|
}
|
|
|
|
Toggle("Right To Left", isOn: $isRightToLeft)
|
|
|
|
if #available(iOS 16.0, *) {
|
|
Picker("Type", selection: $directionType) {
|
|
ForEach(DirectionType.allCases) { type in
|
|
Text(type.name).tag(type)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
|
|
switch directionType {
|
|
case .edge:
|
|
LabeledContent("Edge") {
|
|
Picker("Edge", selection: $edge) {
|
|
Group {
|
|
Text("Leading").tag(Edge.leading)
|
|
Text("Trailing").tag(Edge.trailing)
|
|
Text("Top").tag(Edge.top)
|
|
Text("Bottom").tag(Edge.bottom)
|
|
}
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
.frame(height: 44)
|
|
case .angle:
|
|
LabeledContent("Angle") {
|
|
AngleControl(angle: $angle)
|
|
}
|
|
.frame(height: 44)
|
|
}
|
|
|
|
LabeledContent("Reference") {
|
|
Image(systemName: "arrow.forward.circle")
|
|
.imageScale(.large)
|
|
.rotationEffect(resolvedAngle)
|
|
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
|
|
}
|
|
}
|
|
|
|
let columns: [GridItem] = [
|
|
.init(.flexible()),
|
|
.init(.flexible())
|
|
]
|
|
|
|
LazyVGrid(columns: columns) {
|
|
ForEach(indices, id: \.self) { uuid in
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.fill(Color.accentColor)
|
|
|
|
Text("Hello\nWorld!")
|
|
.foregroundColor(.white)
|
|
.multilineTextAlignment(.center)
|
|
.font(.system(.title, design: .rounded))
|
|
|
|
}
|
|
.transition(
|
|
makeTransition().combined(with: .opacity)
|
|
)
|
|
.aspectRatio(1.1, contentMode: .fit)
|
|
.id(uuid)
|
|
}
|
|
}
|
|
.environment(\.layoutDirection, isRightToLeft ? .rightToLeft : .leftToRight)
|
|
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
|
|
static var previews: some View {
|
|
NavigationView {
|
|
Preview()
|
|
.navigationBarHidden(true)
|
|
}
|
|
}
|
|
}
|
|
#endif
|