Last active
August 17, 2024 10:59
-
-
Save shaps80/88fb7414a8f4c27847af732251eafef2 to your computer and use it in GitHub Desktop.
pinned modified now allows any hashable value
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 | |
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