First version

This commit is contained in:
Christoph Hagen
2023-08-18 22:47:24 +02:00
parent 1d6e36e2de
commit bd87a4fb6f
48 changed files with 1453 additions and 30 deletions

View File

@ -0,0 +1,145 @@
import SwiftUI
@available(iOS 16.0, *)
struct FlowLayout: Layout {
var alignment: Alignment = .center
var spacing: CGFloat?
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
let result = FlowResult(
in: proposal.replacingUnspecifiedDimensions().width,
subviews: subviews,
alignment: alignment,
spacing: spacing
)
return result.bounds
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
let result = FlowResult(
in: proposal.replacingUnspecifiedDimensions().width,
subviews: subviews,
alignment: alignment,
spacing: spacing
)
for row in result.rows {
let rowXOffset = (bounds.width - row.frame.width) * alignment.horizontal.percent
for index in row.range {
let xPos = rowXOffset + row.frame.minX + row.xOffsets[index - row.range.lowerBound] + bounds.minX
let rowYAlignment = (row.frame.height - subviews[index].sizeThatFits(.unspecified).height) *
alignment.vertical.percent
let yPos = row.frame.minY + rowYAlignment + bounds.minY
subviews[index].place(at: CGPoint(x: xPos, y: yPos), anchor: .topLeading, proposal: .unspecified)
}
}
}
struct FlowResult {
var bounds = CGSize.zero
var rows = [Row]()
struct Row {
var range: Range<Int>
var xOffsets: [Double]
var frame: CGRect
}
init(in maxPossibleWidth: Double, subviews: Subviews, alignment: Alignment, spacing: CGFloat?) {
var itemsInRow = 0
var remainingWidth = maxPossibleWidth.isFinite ? maxPossibleWidth : .greatestFiniteMagnitude
var rowMinY = 0.0
var rowHeight = 0.0
var xOffsets: [Double] = []
for (index, subview) in zip(subviews.indices, subviews) {
let idealSize = subview.sizeThatFits(.unspecified)
if index != 0 && widthInRow(index: index, idealWidth: idealSize.width) > remainingWidth {
// Finish the current row without this subview.
finalizeRow(index: max(index - 1, 0), idealSize: idealSize)
}
addToRow(index: index, idealSize: idealSize)
if index == subviews.count - 1 {
// Finish this row; it's either full or we're on the last view anyway.
finalizeRow(index: index, idealSize: idealSize)
}
}
func spacingBefore(index: Int) -> Double {
guard itemsInRow > 0 else { return 0 }
return spacing ?? subviews[index - 1].spacing.distance(to: subviews[index].spacing, along: .horizontal)
}
func widthInRow(index: Int, idealWidth: Double) -> Double {
idealWidth + spacingBefore(index: index)
}
func addToRow(index: Int, idealSize: CGSize) {
let width = widthInRow(index: index, idealWidth: idealSize.width)
xOffsets.append(maxPossibleWidth - remainingWidth + spacingBefore(index: index))
// Allocate width to this item (and spacing).
remainingWidth -= width
// Ensure the row height is as tall as the tallest item.
rowHeight = max(rowHeight, idealSize.height)
// Can fit in this row, add it.
itemsInRow += 1
}
func finalizeRow(index: Int, idealSize: CGSize) {
let rowWidth = maxPossibleWidth - remainingWidth
rows.append(
Row(
range: index - max(itemsInRow - 1, 0) ..< index + 1,
xOffsets: xOffsets,
frame: CGRect(x: 0, y: rowMinY, width: rowWidth, height: rowHeight)
)
)
bounds.width = max(bounds.width, rowWidth)
let ySpacing = spacing ?? ViewSpacing().distance(to: ViewSpacing(), along: .vertical)
bounds.height += rowHeight + (rows.count > 1 ? ySpacing : 0)
rowMinY += rowHeight + ySpacing
itemsInRow = 0
rowHeight = 0
xOffsets.removeAll()
remainingWidth = maxPossibleWidth
}
}
}
}
private extension HorizontalAlignment {
var percent: Double {
switch self {
case .leading: return 0
case .trailing: return 1
default: return 0.5
}
}
}
private extension VerticalAlignment {
var percent: Double {
switch self {
case .top: return 0
case .bottom: return 1
default: return 0.5
}
}
}
struct FlowLayout_Previews: PreviewProvider {
static var previews: some View {
FlowLayout(alignment: .leading) {
ForEach(["Swift", "C", "C++", "Python"]) { tag in
Text(tag)
.fontWeight(.light)
.padding(.horizontal, 8)
.padding(.vertical, 3)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.gray.opacity(0.1))
)
}
}
.previewLayout(.fixed(width: 200, height: 150))
}
}

