NewsBlur/clients/ios/Classes/SwiftUIUtilities.swift
David Sinclair 51684a7812 #1247 (Mac Catalyst edition)
- Rewrote the trainer view using SwiftUI, to be native instead of a web view.
- Added a Feed class and other data enhancements to support this.
2024-04-04 20:06:11 -05:00

210 lines
6.3 KiB
Swift

//
// 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))
}
}
extension Text {
func colored(_ color: Color) -> Text {
if #available(iOS 17.0, *) {
self.foregroundStyle(color)
} else {
self.foregroundColor(color)
}
}
}
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)
)
}
static func themed(_ hex: [NSNumber]) -> Color {
return Color(ThemeManager.color(fromRGB: hex))
}
}
extension NSArray {
@objc(safeObjectAtIndex:) func safeObject(at index: Int) -> Any? {
if index >= 0, index < count {
return self[index]
} else {
return nil
}
}
}
// 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)
}
}
struct WrappingHStack<Model, V>: View where Model: Hashable, V: View {
typealias ViewGenerator = (Model) -> V
var models: [Model]
var horizontalSpacing: CGFloat = 2
var verticalSpacing: CGFloat = 0
var viewGenerator: ViewGenerator
@State private var totalHeight
= CGFloat.zero // << variant for ScrollView/List
// = CGFloat.infinity // << variant for VStack
var body: some View {
VStack {
GeometryReader { geometry in
self.generateContent(in: geometry)
}
}
.frame(height: totalHeight)// << variant for ScrollView/List
//.frame(maxHeight: totalHeight) // << variant for VStack
}
private func generateContent(in geometry: GeometryProxy) -> some View {
var width = CGFloat.zero
var height = CGFloat.zero
return ZStack(alignment: .topLeading) {
ForEach(self.models, id: \.self) { models in
viewGenerator(models)
.padding(.horizontal, horizontalSpacing)
.padding(.vertical, verticalSpacing)
.alignmentGuide(.leading, computeValue: { dimension in
if (abs(width - dimension.width) > geometry.size.width)
{
width = 0
height -= dimension.height
}
let result = width
if models == self.models.last! {
width = 0 //last item
} else {
width -= dimension.width
}
return result
})
.alignmentGuide(.top, computeValue: {dimension in
let result = height
if models == self.models.last! {
height = 0 // last item
}
return result
})
}
}.background(viewHeightReader($totalHeight))
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
}