仕事でSwiftUIを使っていて、書いたものを載せておきます。SwiftUIのCustomView、なかなかわかりにくくて辛かったです。
FlowLayout
childrenとかlineSpacingとかはinitで入れたりしてください。
iOS13だと高さが小数点で変化して、無限に更新されたりして辛かったのでアイテム数が同じなら再計算しないようにしました。なので要素が入れ替わったり、更新されたりするものだと使用できません。辛い。
配置部分
composeみたいに place(x, y)
でやらせてくれって思った。
private let children: [AnyView] private let lineSpacing: CGFloat @State private var flowItemPlaces: FlowPlaces = FlowPlaces(items: [:]) private func makeBody(viewWidth _: CGFloat) -> some View { GeometryReader { geometry in ZStack(alignment: .topLeading) { Rectangle().foregroundColor(.clear) ForEach(children.indices) { index in let child: AnyView = children[index] let place: FlowPlaces.Place? = flowItemPlaces.get(index) child .alignmentGuide(.top, computeValue: { dimension in dimension[.top] - (place?.y ?? 0) }) .alignmentGuide(.leading, computeValue: { dimension in dimension[.leading] - (place?.x ?? 0) }) .anchorPreference( key: FlowPreferencesKey.self, value: .bounds, transform: { anchor in let rect: CGRect = geometry[anchor] return FlowPreferencesKey.Preference( width: geometry.size.width, items: [index: .init(bounds: rect)] ) } ) } } .onPreferenceChange(FlowPreferencesKey.self) { preferences in if flowItemPlaces.items.count != preferences.items.count { let new: MeasureResult = preferences.getPlaces(lineSpacing: lineSpacing) flowItemPlaces = new.places totalHeight = new.height } } } }
計算部分
まぁここは計算するだけなので特筆すべきものは無いです。
private struct FlowPreferencesKey: PreferenceKey { public static var defaultValue: Preference = .init(width: .zero, items: [:]) public static func reduce(value: inout Preference, nextValue: () -> Preference) { value.merge(pref: nextValue()) } struct Preference: Equatable { var width: CGFloat var items: [Int: Item] struct Item: Equatable { let bounds: CGRect } init( width: CGFloat, items: [Int: Item] ) { self.width = width self.items = items } mutating func merge(pref: Preference) { width = pref.width items.merge(pref.items, uniquingKeysWith: { _, right in right }) } func getPlaces(lineSpacing: CGFloat) -> MeasureResult { var resultPlace: [Int: FlowPlaces.Place] = [:] var currentX: CGFloat = 0 var currentY: CGFloat = 0 var currentLineMaxHeight: CGFloat = 0 let maxIndex: Int? = items.keys.max() for (index, item) in (0 ... (maxIndex ?? 0)) .map { index in (index, items[index]) } { guard let item = item else { continue } if width - currentX < item.bounds.width { currentY += currentLineMaxHeight + lineSpacing currentX = 0 currentLineMaxHeight = 0 } resultPlace[index] = FlowPlaces.Place( x: currentX, y: currentY ) currentX += item.bounds.width currentLineMaxHeight = max(currentLineMaxHeight, item.bounds.height) } return MeasureResult( height: currentY + currentLineMaxHeight, places: .init(items: resultPlace) ) } } } private struct MeasureResult: Equatable { let height: CGFloat let places: FlowPlaces } private struct FlowPlaces: Equatable { let items: [Int: Place] func get(_ index: Int) -> Place? { items[index] } struct Place: Equatable { let x: CGFloat let y: CGFloat } }