So I have an image whose width is greater than screen width. And I wanna move it across the screen in horizontal direction. Currently, I am able to animate it seamlessly but it is a continuous animation, and I want it to be in intervals. So right now, whole image is being animated in 10 seconds without stopping. What I want is that image to animate but with small breaks and gaps. (May be image animate to left with 100 pts, then stop, then again continue moving towards left). I have tried playing with offset but it isn't working. Here is the current video Below is my code.
struct AnimatingHorizontalImageView: View {
@State var animate: Bool = false
let animation: Animation = Animation.linear(duration: 10.0).repeatForever(autoreverses: false)
let width = UIScreen.main.bounds.width
let height = UIScreen.main.bounds.height
var body: some View {
HStack(spacing: -1) {
Image(.banner)
.resizable()
.scaledToFill()
.frame(height: 210)
Image(.banner)
.resizable()
.scaledToFill()
.frame(width: width,height: 210, alignment: .leading)
}
.frame(width: width, height: 210,
alignment: animate ? .trailing : .leading)
.onTapGesture {
print(width)
}
.ignoresSafeArea()
.onAppear {
withAnimation(animation) {
animate.toggle()
}
}
}
}
You could try using a PhaseAnimator
for this.
The example below illustrates how it can be used for an image that can have any aspect ratio:
Scaled-to-fill is used for images with an aspect ratio wider than the screen, scaled-to-fit is used for taller images. The choice is made by supplying both variations to ViewThatFits
.
The screen width is measured using a GeometryReader
. This is a better approach than using UIScreen.main
, which is not only deprecated but also doesn't work well with iPad split screen.
The image size is measured by using a hidden version of the image as placeholder, then applying an overlay over the top. The overlay contains another GeometryReader
. Of course, if you already know the image dimensions then this construction could be avoided.
The number of steps is calculated by dividing the image width by 100 and truncating the result. The number of phases is 1 more than the number of steps.
The pause between steps is implemented by adding a delay to the animation.
The animation for step 0 is suppressed, so that the image moves back to its starting position instantaneously.
struct AnimatingHorizontalImageView: View {
private var theImage: some View {
ViewThatFits(in: .vertical) {
let theImage = Image(.image2)
theImage
.resizable()
.scaledToFill()
theImage
.resizable()
.scaledToFit()
}
}
var body: some View {
GeometryReader { screen in
let screenWidth = screen.size.width
theImage
.hidden()
.overlay {
GeometryReader { proxy in
let imageWidth = proxy.size.width
let nSteps = max(1, floor(imageWidth / 100))
let stepSize = imageWidth / nSteps
let nImages = Int(ceil(screenWidth / imageWidth)) + 1
PhaseAnimator(0...Int(nSteps)) { phase in
HStack(spacing: 0) {
ForEach(0..<nImages, id: \.self) { _ in
theImage
}
}
.offset(x: -(CGFloat(phase) * stepSize))
.frame(width: screenWidth, alignment: .leading)
.overlay {
Text("\(phase)")
.font(.largeTitle)
.foregroundStyle(.white)
}
} animation: { phase in
.linear(duration: phase == 0 ? 0 : 1)
.delay(phase == 0 ? 0 : 1)
}
}
}
}
.frame(height: 250)
}
}
EDIT As you pointed out in your comment, PhaseAnimator
requires iOS 17. For earlier iOS versions, you could use an Animatable
ViewModifier
to apply the offset instead. This can decide whether to pause or whether to increase the offset, according to the degree of completion.
This version is not so generic as the other version above, because it is expected that the aspect ratio of the image is wider than the screen (scaled-to-fill is always used).
struct HorizontalAnimator: ViewModifier, Animatable {
let nSteps: Int
let stepSize: CGFloat
var progress: CGFloat
var animatableData: CGFloat {
get { progress }
set { progress = newValue }
}
private var xOffset: CGFloat {
let stepProgress = progress * CGFloat(nSteps)
let step = min(nSteps, Int(stepProgress))
let stepFraction = stepProgress - CGFloat(step)
let isPausing = stepFraction < 0.5
let stepPosition = CGFloat(step) + (isPausing ? 0 : ((stepFraction - 0.5) * 2))
return stepPosition * -stepSize
}
func body(content: Content) -> some View {
content
.offset(x: xOffset)
}
}
struct AnimatingHorizontalImageView: View {
@State private var progress = CGFloat.zero
var body: some View {
GeometryReader { screen in
let screenWidth = screen.size.width
let theImage = Image(.image2)
theImage
.resizable()
.scaledToFill()
.hidden()
.overlay {
GeometryReader { proxy in
let imageWidth = proxy.size.width
let nSteps = max(1, floor(imageWidth / 100))
let stepSize = imageWidth / nSteps
HStack(spacing: 0) {
theImage
.resizable()
.scaledToFill()
theImage
.resizable()
.scaledToFill()
}
.frame(width: screenWidth, alignment: .leading)
.modifier(
HorizontalAnimator(
nSteps: Int(nSteps),
stepSize: stepSize,
progress: progress
)
)
.onAppear {
withAnimation(
.linear(duration: nSteps * 2)
.repeatForever(autoreverses: false)
) {
progress = 1.0
}
}
}
}
}
.frame(height: 250)
}
}