import SwiftUI /// Eine Reihe, die umbricht statt zu quetschen. /// /// **Der Anlass.** Im Kopf einer Detailseite stehen Abspielen, Von vorn, /// Herunterladen und zwei Sprachknöpfe nebeneinander. In einem `HStack` /// teilen sie sich die Breite — auf einem Telefon wurde daraus eine Reihe /// von Säulen aus einzelnen Buchstaben, untereinander gesetzt. Lesbar war /// nichts mehr, und angetippt hätte man trotzdem das Richtige. /// /// **Umbrechen ist die Antwort, nicht Weglassen.** Ein Knopf, der auf einem /// schmalen Gerät verschwindet, ist eine zweite Oberfläche, die gepflegt /// werden will. Zwei Zeilen sind kein Mangel; sie sind, was dort hinpasst. /// /// **Warum eigener `Layout` und kein `ViewThatFits`.** Das wählt zwischen /// fertigen Entwürfen — man müsste jede Aufteilung von Hand hinschreiben, /// und bei fünf Knöpfen sind das fünf. Hier wird gerechnet. public struct Flusszeile: Layout { private let abstand: CGFloat private let zeilenabstand: CGFloat public init(abstand: CGFloat = FlaroSpacing.regular, zeilenabstand: CGFloat? = nil) { self.abstand = abstand self.zeilenabstand = zeilenabstand ?? abstand } public func sizeThatFits( proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) -> CGSize { let breite = proposal.width ?? .infinity let zeilen = umbrechen(subviews, in: breite) let hoehe = zeilen.reduce(CGFloat.zero) { $0 + $1.hoehe } + CGFloat(max(0, zeilen.count - 1)) * zeilenabstand // **Die breiteste Zeile, nicht die angebotene Breite.** Sonst nimmt // die Reihe das ganze Fenster ein, auch wenn zwei Knöpfe darin // stehen — und alles daneben rutscht weg. let genutzt = zeilen.map(\.breite).max() ?? 0 return CGSize(width: min(genutzt, breite), height: hoehe) } public func placeSubviews( in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void ) { var y = bounds.minY for zeile in umbrechen(subviews, in: bounds.width) { var x = bounds.minX for index in zeile.teile { let groesse = subviews[index].sizeThatFits(.unspecified) subviews[index].place( at: CGPoint(x: x, y: y + (zeile.hoehe - groesse.height) / 2), proposal: ProposedViewSize(groesse) ) x += groesse.width + abstand } y += zeile.hoehe + zeilenabstand } } private struct Zeile { var teile: [Int] = [] var breite: CGFloat = 0 var hoehe: CGFloat = 0 } private func umbrechen(_ subviews: Subviews, in breite: CGFloat) -> [Zeile] { var zeilen: [Zeile] = [] var jetzt = Zeile() for index in subviews.indices { let groesse = subviews[index].sizeThatFits(.unspecified) let dazu = jetzt.teile.isEmpty ? groesse.width : jetzt.breite + abstand + groesse.width // **Das erste Element bleibt immer stehen.** Ein Knopf, der // allein schon breiter ist als die Zeile, käme sonst in eine // leere Zeile und von dort in die nächste, endlos. if !jetzt.teile.isEmpty, dazu > breite { zeilen.append(jetzt) jetzt = Zeile() jetzt.teile = [index] jetzt.breite = groesse.width jetzt.hoehe = groesse.height continue } jetzt.teile.append(index) jetzt.breite = dazu jetzt.hoehe = max(jetzt.hoehe, groesse.height) } if !jetzt.teile.isEmpty { zeilen.append(jetzt) } return zeilen } }