我创建了一个类似于滚轮选择器的视图,当我滑动scrollView时,我希望文本的颜色与矩形重合时变为白色。我可以使用
.blendMode
来实现图像中的效果,但当背景改变时,效果也会改变。我想知道有没有一种方法可以不受背景颜色的影响。我找了很久没有找到合适的解决方案。如果您能提供任何帮助,我将不胜感激。
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()
}
以下是我想要的类似效果:
我建议显示一个
Capsule
形状,其中日期列表显示为叠加层,并剪裁到形状上。这样,您就可以使用任何您喜欢的样式。
GeometryReader
检测滚动偏移。您已经这样做了,但是您随后使用 PreferenceKey
来传递值。我建议,使用 .onChange
处理程序来更新状态变量更简单。.scrollTargetLayout
将滚动位置设置为粘性。.scrollPosition
检测滚动项的索引。然后可以从滚动索引中导出所选索引。这是一个如何以这种方式实现的示例。它不需要
PreferenceKey
,也不需要内省。
struct TimePicker: View {
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 baseOffset = CGFloat.zero
@State private var scrollOffset = CGFloat.zero
@State 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(width: 200, height: 50)
}
}
.font(.system(size: 25))
}
private var scrollDetector: some View {
GeometryReader { proxy in
let minY = proxy.frame(in: .scrollView).minY
Color.clear
.onChange(of: minY, initial: true) { oldVal, newVal in
scrollOffset = minY
}
}
}
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: 50)
.overlay(alignment: .top) {
dateList
.offset(y: -baseOffset + scrollOffset)
.foregroundStyle(.white)
}
.clipped()
.allowsHitTesting(false)
.background {
GeometryReader { proxy in
let minY = proxy.frame(in: .named("ZStack")).minY
Color.clear
.onAppear { baseOffset = minY }
}
}
}
.coordinateSpace(.named("ZStack"))
.frame(height: 150)
.padding(.bottom, 50)
.overlay(alignment: .bottom) {
Text("Selected index = \(selectedIndex)")
}
}
}