skip to Main Content

I need to create an infinite horizontal scrolling with ScrollView to scroll a panorama image. So I would like to make this scrolling infinite from both sides, is there any possible way to make it? I have searched maybe using ScrollViewReader can achieve this but no luck.

ScrollView(.horizontal, showsIndicators: false) {
            Image("panorama")
                .resizable()
                .scaledToFill()
        } 

2

Answers


  1. You can put 3 identical panorama images next to each other (to be able to scroll over the edges) and add a custom DragGesture, that basically jumps back to the relative position of the middle image.

    This image is a bad example, obviously it will work better with a real 360° image 😉

    EDIT:
    now with predicted end location + animation and 5 images.

    enter image description here

    struct ContentView: View {
        
        @State private var dragOffset = CGFloat.zero
        @State private var offset = CGFloat.zero
    
        let imageWidth: CGFloat = 500
        
        var body: some View {
            HStack(spacing: 0) {
                ForEach(0..<5) { _ in
                    Image("image")
                        .resizable()
                        .frame(width: imageWidth)
                }
            }
            .offset(x: offset + dragOffset)
            .gesture(
                DragGesture()
                    .onChanged({ value in
                        dragOffset = value.translation.width
                    })
                    .onEnded({ value in
                        withAnimation(.easeOut) {
                            dragOffset = value.predictedEndTranslation.width
                        }
                        offset = (offset + dragOffset).remainder(dividingBy: imageWidth)
                        dragOffset = 0
                    })
            )
        }
    }
    
    Login or Signup to reply.
  2. Actually for such prepared image it is enough to load it only once, and we need only to Image presenters for it (note: they do not copy memory, but use the same loaded image). Everything else is just about layout on the fly moving current of-screen item depending on drag direction to left or to the right of current one.

    Tested with Xcode 13.4 / iOS 15.5

    demo

    Main part of code:

    struct PanoramaView: View {
        let image: UIImage
    
        private struct Item: Identifiable, Equatable {
            let id = UUID()
            var pos: CGFloat!
        }
    
        @State private var items = [Item(), Item()]
    
        // ...
    
        var body: some View {
            GeometryReader { gp in
                let centerY = gp.size.height / 2
                ForEach($items) { $item in
                    Image(uiImage: image)
                        .resizable().aspectRatio(contentMode: .fill)
                        .position(x: item.pos ?? 0, y: centerY)
                        .offset(x: dragOffset)
                }
            }
            .background(GeometryReader {
                Color.clear.preference(key: ViewSizeKey.self,
                                       value: $0.frame(in: .local).size)
            })
            .onPreferenceChange(ViewSizeKey.self) {
                setupLayout($0)
            }
            .contentShape(Rectangle())
            .gesture(scrollGesture)
        }
    

    and usage

        let panorama = UIImage(contentsOfFile: Bundle.main.path(forResource: "panorama", ofType: "jpeg")!)!
        var body: some View {
            PanoramaView(image: panorama)
                .frame(height: 300)   // to demo of dynamic internal layout
        }
    

    Complete test code is here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search