Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Forked from Thomvis/CV+ScrollView.swift
Last active October 25, 2019 18:02
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 chriseidhof/e2fe71404c33e112cc954bfe60d30cf1 to your computer and use it in GitHub Desktop.
Save chriseidhof/e2fe71404c33e112cc954bfe60d30cf1 to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// FlowLayoutST
//
// Created by Chris Eidhof on 22.08.19.
// Copyright © 2019 Chris Eidhof. All rights reserved.
//
import SwiftUI
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 = 0 as CGFloat
var currentY = 0 as CGFloat
var lineHeight = 0 as CGFloat
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)
}
var size: CGSize {
return CGSize(width: containerSize.width, height: currentY + lineHeight)
}
}
func flowLayout<Elements>(for elements: Elements, containerSize: CGSize, sizes: [Elements.Element.ID: CGSize]) -> (offsets: [Elements.Element.ID: CGSize], layoutHeight: CGFloat) where Elements: RandomAccessCollection, Elements.Element: Identifiable {
var state = FlowLayout(containerSize: containerSize)
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 (result, state.size.height)
}
func singleLineLayout<Elements>(for elements: Elements, containerSize: CGSize, sizes: [Elements.Element.ID: CGSize]) -> [Elements.Element.ID: CGSize] where Elements: RandomAccessCollection, Elements.Element: Identifiable {
var result: [Elements.Element.ID: CGSize] = [:]
var offset = CGSize.zero
for element in elements {
result[element.id] = offset
let size = sizes[element.id] ?? CGSize.zero
offset.width += size.width + 10
}
return result
}
fileprivate struct HeightKey: PreferenceKey {
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
static let defaultValue: CGFloat = 0
}
struct CollectionView<Elements, Content>: View where Elements: RandomAccessCollection, Content: View, Elements.Element: Identifiable {
var data: Elements
var layout: (Elements, CGSize, [Elements.Element.ID: CGSize]) -> (offsets: [Elements.Element.ID: CGSize], height: CGFloat)
var content: (Elements.Element) -> Content
@State private var sizes: [Elements.Element.ID: CGSize] = [:]
@State private var height: CGFloat = 0
private func bodyHelper(containerSize: CGSize, laidout: (offsets: [Elements.Element.ID: CGSize], height: CGFloat)) -> some View {
return ZStack(alignment: .topLeading) {
ForEach(data) {
PropagateSize(content: self.content($0), id: $0.id)
.offset(laidout.offsets[$0.id] ?? CGSize.zero)
.animation(.default)
}
Color.clear
.frame(width: containerSize.width, height: containerSize.height)
.fixedSize()
}.onPreferenceChange(CollectionViewSizeKey.self) {
self.sizes = $0
}.background(Color.red)
.preference(key: HeightKey.self, value: laidout.height)
}
var body: some View {
GeometryReader { proxy in
self.bodyHelper(containerSize: proxy.size, laidout: self.layout(self.data, proxy.size, self.sizes))
}
.onPreferenceChange(HeightKey.self, perform: { self.height = $0 })
.frame(height: height)
}
}
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 {
content.background(GeometryReader { proxy in
Color.clear.preference(key: CollectionViewSizeKey<ID>.self, value: [self.id: proxy.size])
})
}
}
// todo hack
extension String: Identifiable {
public var id: String { self }
}
struct ContentView: View {
let strings: [String] = (1...10).map { "Item \($0) " + String(repeating: "x", count: Int.random(in: 0...10)) }
@State var dividerWidth: CGFloat = 100
var body: some View {
VStack {
HStack {
Rectangle()
.fill(Color.red)
.frame(width: dividerWidth)
ScrollView {
CollectionView(data: strings, layout: flowLayout) {
Text($0)
.padding(10)
.background(Color.gray)
}
}
}
Slider(value: $dividerWidth, in: 0...500)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment