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
|
4 Answers
Reset to default 1You 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)
}
}
}
}
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