I've been trying for days but I couldn't find a solution. My goal is to move the Image from one location to another. But I don't want the image to only move up and down while moving it. I want it to look like the image is sliding while the page is sliding. I'm sorry if I didn't explain it clearly. When you run the code on your computer, you will understand the animation I am trying to make. I ask for your help
struct SwiftUIView: View {
@State private var showFirstView = true
@Namespace var namespace
var body: some View {
GeometryReader { geometry in
ZStack {
if showFirstView {
FirstView()
.transition(.move(edge: .trailing))
.frame(width: geometry.size.width, height: geometry.size.height)
} else {
SecondView()
.transition(.move(edge: .leading))
.frame(width: geometry.size.width, height: geometry.size.height)
}
}
.animation(.easeInOut(duration: 0.5), value: showFirstView)
}
.overlay(alignment: .bottom) {
Button(action: {
withAnimation {
showFirstView.toggle()
}
}) {
Text("Switch View")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding()
}
}
func FirstView() -> some View {
ZStack{
Color.red
.ignoresSafeArea()
cusview()
}
}
func SecondView() -> some View {
ZStack {
Color.green
.ignoresSafeArea()
cusview()
.offset(y: -150)
}
}
func cusview() -> some View {
Image(.image10)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 140, height: 100)
.matchedGeometryEffect(id: "one", in: namespace)
}
}
#Preview {
SwiftUIView()
}
When I tried your code in a simulator (iPhone 15 running iOS 17.5), the animation of the image was disconnected:
You said in your post:
I don't want the image to only move up and down
so I was asking in a comment, how exactly you do want it to move? I would suggest, there would be two conceivable ways to animate the image, if the movement should not be vertical-only:
The image in the appearing view could stay as you have it (diagonal movement), but the image in the disappearing view could also move diagonally, instead of only vertically.
This would mean, when one image moves off to the right, the new image comes in from the left at the same height. This would still look disconnected, but at least the image would have a consistent direction of movement.
I tried to find a way to implement it this way, but wasn't successful, sorry.
The animation would be less disconnected if the image would move smoothly from one position to the other. If you don't want the animation to be vertical-only, then the image would need to move in some kind of curve instead.
This second style of animation can be achieved by showing the image as another layer in the ZStack
and have it move between placeholder positions in the two child views.
The placeholders use matchedGeometryPostion
with isSource: true
(which is in fact the default), so these determine the positions being moved between.
The image itself uses matchedGeometryPosition
with isSource: false
, so that it is matched to the source positions.
By default, matchedGeometryEffect
will set the size of the image from the active placeholder, so there is no need to set a separate frame
on the image. However, it's best if the image is shown in an overlay over the ZStack
, instead of as a regular layer. This ensures that the image at its natural size does not cause the size of the ZStack
to bloat, for the case of when the image size is larger than the screen size.
A curved path can be achieved by using different animation durations for the transitions and the matchedGeometryEffect
:
matchedGeometryEffect
is faster than the transition, the image follows a curved path on the side of the disappearing viewmatchedGeometryEffect
is slower than the transition, the image follows a curved path on the side of the appearing viewThe updated example below shows it working this way. Other changes and suggestions:
The two child views already occupy all the space available, because they both contain a Color
, which is greedy. This means, there is no need to set a frame
on these views, so the GeometryReader
is not needed.
A GeometryReader
would not be needed, even if the child views did not contain a Color
. If you want to enlarge a view to maximum size, set a frame
with maxWidth: .infinity, maxHeight: .infinity
.
If you have an .animation
modifier, the flag does not need to be toggled withAnimation
. In fact, using separate .animation
modifiers instead of withAnimation
is the key to being able to control the way the image moves.
The modifier .scaledToFill
is a simpler way of applying .aspectRatio(contentMode: .fill)
.
The modifier .foregroundColor
is deprecated, use .foregroundStyle
instead.
The modifier .cornerRadius
is deprecated too, use .clipShape
instead, or just show a RoundedRectangle
in the background.
The button style .borderedProminent
is actually an easier way to achieve the same button styling. To make it look exactly like you had it before, you just need to add a little padding to the label.
Both the action and the label for the button can be supplied as trailing closures, which avoids a set of parentheses. Doing it this way means labelling the one for the label, instead of the one for the action.
The button is being shown in an overlay as a way of positioning it at the bottom of the ZStack
. This is fine, but you could also consider making it another layer of the ZStack
and positioning by setting maxHeight: .infinity
with alignment: .bottom
. The result is the same, so it's just down to personal preference.
struct SwiftUIView: View {
@State private var showFirstView = true
@Namespace var namespace
var body: some View {
ZStack {
if showFirstView {
FirstView()
.transition(.move(edge: .trailing))
} else {
SecondView()
.transition(.move(edge: .leading))
}
}
.animation(.easeInOut(duration: 0.8), value: showFirstView)
.overlay {
cusview()
// Try adjusting the duration to get different movements
.animation(.easeInOut(duration: 0.7), value: showFirstView)
}
.overlay(alignment: .bottom) {
Button {
showFirstView.toggle()
} label: {
Text("Switch View")
.padding(.horizontal, 4)
.padding(.vertical, 9)
}
.buttonStyle(.borderedProminent)
.padding()
}
}
func FirstView() -> some View {
ZStack{
Color.red
.ignoresSafeArea()
imagePlaceholder
}
}
func SecondView() -> some View {
ZStack {
Color.green
.ignoresSafeArea()
imagePlaceholder
.offset(y: -150)
}
}
private var imagePlaceholder: some View {
Color.clear
.frame(width: 140, height: 100)
.matchedGeometryEffect(id: "one", in: namespace, isSource: true)
}
func cusview() -> some View {
Image(.image10)
.resizable()
.scaledToFill()
.matchedGeometryEffect(id: "one", in: namespace, isSource: false)
}
}