I created a view similar to a wheel picker, and when I swiped the scrollView, I wanted the color of the text to change to white when it coincided with the rectangle.I can use .blendMode
to achieve the effects in the images, but when the background changes, the effect also changes. I want to know if there is any method that can be unaffected by the background color.I searched for a long time but couldn't find a suitable solution. If you could provide any help, I would greatly appreciate it.
import SwiftUI
import SwiftUIIntrospect
import Combine
@_spi(Advanced) import SwiftUIIntrospect
struct TimePickerView: View {
@StateObject var viewModel = TimerPickerViewModel()
private let date = ["", "1", "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "60", "70", "80", "90", "100", "110", "120", ""]
var body: some View {
ZStack {
Image("bg")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.all)
ScrollView(showsIndicators: false) {
LazyVStack(spacing: 0) {
ForEach(0..<date.count, id: \.self) { index in
let time = date[index]
Text(time + (time.isEmpty ? "":" MIN"))
.frame(width: 200, height: 50)
}
}
.font(.system(size: 25))
.foregroundStyle(Color(hex: 0x638FFF))
.background(GeometryReader { geometry in
Color.clear
.preference(key: TimerOffsetPreferenceKey.self,
value: geometry.frame(in: .named("TimePickeView")).origin.y)
})
.onPreferenceChange(TimerOffsetPreferenceKey.self) { value in
viewModel.contentOffset = -value
}
}
.introspect(.scrollView, on: .iOS(.v15, .v16, .v17), customize: { scrollView in
viewModel.scrollView = scrollView
})
.frame(height: 150)
.coordinateSpace(name: "TimePickeView")
RoundedRectangle(cornerRadius: 25)
.fill(Color(hex: 0x638FFF))
.frame(width: 180, height: 50)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
class TimerPickerViewModel: ObservableObject {
@Published var contentOffset: CGFloat = 0
private var cancellable = Set<AnyCancellable>()
@Weak var scrollView: UIScrollView?
init() {
$contentOffset
.dropFirst(2)
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.sink { [weak self] output in
let remainder = Int(output) % 50
var offset = Int(output) - remainder + (remainder > 25 ? 50:0)
offset = max(0, min(offset, 1000))
self?.scrollView?.setContentOffset(CGPoint(x: 0, y: CGFloat(offset)), animated: true)
}
.store(in: &cancellable)
}
}
struct TimerOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
#Preview {
TimePickerView()
}
The following is a similar effect that I want:
You might be able to get it to work using .blendMode
with the following small changes:
.compositingGroup()
to the ZStack
.background
ZStack {
// content as before, but without the background image
}
.compositingGroup()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background( /* the image */ )
The modifier .compositingGroup
isolates the effect of the blend mode to the contents of the ZStack
. However, whether or not it works may depend on the colors you are using and/or the color scheme in operation (light/dark mode).
As a more general-purpose solution, I would suggest showing a Capsule
shape which has the list of dates shown as an overlay, clipped to the shape. This way, you can use any styling you like.
GeometryReader
in the background. You were doing this already, but you were then using a PreferenceKey
to pass on the value. I would suggest, it is simpler to use an .onChange
handler to update a state variable..scrollTargetLayout
..scrollPosition
. The selected index can then be derived from the scrolled index.Here is an example of how it can be implemented this way. It does not need a PreferenceKey
, nor does it use introspection.
struct TimePicker: View {
private let rowHeight: CGFloat = 50
private let date = ["", "1", "5", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55", "60", "70", "80", "90", "100", "110", "120", ""]
@State private var scrollOffset = CGFloat.zero
@State private var scrolledIndex: Int?
var selectedIndex: Int {
min((scrolledIndex ?? 0) + 1, date.count - 2)
}
private var dateList: some View {
VStack(spacing: 0) {
ForEach(Array(date.enumerated()), id: \.offset) { index, time in
Text(time + (time.isEmpty ? "":" MIN"))
.frame(height: rowHeight)
}
}
.font(.system(size: 25))
}
private var scrollDetector: some View {
GeometryReader { proxy in
let minY = proxy.frame(in: .scrollView).minY
Color.clear
.onChange(of: minY) { oldVal, newVal in
scrollOffset = newVal
}
}
}
var body: some View {
ZStack {
ScrollView(showsIndicators: false) {
dateList
.foregroundStyle(.blue) // 0x638FFF
.background(scrollDetector)
}
.scrollPosition(id: $scrolledIndex)
.scrollTargetLayout()
.scrollTargetBehavior(.viewAligned)
Capsule()
.fill(.blue) // 0x638FFF
.frame(width: 180, height: rowHeight)
.overlay(alignment: .top) {
dateList
.foregroundStyle(.background)
.offset(y: scrollOffset - rowHeight)
}
.clipped()
.allowsHitTesting(false)
}
.frame(width: 200, height: 3 * rowHeight)
.padding(.bottom, 50)
.overlay(alignment: .bottom) {
Text("Selected index = \(selectedIndex)")
}
}
}