Skip to content

Instantly share code, notes, and snippets.

@shaps80
Last active April 23, 2024 20:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shaps80/88fb7414a8f4c27847af732251eafef2 to your computer and use it in GitHub Desktop.
Save shaps80/88fb7414a8f4c27847af732251eafef2 to your computer and use it in GitHub Desktop.
pinned modified now allows any hashable value
import SwiftUI
public extension View {
func pinned<T: Hashable>(id: Binding<T?>) -> some View {
modifier(Pinned(pinnedId: .init(
get: { id.wrappedValue },
set: { id.wrappedValue = $0 as? T }
)))
}
}
public extension View {
func pinnedTargetLayout() -> some View {
modifier(PinnedTargetLayout())
}
}
private struct Pinned: ViewModifier {
struct Pin: Equatable, Comparable {
var id: AnyHashable
var rect: CGRect
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.rect.minY < rhs.rect.minY
}
}
@Environment(\.pinnedCoordinateSpace) private var coordinateSpace
@Environment(\.pinnedTargets) private var targets
@State private var frame: CGRect = .zero
@State private var pinned: [Pin] = []
@Binding var pinnedId: AnyHashable?
private var isPinned: Bool { frame.minY < 0 }
var offset: CGFloat {
guard isPinned else { return 0 }
var offset = -frame.minY
let rects = targets.map { $0.rect }
if let idx = rects.firstIndex(where: {
$0.minY > frame.minY && $0.minY < frame.height
}) {
let other = rects[idx]
offset -= frame.height - other.minY
}
return offset
}
func body(content: Content) -> some View {
content.variadic { views in
SwiftUI.ForEach(views) { content in
content
.offset(y: offset)
.zIndex(isPinned ? .infinity : 0)
.overlay {
GeometryReader { proxy in
let f = proxy.frame(in: coordinateSpace)
Color.clear
.onAppear { frame = f }
.onChange(of: f) { newValue in
frame = newValue
}
.preference(
key: PinnedPreference.self,
value: [.init(id: content.id, rect: frame)]
)
}
}
.onChange(of: targets) { newValue in
pinnedId = newValue.last { $0.rect.minY < 0 }?.id
}
}
}
}
}
private struct PinnedTargetLayout: ViewModifier, Identifiable {
@State private var pinnedTargets: [Pinned.Pin] = []
let id = UUID()
func body(content: Content) -> some View {
content
.coordinateSpace(name: id.uuidString)
.environment(\.pinnedCoordinateSpace, .named(id.uuidString))
.environment(\.pinnedTargets, pinnedTargets)
.onPreferenceChange(PinnedPreference.self) {
pinnedTargets = $0.sorted()
}
}
}
private extension EnvironmentValues {
struct PinnedTargetsKey: EnvironmentKey {
static var defaultValue: [Pinned.Pin] = []
}
var pinnedTargets: PinnedTargetsKey.Value {
get { self[PinnedTargetsKey.self] }
set { self[PinnedTargetsKey.self] = newValue }
}
}
private extension EnvironmentValues {
struct CoordinateSpaceKey: EnvironmentKey {
static var defaultValue: CoordinateSpace = .global
}
var pinnedCoordinateSpace: CoordinateSpaceKey.Value {
get { self[CoordinateSpaceKey.self] }
set { self[CoordinateSpaceKey.self] = newValue }
}
}
private struct PinnedPreference: PreferenceKey {
static var defaultValue: [Pinned.Pin] = []
static func reduce(value: inout [Pinned.Pin], nextValue: () -> [Pinned.Pin]) {
value.append(contentsOf: nextValue())
}
}
private extension View {
func variadic<R: View>(@ViewBuilder _ transform: @escaping (_VariadicView.Children) -> R) -> some View {
_VariadicView.Tree(Helper(transform: transform)) { self }
}
}
private struct Helper<R: View>: _VariadicView.MultiViewRoot {
var transform: (_VariadicView.Children) -> R
func body(children: _VariadicView.Children) -> some View {
transform(children)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment