最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - How to make a scrollview center on the currently selected index - Stack Overflow

programmeradmin1浏览0评论

scrollview that displays images, I am trying to make it look like the image viewer in the iphone camera. How do I make it so the selected image is always in the center of the screen, meaning if it is the first image it should be in the middle and there should be no images to the right of it, if it the third image it should be in the center with 2 images to the right of it etc..

struct ImagesScrollView: View {
    
    let selectedImages: [UIImage]
    @Binding var selectedImageIndex: Int
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0..<selectedImages.count, id: \.self) { index in
                    Image(uiImage: selectedImages[index])
                        .resizable()
                        .scaledToFill()
                        .frame(width: index == selectedImageIndex ? 32 : 20, height: 30) // Conditional frame size
                        .clipShape(RoundedRectangle(cornerRadius: 3))
                        .onTapGesture {
                            selectedImageIndex = index
                        }
                        .padding(.horizontal, index == selectedImageIndex ? 7 : 0)
                    
                }
            }
            .scrollTargetLayout()
        }
        .padding(.horizontal, 20.0)
        .frame(maxWidth: .infinity)
        .padding(.bottom, 100)
    }
}

scrollview that displays images, I am trying to make it look like the image viewer in the iphone camera. How do I make it so the selected image is always in the center of the screen, meaning if it is the first image it should be in the middle and there should be no images to the right of it, if it the third image it should be in the center with 2 images to the right of it etc..

struct ImagesScrollView: View {
    
    let selectedImages: [UIImage]
    @Binding var selectedImageIndex: Int
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0..<selectedImages.count, id: \.self) { index in
                    Image(uiImage: selectedImages[index])
                        .resizable()
                        .scaledToFill()
                        .frame(width: index == selectedImageIndex ? 32 : 20, height: 30) // Conditional frame size
                        .clipShape(RoundedRectangle(cornerRadius: 3))
                        .onTapGesture {
                            selectedImageIndex = index
                        }
                        .padding(.horizontal, index == selectedImageIndex ? 7 : 0)
                    
                }
            }
            .scrollTargetLayout()
        }
        .padding(.horizontal, 20.0)
        .frame(maxWidth: .infinity)
        .padding(.bottom, 100)
    }
}
Share Improve this question asked Mar 13 at 16:15 john smithjohn smith 1516 bronze badges 1
  • Your code is not reproducible as it seems to include only the child view, leaving out the parent view that probably has the rest of the selectedImageIndex logic. You should update the code to be complete and reproducible for better testing and easier understanding for all. – Andrei G. Commented Mar 14 at 18:05
Add a comment  | 

4 Answers 4

Reset to default 1

You could try this approach using a ScrollViewReader and proxy.scrollTo(selectedImageIndex) to make the selected image always in the center of the screen.

Example code:

Adjust the spacings, numbers etc... to suit your requirements and device sizes.


struct ImagesScrollView: View {
    let selectedImages: [UIImage]
    @Binding var selectedImageIndex: Int
    
    let buffer = 5
    
    var body: some View {
        VStack {
            Image(uiImage: selectedImages[selectedImageIndex])
                .resizable()
                .scaledToFill()
                .frame(width: 333, height: 333)
            
            Spacer()
            
            ScrollViewReader { proxy in
                ScrollView(.horizontal) {
                    HStack {
                        ForEach(0..<selectedImages.count, id: \.self) { index in
                            Image(uiImage: selectedImages[index])
                                .resizable()
                               .scaledToFill()
                               .frame(width: index == selectedImageIndex ? 32 : 20, height: 30)
                               .clipShape(RoundedRectangle(cornerRadius: 3))
                               .onTapGesture {
                                   // --> more logic if needd be
                                   if index > buffer &&
                                        index < selectedImages.count - (buffer + 1)  {
                                       selectedImageIndex = index
                                       withAnimation {
                                           proxy.scrollTo(selectedImageIndex, anchor: .center)
                                       }
                                   }
                                }
                                .border(index == selectedImageIndex ? Color.red : Color.clear)
                                .padding(.horizontal, index == selectedImageIndex ? 7 : 0)
                        }
                    }
                    .scrollTargetLayout()
                }
                .padding(.horizontal, 20.0)
                .padding(.bottom, 100)
            }
        }
    }
}

struct ContentView: View {
    // note the ad hock empty images before and after
    let selectedImages: [UIImage] = [
        UIImage(), UIImage(), UIImage(), UIImage(), UIImage(), UIImage(),
        UIImage(systemName: "photo")!, UIImage(systemName: "globe")!, UIImage(systemName: "info")!, UIImage(systemName: "circle.fill")!, UIImage(systemName: "photo")!,
        UIImage(systemName: "photo")!, UIImage(systemName: "globe")!, UIImage(systemName: "info")!, UIImage(systemName: "circle.fill")!, UIImage(systemName: "photo")!,
        UIImage(systemName: "photo")!, UIImage(systemName: "globe")!, UIImage(systemName: "info")!, UIImage(systemName: "circle.fill")!, UIImage(systemName: "photo")!,
        UIImage(), UIImage(), UIImage(), UIImage(), UIImage(), UIImage()
    ]
    
    @State private var selectedImageIndex = 6
    
    var body: some View {
        ImagesScrollView(selectedImages: selectedImages, selectedImageIndex: $selectedImageIndex)
    }
}

