2023-02-02 21:41:10 -06:00
|
|
|
//
|
|
|
|
// SwiftUIUtilities.swift
|
|
|
|
// NewsBlur
|
|
|
|
//
|
|
|
|
// Created by David Sinclair on 2023-02-01.
|
|
|
|
// Copyright © 2023 NewsBlur. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
/// Some useful SwiftUI extensions.
|
|
|
|
|
|
|
|
extension View {
|
|
|
|
@ViewBuilder
|
|
|
|
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
|
|
|
|
if condition {
|
|
|
|
transform(self)
|
|
|
|
} else {
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension View {
|
|
|
|
@ViewBuilder
|
|
|
|
func modify<Content: View>(@ViewBuilder _ transform: (Self) -> Content?) -> some View {
|
|
|
|
if let view = transform(self), !(view is EmptyView) {
|
|
|
|
view
|
|
|
|
} else {
|
|
|
|
self
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension View {
|
|
|
|
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
|
|
|
clipShape(RoundedCorner(radius: radius, corners: corners))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct RoundedCorner: Shape {
|
|
|
|
var radius: CGFloat = .infinity
|
|
|
|
var corners: UIRectCorner = .allCorners
|
|
|
|
|
|
|
|
func path(in rect: CGRect) -> Path {
|
|
|
|
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
|
|
|
|
return Path(path.cgPath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension Color {
|
|
|
|
static var random: Color {
|
|
|
|
return Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)
|
|
|
|
)
|
|
|
|
}
|
2023-03-02 15:31:21 -07:00
|
|
|
|
|
|
|
static func themed(_ hex: [NSNumber]) -> Color {
|
|
|
|
return Color(ThemeManager.color(fromRGB: hex))
|
|
|
|
}
|
2023-02-02 21:41:10 -06:00
|
|
|
}
|
2023-03-21 21:54:21 -07:00
|
|
|
|
|
|
|
// From https://www.swiftbysundell.com/articles/observing-swiftui-scrollview-content-offset/
|
|
|
|
|
|
|
|
struct PositionObservingView<Content: View>: View {
|
|
|
|
var coordinateSpace: CoordinateSpace
|
|
|
|
@Binding var position: CGPoint
|
|
|
|
@ViewBuilder var content: () -> Content
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
content()
|
|
|
|
.background(GeometryReader { geometry in
|
|
|
|
Color.clear.preference(
|
|
|
|
key: PreferenceKey.self,
|
|
|
|
value: geometry.frame(in: coordinateSpace).origin
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.onPreferenceChange(PreferenceKey.self) { position in
|
|
|
|
self.position = position
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private extension PositionObservingView {
|
|
|
|
struct PreferenceKey: SwiftUI.PreferenceKey {
|
|
|
|
static var defaultValue: CGPoint { .zero }
|
|
|
|
|
|
|
|
static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
|
|
|
|
// No-op
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct OffsetObservingScrollView<Content: View>: View {
|
|
|
|
var axes: Axis.Set = [.vertical]
|
|
|
|
var showsIndicators = true
|
|
|
|
@Binding var offset: CGPoint
|
|
|
|
@ViewBuilder var content: () -> Content
|
|
|
|
|
|
|
|
// The name of our coordinate space doesn't have to be
|
|
|
|
// stable between view updates (it just needs to be
|
|
|
|
// consistent within this view), so we'll simply use a
|
|
|
|
// plain UUID for it:
|
|
|
|
private let coordinateSpaceName = UUID()
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
ScrollView(axes, showsIndicators: showsIndicators) {
|
|
|
|
PositionObservingView(
|
|
|
|
coordinateSpace: .named(coordinateSpaceName),
|
|
|
|
position: Binding(
|
|
|
|
get: { offset },
|
|
|
|
set: { newOffset in
|
|
|
|
offset = CGPoint(
|
|
|
|
x: -newOffset.x,
|
|
|
|
y: -newOffset.y
|
|
|
|
)
|
|
|
|
}
|
|
|
|
),
|
|
|
|
content: content
|
|
|
|
)
|
|
|
|
}
|
|
|
|
.coordinateSpace(name: coordinateSpaceName)
|
|
|
|
}
|
|
|
|
}
|