I have the following view in SwiftUI that displays a scrolling carousel of images. I'm updating the opacity of an overlay Text element as the image is scrolled and it works fine. However I want to update the opacity of another Text view that lies outside of the ScrollView as well such that the opacities of both Text elements is always the same. How can I achieve this?
struct ScrollTestView: View {
var imageNames: [String] {
Array(1...3).map {"main-ironman\($0)"}
}
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack(spacing: 0) {
ForEach(imageNames, id: \.self) { imageName in
VStack(spacing: 0) {
GeometryReader(content: { geometry in
Image(imageName)
.resizable()
.frame(maxWidth: .infinity)
.frame(height: 200)
.overlay {
VStack {
Spacer()
Text(imageName) // text item
.font(.largeTitle)
.foregroundStyle(.white)
.opacity(1.0 - (abs(geometry.frame(in: .global).origin.x)/100.0))
}
}
})
}
.containerRelativeFrame(.horizontal)
}
}
.scrollTargetLayout()
}
.frame(height: 200)
.scrollIndicators(.hidden)
.scrollTargetBehavior(.viewAligned)
Text("SOME OTHER TEXT") // update opacity same as that of text item above
.font(.largeTitle)
.foregroundStyle(.black)
Spacer()
}
.background(.orange)
}
}
Each of the image overlays has its own opacity setting, but I am guessing you want the opacity of the text to correspond to the opacity of the overlay that is currently in view. This is more tricky, because it means that the text and the overlays cannot simply share the same state variable for controlling opacity.
So what you can do is use a state variable to control the opacity of the text and you update this using an onChange
callback that is monitoring the GeometryProxy.frame
of each image. However, you only want to update the state variable when the image concerned is actually in view, or is coming into view.
Like this:
@State private var textOpacity = 1.0
Image(imageName)
// other modifiers as before
.onChange(of: geometry.frame(in: .global)) { oldFrame, newFrame in
let halfWidth = newFrame.width / 2
if newFrame.minX > -halfWidth && newFrame.minX < halfWidth {
textOpacity = 1 - (abs(newFrame.minX) / 100.0)
}
}
Text("SOME OTHER TEXT")
.opacity(textOpacity)
// + other modifiers as before
As you can see, the onChange
callback is using the global coordinate space and it is expecting the ScrollView
to be left-aligned in this coordinate space. If this were not the case, you could name the coordinate space of the ScrollView
and use this coordinate space instead. Like this:
ScrollView(.horizontal) {
// other content as before
.onChange(of: geometry.frame(in: .named("ScrollView"))) { oldFrame, newFrame in
// content as above
}
}
.frame(height: 200)
.coordinateSpace(name: "ScrollView")