How to update opacity of a view based on another view within GeometryReader in SwiftUI?

206 Views Asked by At

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)
    }
}
2

There are 2 best solutions below

2
Benzy Neez On BEST ANSWER

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")

Carousel

1
workingdog support Ukraine On

You could try this approach, using a LazyHStack with a enumerated() in the ForEach and a @State var opa as shown in the example code.

The LazyHStack to display the individual views on the screen, not HStack that computes all views. The enumerated() to have the index of the displayed view, and the @State var opa to record which index is displayed.

Note, your 1.0 - (abs(geometry.frame(in: .global).origin.x)/100.0) does not produce valid opacity values.

Adjust the approach (opacity values) to suit your requirements.

struct ScrollTestView: View {
    var imageNames: [String] {
        Array(1...3).map {"\($0).circle"}
    }
    
    @State var opa = 1.0 // <--- here
    
    var body: some View {
        VStack {
            ScrollView(.horizontal) {
                LazyHStack(spacing: 0) {  // <--- here
                    ForEach(Array(imageNames.enumerated()), id: \.offset) { ndx, imageName in // <--- here
                        GeometryReader { geometry in
                            VStack(spacing: 0) {
                                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))
                                        }
                                    }
                            }
                            .onChange(of: geometry.frame(in: .global).midX) {
                                let d = 1.0 / Double(imageNames.count)
                                opa = d * Double(ndx) + d // <--- for testing
                            }
                        }
                        .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)
                .opacity(opa) // <--- here
            Spacer()
        }
        .background(.orange)
    }
}