Skip to content

Instantly share code, notes, and snippets.

@simme
Created February 19, 2020 20:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simme/37e670c5f897f89807707f1f22a49c43 to your computer and use it in GitHub Desktop.
Save simme/37e670c5f897f89807707f1f22a49c43 to your computer and use it in GitHub Desktop.
import SwiftUI
extension String: Identifiable {
public var id: String { self }
}
struct CollectionViewSizeKey<ID: Hashable>: PreferenceKey {
typealias Value = [ID: CGSize]
static var defaultValue: [ID: CGSize] { [:] }
static func reduce(value: inout [ID: CGSize], nextValue: () -> [ID: CGSize]) {
value.merge(nextValue(), uniquingKeysWith: { $1 })
}
}
struct PropagateSize<V: View, ID: Hashable>: View {
var content: V
var id: ID
var body: some View {
self.content
.background(GeometryReader { proxy in
Color.clear.preference(key: CollectionViewSizeKey<ID>.self, value: [self.id: proxy.size])
})
}
}
func singleLineLayout<Elements>(for elements: Elements, containerSize: CGSize, sizes: [Elements.Element.ID: CGSize]) -> (CGFloat, [Elements.Element.ID: CGSize])
where Elements: RandomAccessCollection, Elements.Element: Identifiable
{
var result: [Elements.Element.ID: CGSize] = [:]
var offset = CGSize.zero
var height: CGFloat = 0
for element in elements {
result[element.id] = offset
let size = sizes[element.id] ?? .zero
offset.width += size.width + 12
height = max(height, size.height)
}
return (height, result)
}
struct FlowLayout {
let spacing: UIOffset
let containerSize: CGSize
init(containerSize: CGSize, spacing: UIOffset = UIOffset(horizontal: 10, vertical: 10)) {
self.spacing = spacing
self.containerSize = containerSize
}
var currentX: CGFloat = 0
var currentY: CGFloat = 0
var lineHeight: CGFloat = 0
mutating func add(element size: CGSize) -> CGRect {
if currentX + size.width > containerSize.width {
currentX = 0
currentY += lineHeight + spacing.vertical
lineHeight = 0
}
defer {
lineHeight = max(lineHeight, size.height)
currentX += size.width + spacing.horizontal
}
return CGRect(origin: CGPoint(x: currentX, y: currentY), size: size)
}
}
func flowLayout<Elements>(spacing: UIOffset = UIOffset(horizontal: 12, vertical: 12)) -> (Elements, CGSize, [Elements.Element.ID: CGSize]) -> (CGFloat, [Elements.Element.ID: CGSize])
where Elements: RandomAccessCollection, Elements.Element: Identifiable
{
return { elements, containerSize, sizes in
var state = FlowLayout(containerSize: containerSize, spacing: spacing)
var result: [Elements.Element.ID: CGSize] = [:]
for element in elements {
let rect = state.add(element: sizes[element.id] ?? .zero)
result[element.id] = CGSize(width: rect.origin.x, height: rect.origin.y)
}
return (state.currentY + state.lineHeight, result)
}
}
struct CollectionView<Elements, Content>: View
where Elements: RandomAccessCollection, Content: View, Elements.Element: Identifiable
{
var data: Elements
var layout: (Elements, CGSize, [Elements.Element.ID: CGSize]) -> (CGFloat, [Elements.Element.ID: CGSize])
var content: (Elements.Element) -> Content
@State private var sizes: [Elements.Element.ID: CGSize] = [:]
private func bodyHelper(containerSize: CGSize, layout: (height: CGFloat, offsets: [Elements.Element.ID: CGSize])) -> some View {
ZStack(alignment: .topLeading) {
ForEach(self.data) {
PropagateSize(content: self.content($0), id: $0.id)
.offset(layout.offsets[$0.id] ?? .zero)
.animation(.default)
}
Color.clear.frame(width: containerSize.width, height: layout.height)
}
.background(Color.green)
.onPreferenceChange(CollectionViewSizeKey.self) { self.sizes = $0 }
}
var body: some View {
GeometryReader { proxy in
self.bodyHelper(containerSize: proxy.size, layout: self.layout(self.data, proxy.size, self.sizes))
}
.background(Color.pink.opacity(0.3))
}
}
// MARK: -
struct CompactStatView: View {
var stat: Stat
var body: some View {
HStack {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: 12)
VStack(alignment: .leading) {
Text(stat.title)
Text(stat.description)
}
}
.frame(maxWidth: 120, maxHeight: 40)
}
}
// MARK: -
struct Stat: Identifiable {
var id: UUID
var title: String
var description: String
}
struct NewExperiment: View {
var texts: [String] = ["All", "Option A", "Option B", "Option C", "Option D"]
var stats: [Stat] = [
.init(id: UUID(), title: "Metric A", description: "23467 u"),
.init(id: UUID(), title: "Metric B", description: "234 dB"),
.init(id: UUID(), title: "Metric C", description: "8 °C"),
.init(id: UUID(), title: "Metric D", description: "2.3 Kg")
]
var body: some View {
NavigationView {
ScrollView {
ZStack {
LinearGradient(gradient: Gradient(colors: [Color.black.opacity(0.4), Color.black.opacity(0)]), startPoint: .top, endPoint: .bottom)
.aspectRatio(0.8, contentMode: .fill)
CollectionView(data: texts, layout: flowLayout(spacing: UIOffset(horizontal: 12, vertical: 12))) { item in
Text(item)
.font(.custom("AvenirNext-DemiBold", size: 15))
.padding(EdgeInsets(top: 4, leading: 16, bottom: 4, trailing: 16))
.background(RoundedRectangle(cornerRadius: 30).fill(Color.black.opacity(item == "All" ? 0.4 : 0.2)))
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.black.opacity(0.2), lineWidth: 1)
.padding(-2)
)
}
.padding()
.offset(y: 128)
CollectionView(data: self.stats, layout: flowLayout(spacing: UIOffset(horizontal: 12, vertical: 12))) { stat in
CompactStatView(stat: stat)
}
.padding()
.offset(y: 428)
}
}
.edgesIgnoringSafeArea(.vertical)
.navigationBarTitle("Overview")
.navigationBarItems(
leading: HStack { Button(action: {}) { Image(systemName: "gear").foregroundColor(.black) }},
trailing: HStack { Button(action: {}) { Image(systemName: "plus").foregroundColor(.black) }}
)
}
}
}
struct NewExperiment_Previews: PreviewProvider {
static var previews: some View {
NewExperiment()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment