Files
Flaro/Packages/FlaroKit/Sources/FlaroDesign/Components/Flusszeile.swift
T
ScarriffleandClaude Opus 5 94458ef41c Fünf Knöpfe in einer Reihe wurden auf dem Telefon zu Säulen
In einem `HStack` teilen sich die Knöpfe die Breite. Abspielen, Von vorn,
Herunterladen und zwei Sprachknöpfe passten auf einem Telefon nicht
nebeneinander — heraus kamen Säulen aus einzelnen Buchstaben, senkrecht
untereinander. Lesbar war nichts mehr; angetippt hätte man trotzdem das
Richtige.

`Flusszeile` bricht um, statt zu quetschen: was nicht mehr hinpasst,
kommt in die nächste Zeile. Zwei Zeilen sind kein Mangel, sie sind, was
dort hinpasst — und ein Knopf, der auf schmalen Geräten verschwindet,
wäre eine zweite Oberfläche, die gepflegt werden will.

Kein `ViewThatFits`: das wählt zwischen fertigen Entwürfen, und die
müsste man für jede Zahl von Knöpfen von Hand hinschreiben. Hier wird
gerechnet.

Dazu bleibt die Beschriftung eines Sprachknopfs einzeilig. Umbrochen wird
die Reihe, nicht das Wort darin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-10 23:20:57 +02:00

104 lines
3.7 KiB
Swift

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
}
}