iosimageanimationswiftuioffset

How to infinitely loop and move image in swiftUI in intervals?


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()
            }
         }
    }
}

Solution

  • 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:

    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)
        }
    }
    

    Animation


    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)
        }
    }