Skip to content

Instantly share code, notes, and snippets.

@Thomvis
Created October 21, 2019 17:49
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Thomvis/cb86cad637e21f5385cd243d4f7a8ad1 to your computer and use it in GitHub Desktop.
Save Thomvis/cb86cad637e21f5385cd243d4f7a8ad1 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]) -> [Elements.Element.ID: CGSize] 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
}
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
}
struct CollectionView<Elements, Content>: View where Elements: RandomAccessCollection, Content: View, Elements.Element: Identifiable {
var data: Elements
var layout: (Elements, CGSize, [Elements.Element.ID: CGSize]) -> [Elements.Element.ID: CGSize]
var content: (Elements.Element) -> Content
@State private var sizes: [Elements.Element.ID: CGSize] = [:]
private func bodyHelper(containerSize: CGSize, offsets: [Elements.Element.ID: CGSize]) -> some View {
ZStack(alignment: .topLeading) {
ForEach(data) {
PropagateSize(content: self.content($0), id: $0.id)
.offset(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)
}
var body: some View {
GeometryReader { proxy in
self.bodyHelper(containerSize: proxy.size, offsets: self.layout(self.data, proxy.size, self.sizes))
}
}
}
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()
}
}
@Thomvis
Copy link
Author

Thomvis commented Oct 21, 2019

When CollectionView is wrapped in a ScrollView, a warning is printed to the console:

Bound preference CollectionViewSizeKey<String> tried to update multiple times per frame.

@nachonavarro
Copy link

Did you manage to understand why it's giving that warning? I'm getting the same warning "Bound preference WidthPreferenceKey tried to update multiple times per frame."

@muhammadelkady25
Copy link

@nachonavarro gives me the same warning did you find any solution?

@Thomvis
Copy link
Author

Thomvis commented Nov 28, 2019

I still have the warning, but it seems harmless in some cases. In others, I now force a collection change in onAppear and asynchronously undo the change immediately after. It's not nice, but it makes sure the view doesn't break.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment