Minha tarefa é criar uma scrollview, que recebe um array de strings e uma sobreposição, localizada no centro. Quando alguma visualização entra nessa sobreposição, ela deve se alinhar imediatamente às suas bordas, ou seja, estar no centro da tela. A lista deve começar no meio da tela com o primeiro elemento. Eu estava procurando uma solução aqui .
Eu tenho estrutura
struct Region: Equatable, Hashable {
let name: String
}
Estrutura de Chave de Preferência
struct ViewOffsetKey: PreferenceKey {
static var defaultValue: [String: CGRect] = [:]
static func reduce(value: inout [String: CGRect], nextValue: () -> [String: CGRect]) {
value.merge(nextValue(), uniquingKeysWith: { $1 })
}
}
e subView
struct RegionView: View {
let name: String
var body: some View {
HStack {
Text(self.name)
.foregroundColor(.white)
.font(.system(.body))
Spacer()
}
.padding(16)
.frame(width: 348, alignment: .topLeading)
.background(Color.black)
.cornerRadius(50)
.id(name)
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: [name: $0.frame(in: .global)])
})
}
}
A visualização principal é realizada por meio de CurrentValueSubject e AnyPublisher. Verifico se a subView intercepta um retângulo com altura 1 e, em seguida, a escrevo como exibida. Quando a rolagem termina, chamo o método scrollTo.
struct RegionListView: View {
let detector: CurrentValueSubject<[String: CGRect], Never>
let publisher: AnyPublisher<[String: CGRect], Never>
let regions: [Region] = [
Region(name: "Region 1"),
Region(name: "Region 2"),
Region(name: "Region 3"),
Region(name: "Region 4"),
Region(name: "Region 5"),
Region(name: "Region 6"),
Region(name: "Region 7"),
Region(name: "Region 8"),
Region(name: "Region 9"),
Region(name: "Region 10"),
Region(name: "Region 11"),
Region(name: "Region 12"),
Region(name: "Region 13"),
Region(name: "Region 14"),
Region(name: "Region 15"),
Region(name: "Region 16"),
Region(name: "Region 17"),
Region(name: "Region 18"),
Region(name: "Region 19"),
Region(name: "Region 20"),
]
@State private var topVisibleChildId: String?
init() {
let detector = CurrentValueSubject<[String: CGRect], Never>([:])
self.publisher = detector
.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
.dropFirst()
.eraseToAnyPublisher()
self.detector = detector
}
var body: some View {
GeometryReader { geometryReader in
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 8) {
ForEach(self.regions, id: \.self) { region in
RegionView(name: region.name)
}
}
.frame(maxWidth: .infinity)
.onPreferenceChange(ViewOffsetKey.self) { childFrames in
detector.send(childFrames)
var visibleChildIds = [String]()
let screenMid = geometryReader.size.height / 2 + 56
for (id, childFrame) in childFrames where childFrame.intersects(CGRect(x: 0, y: Int(screenMid), width: .max, height: 56)) {
print("id \(id) childFrame \(childFrame)")
visibleChildIds.append(id)
}
visibleChildIds.sort()
if let first = visibleChildIds.first {
topVisibleChildId = first
}
}
}
.safeAreaPadding(.top, geometryReader.size.height / 2 - 28)
.safeAreaPadding(.bottom, geometryReader.size.height / 2 - 28)
.background(Color.black)
.onReceive(publisher) { _ in
proxy.scrollTo(topVisibleChildId, anchor: .center)
}
.overlay(
Text("Top Visible Child: \(topVisibleChildId ?? "")")
.padding()
.background(Color.blue.opacity(1))
.foregroundColor(.white)
.cornerRadius(10),
alignment: .top
)
.overlay(
Rectangle()
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(Color.clear)
.border(.green, width: 4),
alignment: .center
)
}
}
}
}
Minha pergunta: é possível implementar um comportamento em que a mudança de células seja semelhante à escolha de um horário ao definir um alarme? Ou seja, a célula não precisará ser rolada para o centro se o usuário não terminar de rolar no meio. Espero ter explicado claramente.