View File

@ -0,0 +1,35 @@
import SwiftUI
struct LeftBorderView<Content>: View where Content: View {
let color: Color
let spacing: CGFloat
let borderWidth: CGFloat
private let content: Content
init(color: Color, spacing: CGFloat, borderWidth: CGFloat, @ViewBuilder content: () -> Content) {
self.color = color
self.spacing = spacing
self.borderWidth = borderWidth
self.content = content()
}
var body: some View {
HStack(spacing: spacing) {
Rectangle()
.fill(color)
.frame(width: borderWidth)
content
}.fixedSize(horizontal: false, vertical: true)
}
}
struct LeftBorderView_Previews: PreviewProvider {
static var previews: some View {
LeftBorderView(color: .orange, spacing: 5, borderWidth: 3) {
Text("Some")
}
}
}

View File

@ -0,0 +1,28 @@
import SwiftUI
import SFSafeSymbols
struct LeftImageLabel: View {
let text: String
let systemSymbol: SFSymbol
init(_ text: String, systemSymbol: SFSymbol) {
self.text = text
self.systemSymbol = systemSymbol
}
var body: some View {
HStack(spacing: 0) {
Text(text)
Image(systemSymbol: systemSymbol)
.frame(width: 20)
}
}
}
struct LeftImageLabel_Previews: PreviewProvider {
static var previews: some View {
LeftImageLabel("Home address", systemSymbol: .house)
}
}

View File

@ -0,0 +1,28 @@
import SwiftUI
import SFSafeSymbols
struct RightImageLabel: View {
let text: String
let systemSymbol: SFSymbol
init(_ text: String, systemSymbol: SFSymbol) {
self.text = text
self.systemSymbol = systemSymbol
}
var body: some View {
HStack(spacing: 0) {
Image(systemSymbol: systemSymbol)
.frame(width: 20)
Text(text)
}
}
}
struct RightImageLabel_Previews: PreviewProvider {
static var previews: some View {
RightImageLabel("Home address", systemSymbol: .house)
}
}

View File

@ -0,0 +1,33 @@
import SwiftUI
struct TagView: View {
let text: String
let rounding: CGFloat
let color: Color
init(_ text: String, rounding: CGFloat, color: Color) {
self.text = text
self.rounding = rounding
self.color = color
}
var body: some View {
Text(text)
.fontWeight(.light)
.padding(.horizontal, rounding)
.padding(.vertical, 3)
.background(
RoundedRectangle(cornerRadius: rounding)
.fill(color)
)
}
}
struct TagView_Previews: PreviewProvider {
static var previews: some View {
TagView("Text", rounding: 8, color: .gray)
}
}

View File

@ -0,0 +1,34 @@
import SwiftUI
struct TitledSection<Content>: View where Content: View {
private let content: Content
private let title: String
private let spacing: CGFloat
init(title: String, spacing: CGFloat, @ViewBuilder content: () -> Content) {
self.title = title
self.spacing = spacing
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text(title)
.font(.title)
.fontWeight(.light)
.padding(.bottom, spacing)
content
}
}
}
struct TitledSection_Previews: PreviewProvider {
static var previews: some View {
TitledSection(title: "Title", spacing: 10) {
Text("Some more text")
}
}
}