Hi! I'm trying to create a view where the user should be able to select images from a recorded video as in the attached example image.
There should be a scrollable HStack, showing every 20th frame from the recorded video, with a vertical line (horizontally positioned at the middle of the screen) marking the current frame. The current frame should be shown as an enlarged image above the thumbnail images. The user should be able to swipe the enlarged image to go between frames, i.e. scroll one frame position back or forth (and also be able to change frame by dragging/scrolling the HStack, but that will be a later problem).
I have managed to generate frames from a recorded video and set up a TabView to present the current frame (enlarged image) with swiping functionality, and a ScrollView below to present the thumbnails. But I can't figure out how to get the scrollTo-funcitonality to work. Right know the scrolling positions are off (my invisible scroll to-elements do not line up correctly with the thumbnail images. I have attached my messy code of the scrollable HStack if that is of any help.
Thankful for any tips!
`struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct ViewMinXKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct ViewMaxXKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct StackedPhotosView: View {
@EnvironmentObject var videoManager: VideoManager
@State var minX: CGFloat = 0.0
@State var maxX: CGFloat = 0.0
var body: some View {
GeometryReader { geometry in
ScrollViewReader { reader in
ScrollView(.horizontal) {
HStack(spacing: 0) {
// Rectangle to create an offset so that the thumbnail images start at the middle of the screen
Rectangle().frame(width:geometry.size.width/2, height: 70).foregroundColor(Color.clear)
ZStack {
HStack(spacing: 0) {
// Show every 20th image
ForEach(Array(videoManager.frames.enumerated()), id: .1.id) { (index, item) in
if index % 20 == 0 {
Image(uiImage: item.image)
.resizable()
.border(.black, width: 1)
.frame(height: 70)
.aspectRatio(1.0, contentMode: .fit)
.rotationEffect(.degrees(90))
}
}
}
if maxX > 0 {
// Create invisible "scroll to" elements with ids for every frame
HStack(spacing:0) {
ForEach(videoManager.frames.indices, id: .self) { i in
Spacer()
.id(i)
.frame(width: Double((maxX-(geometry.size.width/2))/CGFloat(videoManager.frames.count)), height: 70)
.foregroundColor(.red)
.border(.black, width: 1)
.opacity(0.5)
}
}
}
}
// Rectangle extending the HStack and making all images being able to pass the vertical blue line
Rectangle().frame(width:geometry.size.width/2, height: 70).foregroundColor(Color.clear)
}
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: -$0.frame(in: .named("scroll")).origin.x) // Will be used later to change top image when drag-scrolling on the HStack
Color.clear.preference(key: ViewMinXKey.self,
value: $0.frame(in: .local).minX)
Color.clear.preference(key: ViewMaxXKey.self,
value: $0.frame(in: .local).maxX - geometry.size.width)
})
}
.onChange(of: videoManager.currentFrameNr, perform: { index in
withAnimation(.linear) {
reader.scrollTo(index, anchor: .leading)
}
})
.onPreferenceChange(ViewMinXKey.self) {
minX = CGFloat($0)
}
.onPreferenceChange(ViewMaxXKey.self) {
maxX = CGFloat($0)
}
}
}
} }`