Created
January 29, 2023 20:46
-
-
Save gromwel/404377a22c72f61aa73d961d23ec9254 to your computer and use it in GitHub Desktop.
Прилипающие заголовки (есть проблема с перфомансом)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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