Skip to content

Instantly share code, notes, and snippets.

@gromwel
Created January 29, 2023 20:46
Show Gist options
  • Save gromwel/404377a22c72f61aa73d961d23ec9254 to your computer and use it in GitHub Desktop.
Save gromwel/404377a22c72f61aa73d961d23ec9254 to your computer and use it in GitHub Desktop.
Прилипающие заголовки (есть проблема с перфомансом)
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
contents
}
.navigationTitle("title")
.useStickyHeaders()
}
}
@ViewBuilder
var contents: some View {
Text("top")
.font(.title)
.padding(4)
.pickerStyle(.segmented)
.frame(maxWidth: .infinity)
.background(.yellow)
.border(.gray)
.sticky()
HStack {
Group {
Image(systemName: "globe")
.resizable()
.foregroundColor(.red)
Image(systemName: "person.crop.circle")
.resizable()
.foregroundColor(.teal)
Image(systemName: "escape")
.resizable()
.foregroundColor(.purple)
}
.frame(width: 100, height: 100)
}
.padding()
Text("bottom")
.font(.title)
.padding(4)
.pickerStyle(.segmented)
.frame(maxWidth: .infinity)
.background(.mint)
.border(.gray)
.sticky()
ForEach(0..<1000) {
Text(String($0))
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// MARK: - модификатор вьюшки которая должна быть приклеена
extension View {
// модицикатор для той вью которая должна прилипать
func sticky() -> some View {
modifier(StickyModifier())
}
}
struct StickyModifier: ViewModifier {
// считываем из окружения данные о положении и размере всех липких вьюх
@Environment(\.stickyRects) var stickyRects
// данные о размере триггирящие перерисовку
@State private var frame: CGRect = .zero
// идентификатор для вьюхи
// просто создать id: UUID не подойдет так как он будет меняться при каждой перерисовке
// по документации namespace должен быть привязан к вьюхе
@Namespace private var id
// прилипшая ли вьюха
private var isSticking: Bool {
frame.minY < 0
}
// рассчет отступа
private var offset: CGFloat {
guard isSticking else { return 0 }
guard let stickyRects else { return 0 }
// отступ равный компенсации прокрутки вьюхи за пределы
// что бы вьюха оставась "прилипшей"
var offset = -frame.minY
// поиск другой вьюхи которая может вытеснять текущую
if let other = stickyRects.first(
where: { (key, value) in
// ищем вьюху которая не текущая по id
// которая расположена ниже относительно текущей
// которая уже наехала на текущую
key != id && value.minY > frame.minY && value.minY < frame.height
}
) {
// компенцая отступа
// уменьшаем отступ на сколько вытесяняющая вьюха наезжает на вытесняемую
offset -= frame.height - other.value.minY
}
return offset
}
func body(content: Content) -> some View {
content
// правка положения на рассчитанный отступ
.offset(y: offset)
// выдвигаем на передний план если прилипли
.zIndex(isSticking ? .infinity : 0)
.overlay {
//
GeometryReader { proxy in
// положение и размер относительно контейнера к которому будет прилипать
let f = proxy.frame(in: .named("container"))
// заполнитель для передачи размера и положения дальше по иерархии
// и перерисовки
Color.clear
.onAppear { frame = f }
.onChange(of: f, perform: { frame = $0 })
.preference(key: FramePreference.self, value: [id: frame])
}
}
}
}
// MARK: -
extension View {
// модификатор для вьюхи к верхней границе которой будет прилипать
func useStickyHeaders() -> some View {
modifier(UseStickyHeaders())
}
}
struct UseStickyHeaders: ViewModifier {
// данные о положении и размере всех прилипающих вьюх
@State private var frames: StickyRects.Value = [:]
func body(content: Content) -> some View {
content
// при появлении новой прилипающей вьюхи будет перерисовка и передача вниз новых данных
.onPreferenceChange(FramePreference.self) {
frames = $0
}
// передаем вниз по иерархии фреймы всех прилипающих вьюх
.environment(\.stickyRects, frames)
// определяем пространство относительно которого будет высчитывать положение прилипающих
.coordinateSpace(name: "container")
}
}
// MARK: - Вспомогательные
// ключ для передачи данных вверх по иерархии
// сообщаем о новых прилипающий вьюхах
struct FramePreference: PreferenceKey {
static var defaultValue: [Namespace.ID: CGRect] = [:]
static func reduce(value: inout Value, nextValue: () -> Value) {
value.merge(nextValue()) { $1 }
}
}
// ключ для доступа к свойству в окружении
// передаем данные о прилипших вьюхах
enum StickyRects: EnvironmentKey {
static var defaultValue: [Namespace.ID: CGRect]? = nil
}
// свойство в окружении
extension EnvironmentValues {
var stickyRects: StickyRects.Value {
get { self[StickyRects.self] }
set { self[StickyRects.self] = newValue }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment