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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f85891aba4
commit
94458ef41c
+5172
-5160
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,12 @@ struct AudioLanguagePicker: View {
|
||||
Image(systemName: kind == .audio ? "waveform" : "captions.bubble")
|
||||
Text("\(Text(title)): \(currentLabel)")
|
||||
.font(.flaroCardTitle)
|
||||
// **Eine Zeile, keine Säule.** Umbrochen wird die
|
||||
// Reihe, nicht der einzelne Knopf — sonst steht
|
||||
// „Untertitel" wieder Buchstabe für Buchstabe
|
||||
// untereinander.
|
||||
.lineLimit(1)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
Image(systemName: "chevron.up.chevron.down")
|
||||
.font(.flaroCaption)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -291,7 +291,10 @@ public struct HeroBanner<Actions: View>: View {
|
||||
.frame(maxWidth: 640 * scale, alignment: .leading)
|
||||
}
|
||||
|
||||
HStack(spacing: FlaroSpacing.regular) {
|
||||
// **Umbrechen statt quetschen.** In einem `HStack` teilen sich
|
||||
// fünf Knöpfe die Breite; auf einem Telefon wurde daraus eine
|
||||
// Reihe von Säulen aus einzelnen Buchstaben.
|
||||
Flusszeile {
|
||||
actions
|
||||
}
|
||||
.padding(.top, FlaroSpacing.tight)
|
||||
|
||||
Reference in New Issue
Block a user