Skip to content

Commit

Permalink
Merge pull request #15 from tevelee/feature/justification
Browse files Browse the repository at this point in the history
Proper justified layout
  • Loading branch information
tevelee authored Jun 26, 2024
2 parents a1baae9 + cc9a722 commit fd38a57
Show file tree
Hide file tree
Showing 11 changed files with 707 additions and 378 deletions.
65 changes: 35 additions & 30 deletions Sources/Flow/Example/ContentView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import SwiftUI

@available(macOS 14.0, *)
@available(macOS 13.0, *)
struct ContentView: View {
@State private var axis: Axis = .horizontal
@State private var contents: Contents = .boxes
Expand All @@ -11,6 +11,7 @@ struct ContentView: View {
@State private var justified: Justified = .none
@State private var horizontalAlignment: HAlignment = .center
@State private var verticalAlignment: VAlignment = .center
@State private var distibuteItemsEvenly: Bool = false
private let texts = "This is a long text that wraps nicely in flow layout".components(separatedBy: " ").map { string in
AnyView(Text(string))
}
Expand Down Expand Up @@ -68,22 +69,21 @@ struct ContentView: View {
}
Section(header: Text("Alignment")) {
switch axis {
case .horizontal:
picker($verticalAlignment)
case .vertical:
picker($horizontalAlignment)
case .horizontal: picker($verticalAlignment)
case .vertical: picker($horizontalAlignment)
}
}
Section(header: Text("Spacing")) {
stepper("Item", $itemSpacing)
stepper("Line", $lineSpacing)
}
Section(header: Text("Justification")) {
picker($justified)
Section(header: Text("Extras")) {
picker($justified, style: .radioGroup)
Toggle("Distibute evenly", isOn: $distibuteItemsEvenly.animation())
}
}
.listStyle(.sidebar)
.frame(minWidth: 280)
.frame(minWidth: 250)
.navigationTitle("Flow Layout")
.padding()
} detail: {
Expand All @@ -98,32 +98,34 @@ struct ContentView: View {
.frame(maxWidth: width, maxHeight: height)
.border(.red)
}
.frame(minWidth: 600, minHeight: 500)
.frame(minWidth: 600, minHeight: 600)
}

private func stepper(_ title: String, _ selection: Binding<CGFloat?>) -> some View {
HStack {
Toggle(isOn: Binding(get: { selection.wrappedValue != nil },
set: { selection.wrappedValue = $0 ? 8 : nil }).animation()) {
Toggle(isOn: Binding(
get: { selection.wrappedValue != nil },
set: { selection.wrappedValue = $0 ? 8 : nil }).animation()
) {
Text(title)
}
if let value = selection.wrappedValue {
Text("\(value.formatted())")
Stepper("", value: Binding(get: { value },
set: { selection.wrappedValue = $0 }).animation(), step: 4)
Stepper("", value: Binding(
get: { value },
set: { selection.wrappedValue = $0 }
).animation(), step: 4)
}
}.fixedSize()
}

private func picker<Value>(_ selection: Binding<Value>) -> some View where Value: Hashable & CaseIterable & CustomStringConvertible, Value.AllCases: RandomAccessCollection {
private func picker<Value>(_ selection: Binding<Value>, style: some PickerStyle = .segmented) -> some View where Value: Hashable & CaseIterable & CustomStringConvertible, Value.AllCases: RandomAccessCollection {
Picker("", selection: selection.animation()) {
ForEach(Value.allCases, id: \.self) { value in
Text(value.description).tag(value)
}
}
#if !os(watchOS)
.pickerStyle(.segmented)
#endif
.pickerStyle(style)
}

private var layout: AnyLayout {
Expand All @@ -134,7 +136,8 @@ struct ContentView: View {
alignment: verticalAlignment.value,
itemSpacing: itemSpacing,
rowSpacing: lineSpacing,
justification: justified.justification
justification: justified.justification,
distibuteItemsEvenly: distibuteItemsEvenly
)
)
case .vertical:
Expand All @@ -143,7 +146,8 @@ struct ContentView: View {
alignment: horizontalAlignment.value,
itemSpacing: itemSpacing,
columnSpacing: lineSpacing,
justification: justified.justification
justification: justified.justification,
distibuteItemsEvenly: distibuteItemsEvenly
)
)
}
Expand All @@ -157,25 +161,26 @@ enum Contents: String, CustomStringConvertible, CaseIterable {
var description: String { rawValue }
}

@available(macOS 13.0, *)
enum Justified: String, CustomStringConvertible, CaseIterable {
case none
case stretchItems
case stretchSpaces
case none = "no justification"
case stretchItems = "stretch items"
case stretchSpaces = "stretch spaces"
case stretchItemsAndSpaces = "stretch both"

var description: String { rawValue }

var justification: Justification? {
switch self {
case .none: nil
case .stretchItems: .stretchItems
case .stretchSpaces: .stretchSpaces
case .none: nil
case .stretchItems: .stretchItems
case .stretchSpaces: .stretchSpaces
case .stretchItemsAndSpaces: .stretchItemsAndSpaces
}
}
}

@available(macOS 14.0, *)
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
@available(macOS 13.0, *)
#Preview {
ContentView()
}
20 changes: 15 additions & 5 deletions Sources/Flow/HFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ public struct HFlow<Content: View>: View {
itemSpacing: CGFloat? = nil,
rowSpacing: CGFloat? = nil,
justification: Justification? = nil,
distibuteItemsEvenly: Bool = false,
@ViewBuilder content contentBuilder: () -> Content
) {
content = contentBuilder()
layout = HFlowLayout(
alignment: alignment,
itemSpacing: itemSpacing,
rowSpacing: rowSpacing,
justification: justification
justification: justification,
distibuteItemsEvenly: distibuteItemsEvenly
)
}

Expand All @@ -65,13 +67,15 @@ public struct HFlow<Content: View>: View {
alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
justification: Justification? = nil,
distibuteItemsEvenly: Bool = false,
@ViewBuilder content contentBuilder: () -> Content
) {
self.init(
alignment: alignment,
itemSpacing: spacing,
rowSpacing: spacing,
justification: justification,
distibuteItemsEvenly: distibuteItemsEvenly,
content: contentBuilder
)
}
Expand Down Expand Up @@ -105,13 +109,15 @@ extension HFlow: Layout where Content == EmptyView {
alignment: VerticalAlignment = .center,
itemSpacing: CGFloat? = nil,
rowSpacing: CGFloat? = nil,
justification: Justification? = nil
justification: Justification? = nil,
distibuteItemsEvenly: Bool = false
) {
self.init(
alignment: alignment,
itemSpacing: itemSpacing,
rowSpacing: rowSpacing,
justification: justification
justification: justification,
distibuteItemsEvenly: distibuteItemsEvenly
) {
EmptyView()
}
Expand All @@ -136,15 +142,15 @@ extension HFlow: Layout where Content == EmptyView {
}
}

public func sizeThatFits(proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout ()) -> CGSize {
public func sizeThatFits(proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout FlowLayoutCache) -> CGSize {
layout.sizeThatFits(
proposal: proposal,
subviews: subviews,
cache: &cache
)
}

public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout ()) {
public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout FlowLayoutCache) {
layout.placeSubviews(
in: bounds,
proposal: proposal,
Expand All @@ -153,6 +159,10 @@ extension HFlow: Layout where Content == EmptyView {
)
}

public func makeCache(subviews: LayoutSubviews) -> FlowLayoutCache {
FlowLayoutCache(subviews, axis: .horizontal)
}

public static var layoutProperties: LayoutProperties {
HFlowLayout.layoutProperties
}
Expand Down
14 changes: 10 additions & 4 deletions Sources/Flow/Internal/HFlowLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,33 @@ public struct HFlowLayout {
alignment: VerticalAlignment = .center,
itemSpacing: CGFloat? = nil,
rowSpacing: CGFloat? = nil,
justification: Justification? = nil
justification: Justification? = nil,
distibuteItemsEvenly: Bool = false
) {
layout = .horizontal(
alignment: alignment,
itemSpacing: itemSpacing,
lineSpacing: rowSpacing,
justification: justification
justification: justification,
distibuteItemsEvenly: distibuteItemsEvenly
)
}
}

@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)
extension HFlowLayout: Layout {
public func sizeThatFits(proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout ()) -> CGSize {
public func sizeThatFits(proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout FlowLayoutCache) -> CGSize {
layout.sizeThatFits(proposal: proposal, subviews: subviews, cache: &cache)
}

public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout ()) {
public func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: LayoutSubviews, cache: inout FlowLayoutCache) {
layout.placeSubviews(in: bounds, proposal: proposal, subviews: subviews, cache: &cache)
}

public func makeCache(subviews: LayoutSubviews) -> FlowLayoutCache {
FlowLayoutCache(subviews, axis: .horizontal)
}

public static var layoutProperties: LayoutProperties {
var properties = LayoutProperties()
properties.stackOrientation = .horizontal
Expand Down
Loading

0 comments on commit fd38a57

Please sign in to comment.