If more precise positioning is required instead of the ad hock empty images, then try using GeometryReader. Note, that using ForEach(0..<selectedImages.count ... is not recommended for your real code.

Another alternative is to use .scrollPosition(id: $scrollPosition, anchor: .center), such as:


struct ImagesScrollView: View {
    let selectedImages: [UIImage]
    @Binding var selectedImageIndex: Int
    
    @State private var scrollPosition: Int?

    var body: some View {
        VStack {
            Image(uiImage: selectedImages[selectedImageIndex])
                .resizable()
                .scaledToFill()
                .frame(width: 333, height: 333)

            Spacer()

            ScrollView(.horizontal) {
                HStack {
                    ForEach(0..<selectedImages.count, id: \.self) { index in
                        Image(uiImage: selectedImages[index])
                            .resizable()
                            .scaledToFill()
                            .frame(width: index == selectedImageIndex ? 32 : 20, height: 30)
                            .clipShape(RoundedRectangle(cornerRadius: 3))
                            .onTapGesture {
                                selectedImageIndex = index
                                scrollPosition = index 
                            }
                            .padding(.horizontal, index == selectedImageIndex ? 7 : 0)
                            .border(index == selectedImageIndex ? Color.red : Color.clear) // <-- for testing
                            .id(index)
                    }
                }
                .scrollTargetLayout()
            }
            .scrollPosition(id: $scrollPosition, anchor: .center)
            .padding(.horizontal, 20.0)
            .padding(.bottom, 100)
            .onAppear {
                scrollPosition = selectedImageIndex
            }
        }
    }
}


To achieve the centering, you need to limit the effective area of the scrollview to the width of the thumbnail image, without clipping it.

You can do so with .safeAreaPadding, which will also basically define it as the identity region, allowing you to easily use .scrollTransition for additional effects.

The amount of safe horizontal padding should be the width of the screen minus the width of your element/thumbnail, divided by 2 (half on each side).

Here's a simplified complete example:

import SwiftUI

struct ImagesScrollView: View {
    
    //Constants
    let indexes = 1...30
    let thumbWidth = 50.0
    
    //State values
    @State private var selectedImageIndex: Int?
    
    //Body
    var body: some View {
        
        //Unwrap optional
        let selectedImageIndex = selectedImageIndex ?? 0
        
        Rectangle()
            .fill(.blue)
            .hueRotation(.degrees(hueAngle(for: selectedImageIndex)))
            .overlay {
                Text("\(selectedImageIndex)")
                    .font(.largeTitle)
                    .foregroundStyle(.white)
            }
        
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 0) {
                ForEach(indexes, id: \.self) { index in
                    RoundedRectangle(cornerRadius: 4)
                        .fill(.blue)
                        .hueRotation(.degrees(hueAngle(for: index)))
                        .onTapGesture {
                            self.selectedImageIndex = index
                        }
                        .overlay(alignment: .center) {
                            Text("\(index)")
                                .font(.caption)
                                .foregroundStyle(.white.opacity(0.8))
                        }
                        .scrollTransition(.interactive) { content, phase in
                            content
                                .scaleEffect(x: phase.isIdentity ? 1 : 0.7, anchor: phase.value > 0 ? .trailing : .leading)
                                .opacity(phase.isIdentity ? 1 : 0.7)
                        }
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .scrollPosition(id: $selectedImageIndex, anchor: .center)
        .frame(maxHeight: 50)
        .padding(.bottom, 50)
        .onAppear {
            self.selectedImageIndex = indexes.upperBound
        }
        .safeAreaPadding(.horizontal, (UIScreen.main.bounds.width - thumbWidth) / 2)
        .animation(.smooth, value: selectedImageIndex)
    }
    
    private func hueAngle(for index: Int) -> Double {
        Double(30 * index)
    }
}

#Preview {
    ImagesScrollView()
}

Try adding a spacer view when you're at the first and last index

i.e. Something like this

var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0..<selectedImages.count, id: \.self) { index in
                    if (index == 0) {
                        View() // Add padding to first spacer to fill the screen according to your needs
                    }
                    if (index == selectedImages.count - 1) {
                        View() // Add padding to last spacer to fill screen
                    }
                    Image(uiImage: selectedImages[index])
                        .resizable()
                        .scaledToFill()
                        .frame(width: index == selectedImageIndex ? 32 : 20, height: 30) // Conditional frame size
                        .clipShape(RoundedRectangle(cornerRadius: 3))
                        .onTapGesture {
                            selectedImageIndex = index
                        }
                        .padding(.horizontal, index == selectedImageIndex ? 7 : 0)
                    
                }
            }
            .scrollTargetLayout()
        }
        .padding(.horizontal, 20.0)
        .frame(maxWidth: .infinity)
        .padding(.bottom, 100)
    }

I did something similar which represent a calendar and I made the selected day always centered.
This should work now

struct ImageScrollView: View {
    @State private var selectedIndex = 0
    let images = ["photo1", "photo2", "photo3"]
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 16) {
                    ForEach(images.indices, id: \.self) { index in
                        Image(images[index])
                            .resizable()
                            .scaledToFit()
                            .frame(width: 300, height: 400) 
                            .id(index)
                            .onTapGesture {
                                withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                                    selectedIndex = index
                                    proxy.scrollTo(index, anchor: .center)
                                }
                            }
                    }
                }
                .padding(.horizontal)
            }
            .onAppear {
                proxy.scrollTo(selectedIndex, anchor: .center)
            }
        }
    }
}
发布评论

评论列表(0)

  1. 暂无评论