Created
March 5, 2022 18:26
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
// Adapted from: https://stackoverflow.com/questions/62102647/swiftui-hstack-with-wrap-and-dynamic-height/62103264#62103264 | |
struct WrappingHStack<Model, V>: View where Model: Hashable, V: View { | |
typealias ViewGenerator = (Model) -> V | |
var models: [Model] | |
var isScrollView: Bool | |
var viewGenerator: ViewGenerator | |
var horizontalSpacing: CGFloat = 2 | |
var verticalSpacing: CGFloat = 0 | |
@State private var totalHeight = CGFloat.zero | |
init(models: [Model], isScrollView: Bool = true, viewGenerator: @escaping ViewGenerator) { | |
self.models = models | |
self.isScrollView = isScrollView | |
self.viewGenerator = viewGenerator | |
self.totalHeight = isScrollView ? CGFloat.zero : CGFloat.infinity | |
} | |
var body: some View { | |
if isScrollView { | |
VStack { | |
GeometryReader { geometry in | |
self.generateContent(in: geometry) | |
} | |
} | |
.frame(height: totalHeight) | |
} else { | |
VStack { | |
GeometryReader { geometry in | |
self.generateContent(in: geometry) | |
} | |
} | |
.frame(maxHeight: totalHeight) | |
} | |
} | |
private func generateContent(in geometry: GeometryProxy) -> some View { | |
var width = CGFloat.zero | |
var height = CGFloat.zero | |
return ZStack(alignment: .topLeading) { | |
ForEach(self.models, id: \.self) { models in | |
viewGenerator(models) | |
.padding(.horizontal, horizontalSpacing) | |
.padding(.vertical, verticalSpacing) | |
.alignmentGuide(.leading, computeValue: { dimension in | |
if abs(width - dimension.width) > geometry.size.width { | |
width = 0 | |
height -= dimension.height | |
} | |
let result = width | |
if models == self.models.last! { | |
width = 0 // last item | |
} else { | |
width -= dimension.width | |
} | |
return result | |
}) | |
.alignmentGuide(.top, computeValue: {_ in | |
let result = height | |
if models == self.models.last! { | |
height = 0 // last item | |
} | |
return result | |
}) | |
} | |
}.background(viewHeightReader($totalHeight)) | |
} | |
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View { | |
return GeometryReader { geometry -> Color in | |
let rect = geometry.frame(in: .local) | |
DispatchQueue.main.async { | |
binding.wrappedValue = rect.size.height | |
} | |
return .clear | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment