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.
Parece que você quer que a visualização de rolagem se ajuste a uma visualização quando terminar de rolar, e então você quer ser capaz de detectar qual das visualizações está sob a sobreposição.
Existem vários modificadores que fornecem essa funcionalidade gratuitamente:
.scrollPosition
para rastrear a posição atual de rolagem. Isso funciona em conjunto com.scrollTargetLayout
, que precisa ser aplicado aoVStack
..scrollTargetBehavior(.viewAligned)
para ajustar uma visualização.Para que a visualização de rolagem seja posicionada com a primeira das visualizações posicionadas no centro quando ela aparecer pela primeira vez, adicione
.contentMargins
preenchimento ao conteúdo rolado.A propósito, seu exemplo está usando muitos modificadores obsoletos. Você pode considerar as seguintes substituições:
.foregroundColor
porforegroundStyle
..cornerRadius
por uma forma de clipe ou preencha o fundoin
com uma forma..overlay
modificadores que você estava usando também foram descontinuados.Aqui está o exemplo atualizado para mostrar tudo funcionando: