Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// MatchGeometryEffectUITests.swift
// OpenSwiftUIUITests

import Testing
import SnapshotTesting

@MainActor
@Suite(.snapshots(record: .never, diffTool: diffTool))
struct MatchGeometryEffectExampleTests {
@Test
func axisChange() {
struct ContentView: AnimationTestView {
nonisolated static var model: AnimationTestModel {
AnimationTestModel(duration: 2, count: 5)
}

@State private var isVertical = true
@Namespace private var animation

var body: some View {
VStack {
if isVertical {
VStack {
Ellipse()
.fill(.red)
.matchedGeometryEffect(id: "ellipse", in: animation)
Rectangle()
.fill(.blue)
.matchedGeometryEffect(id: "rectangle", in: animation, properties: .size)
.transition(.opacity)
}
} else {
HStack {
Ellipse()
.fill(.red)
.matchedGeometryEffect(id: "ellipse", in: animation)
Rectangle()
.fill(.blue)
.matchedGeometryEffect(id: "rectangle", in: animation, properties: .size)
.transition(.opacity)
}

}
}
.onAppear {
withAnimation(.easeInOut(duration: 2)) {
isVertical.toggle()
}
}
}
}
// When run seperately, precision: 1.0 works fine
// but in the full suite, it will hit the same issue of #340
withKnownIssue("#690", isIntermittent: true) {
openSwiftUIAssertAnimationSnapshot(
of: ContentView()
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// MatchGeometryEffectExample.swift
// Shared

#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif

struct MatchGeometryEffectExample: View {
@State private var isVertical = true
@Namespace private var animation

var body: some View {
VStack {
if isVertical {
VStack {
Ellipse()
.fill(.red)
.matchedGeometryEffect(id: "ellipse", in: animation)
Rectangle()
.fill(.blue)
.matchedGeometryEffect(id: "rectangle", in: animation, properties: .size)
.transition(.opacity)
}
} else {
HStack {
Ellipse()
.fill(.red)
.matchedGeometryEffect(id: "ellipse", in: animation)
Rectangle()
.fill(.blue)
.matchedGeometryEffect(id: "rectangle", in: animation, properties: .size)
.transition(.opacity)
}

}
}
.onAppear {
withAnimation(.easeInOut(duration: 2)) {
isVertical.toggle()
}
}
}
}
138 changes: 131 additions & 7 deletions Sources/OpenSwiftUICore/Layout/View/ViewTransform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public struct ViewTransform: Equatable, CustomStringConvertible {
var stop = false
if inverted {
if pendingTranslation != .zero {
body(.translation(pendingTranslation), &stop)
body(.translation(-pendingTranslation), &stop)
if stop { return }
}
var element: AnyElement = head
Expand All @@ -265,7 +265,7 @@ public struct ViewTransform: Equatable, CustomStringConvertible {
) { bufferPointer in
bufferPointer.initializeElement(at: 0, to: head)
var element = head
var index = 0
var index = 1
while let next = element.next {
bufferPointer.initializeElement(at: index, to: next)
element = next
Expand Down Expand Up @@ -296,26 +296,119 @@ public struct ViewTransform: Equatable, CustomStringConvertible {
} else if case .local = space2 {
return true
} else {
let name1: CoordinateSpace.Name = switch space1 {
case let .named(name): .name(name)
case let .id(id): .id(id)
default: _openSwiftUIUnreachableCode()
}
let name2: CoordinateSpace.Name = switch space2 {
case let .named(name): .name(name)
case let .id(id): .id(id)
default: _openSwiftUIUnreachableCode()
}
var found1 = false
var found2 = false
forEach(inverted: false) { item, stop in
// TODO
switch item {
case let .coordinateSpace(name), let .sizedSpace(name, _):
found1 = name1 == name
found2 = name2 == name
default:
break
}
stop = found1 || found2
}
_openSwiftUIUnimplementedFailure()
return found1 && !found2
}
}

package func convert(_ conversion: ViewTransform.Conversion, _ body: (ViewTransform.Item) -> Void) {
guard !isEmpty else { return }
_openSwiftUIUnimplementedFailure()
let conversion = conversion.normalized()

var active = false
switch conversion {
case let .rootToSpace(space):
guard space != .root else { return }
active = true
case let .spaceToRoot(space):
active = space == .local
case let .localToSpace(space):
guard space != .local else { return }
active = true
case let .spaceToLocal(space):
active = space == .root
case .spaceToSpace:
active = false
}

let inverted = switch conversion {
case .rootToSpace, .spaceToLocal:
false
case .spaceToRoot, .localToSpace:
true
case let .spaceToSpace(space1, space2):
!spaceBeforeSpace(space1, space2)
}

forEach(inverted: inverted) { item, stop in
switch item {
case let .coordinateSpace(name), let .sizedSpace(name, _):
let space = name.space
switch conversion {
case let .rootToSpace(target), let .localToSpace(target):
guard space != target else {
stop = true
return
}
case let .spaceToRoot(source), let .spaceToLocal(source):
if space == source {
active = true
}
case let .spaceToSpace(source, target):
guard space != target else {
stop = true
return
}
if space == source {
active = true
}
}
default:
break
}
if active {
body(item)
}
}
}

package func convert<Points>(
_ conversion: ViewTransform.Conversion,
points: inout Points
) where Points: MutableCollection, Points.Element == CGPoint {
guard !isEmpty else { return }
guard !points.isEmpty else { return }
convert(conversion) { item in
points._applyTransform(item: item)
}
}

package func convert(_ conversion: ViewTransform.Conversion, points: inout [CGPoint]) {
guard !isEmpty else { return }
_openSwiftUIUnimplementedFailure()
guard !points.isEmpty else { return }
convert(conversion) { item in
points._applyTransform(item: item)
}
}

package func convert(_ conversion: ViewTransform.Conversion, point: CGPoint) -> CGPoint {
guard !isEmpty else { return point }
_openSwiftUIUnimplementedFailure()
var point = point
convert(conversion) { item in
point.applyTransform(item: item)
}
return point
}

package var containingScrollGeometry: ScrollGeometry? {
Expand Down Expand Up @@ -351,6 +444,37 @@ public struct ViewTransform: Equatable, CustomStringConvertible {
@available(*, unavailable)
extension ViewTransform: Sendable {}

// Mark: MutableCollection + Extension

extension MutableCollection where Element == CGPoint {
fileprivate mutating func _apply(_ matrix: ProjectionTransform, inverse: Bool) {
for index in indices {
let point = self[index]
self[index] = inverse ? point.unapplying(matrix) : point.applying(matrix)
}
}

fileprivate mutating func _applyTransform(item: ViewTransform.Item) {
switch item {
case let .translation(offset):
for index in indices {
self[index].x += offset.width
self[index].y += offset.height
}
case let .affineTransform(matrix, inverse):
let transform = inverse ? matrix.inverted() : matrix

@augmentcode augmentcode Bot Jun 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MutableCollection where Element == CGPoint applies CGAffineTransform.inverted() / CGPoint.applying(_:) directly, while other affine paths in ViewTransform are guarded with #if canImport(CoreGraphics) and fall back to _openSwiftUIPlatformUnimplementedWarning().
On non-CoreGraphics platforms this can become a compile-time break (or silently change behavior compared to the existing guarded implementations).

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

for index in indices {
let point = self[index]
self[index] = point.applying(transform)
}
case let .projectionTransform(matrix, inverse):
_apply(matrix, inverse: inverse)
default:
break
}
}
}

@_spi(ForOpenSwiftUIOnly)
extension ViewTransform {
package struct UnsafeBuffer: Equatable {
Expand Down
Loading
Loading