First version
This commit is contained in:
145
ResumeBuilder/Generic Elements/FlowLayout.swift
Normal file
145
ResumeBuilder/Generic Elements/FlowLayout.swift
Normal 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))
|
||||
}
|
||||
}
|
35
ResumeBuilder/Generic Elements/LeftBorderView.swift
Normal file
35
ResumeBuilder/Generic Elements/LeftBorderView.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
28
ResumeBuilder/Generic Elements/LeftImageLabel.swift
Normal file
28
ResumeBuilder/Generic Elements/LeftImageLabel.swift
Normal 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)
|
||||
}
|
||||
}
|
28
ResumeBuilder/Generic Elements/RightImageLabel.swift
Normal file
28
ResumeBuilder/Generic Elements/RightImageLabel.swift
Normal 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)
|
||||
}
|
||||
}
|
33
ResumeBuilder/Generic Elements/TagView.swift
Normal file
33
ResumeBuilder/Generic Elements/TagView.swift
Normal 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)
|
||||
}
|
||||
}
|
34
ResumeBuilder/Generic Elements/TitledSection.swift
Normal file
34
ResumeBuilder/Generic Elements/TitledSection.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user