The masking approach could work for scroll views too, if you track the frames of each subview of the scroll view. You could use a struct like this:
struct ScrollContentFrame: Identifiable {
let id: AnyHashable
let frame: Anchor
let shape: AnyShape
}
I’m keeping things very general here by using AnyHashable
and AnyShape
. If this is just a one-off effect you want to do, I would recommend using an appropriate ID type, and if the shape is known and constant, remove shape
.
Next, write a preference key that collects an array of these,
struct ScrollContentFramesKey: PreferenceKey {
static var defaultValue: [ScrollContentFrame] { [] }
static func reduce(value: inout [ScrollContentFrame], nextValue: () -> [ScrollContentFrame]) {
value.append(contentsOf: nextValue())
}
}
Set the preference key using an anchorPreference
,
.anchorPreference(key: ScrollContentFramesKey.self, value: .bounds) { anchor in
[
ScrollContentFrame(
id: i,
frame: anchor,
shape: AnyShape(RoundedRectangle(cornerRadius: 16))
)
]
}
The color-blended linear gradient can be added as an overlayPreferenceValue
instead of ZStack
.
.overlayPreferenceValue(ScrollContentFramesKey.self) { scrollContentFrames in
LinearGradient(
colors: [.blue, .orange],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.allowsHitTesting(false)
.mask {
GeometryReader { proxy in
ForEach(scrollContentFrames) { content in
let frame = proxy[content.frame]
content.shape
.frame(width: frame.width, height: frame.height)
.position(x: frame.midX, y: frame.midY)
}
}
}
.blendMode(.color)
.ignoresSafeArea()
}
Full code for the view:
struct ContentView: View {
var body: some View {
ZStack {
LinearGradient(
colors: [.purple, .pink],
startPoint: .top,
endPoint: .bottom
)
.ignoresSafeArea()
ScrollView {
LazyVStack(spacing: 20) {
ForEach(0..<10) { i in
RoundedRectangle(cornerRadius: 16)
.fill(.white)
.frame(height: 120)
.overlay(
Text("Card \(i)")
.font(.headline)
.foregroundColor(.red)
)
.anchorPreference(key: ScrollContentFramesKey.self, value: .bounds) { anchor in
[
ScrollContentFrame(
id: i,
frame: anchor,
shape: AnyShape(RoundedRectangle(cornerRadius: 16))
)
]
}
.padding(.horizontal)
}
}
.padding(.vertical, 50)
}
.background(Color.clear)
.overlayPreferenceValue(ScrollContentFramesKey.self) { scrollContentFrames in
LinearGradient(
colors: [.blue, .orange],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.allowsHitTesting(false)
.mask {
GeometryReader { proxy in
ForEach(scrollContentFrames) { content in
let frame = proxy[content.frame]
content.shape
.frame(width: frame.width, height: frame.height)
.position(x: frame.midX, y: frame.midY)
}
}
}
.blendMode(.color)
.ignoresSafeArea()
}
}
}
}