アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

SwiftUIでFlowLayout

仕事で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
    }
}