Search code examples
swiftswiftuigeometryreader

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


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

Solution

  • 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