Skip to content

Commit 5dbb4f1

Browse files
committed
Redesign Dynamic Island: single right-side pill instead of broken dual wings
1 parent 453db13 commit 5dbb4f1

1 file changed

Lines changed: 84 additions & 117 deletions

File tree

‎Sources/VoidBar/UI/DynamicIslandView.swift‎

Lines changed: 84 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -3,153 +3,120 @@ import SwiftUI
33
struct DynamicIslandView: View {
44
@ObservedObject var vm: NotchViewModel
55

6-
// The width of the actual physical notch
76
private var notchWidth: CGFloat { vm.geometry.notchSize.width }
8-
// The height of the physical notch
97
private var notchHeight: CGFloat { vm.geometry.notchSize.height }
10-
11-
// Animation for equalizer
12-
@State private var phase: CGFloat = 0
138

149
var body: some View {
15-
ZStack {
16-
if vm.timer.state == .running {
17-
timerPill
18-
} else if vm.media.isPlaying {
19-
mediaPill
20-
} else if let weather = vm.weather.weather {
21-
weatherPill(weather)
10+
// Single right-side wing that merges with the notch edge.
11+
// By placing the pill inside a wider-than-notch frame and aligning it
12+
// to trailing, we guarantee it sits flush against the notch's right
13+
// edge regardless of how wide the notch is on a given Mac.
14+
HStack(spacing: 0) {
15+
Spacer(minLength: 0)
16+
17+
if pillKind != nil {
18+
activePill
19+
.transition(.asymmetric(
20+
insertion: .move(edge: .trailing).combined(with: .opacity),
21+
removal: .move(edge: .trailing).combined(with: .opacity)
22+
))
2223
}
2324
}
24-
.frame(width: notchWidth, height: notchHeight) // Center aligns with notch
25-
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: vm.media.isPlaying || vm.timer.state == .running || vm.weather.weather != nil)
25+
.frame(width: notchWidth + 120, height: notchHeight)
26+
.animation(.spring(response: 0.35, dampingFraction: 0.8), value: pillKind)
2627
}
2728

29+
// MARK: - Active Pill
30+
2831
@ViewBuilder
29-
private var timerPill: some View {
30-
if vm.geometry.isPhysical {
31-
HStack {
32-
Spacer()
32+
private var activePill: some View {
33+
switch pillKind {
34+
case .timer:
35+
pillBody {
3336
Image(systemName: "timer")
34-
.font(.system(size: 10, weight: .medium))
37+
.font(.system(size: 10, weight: .semibold))
3538
.foregroundColor(Theme.tertiary)
36-
.padding(.trailing, 10)
37-
}
38-
.frame(width: 44, height: notchHeight)
39-
.background(Color.black)
40-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
41-
.offset(x: -notchWidth / 2 - 22 + 8)
42-
43-
HStack {
4439
Text(vm.timer.formattedTime)
45-
.font(.system(size: 10, weight: .medium).monospacedDigit())
46-
.foregroundColor(Color.white)
40+
.font(.system(size: 11, weight: .medium).monospacedDigit())
41+
.foregroundColor(.white)
4742
.lineLimit(1)
4843
.fixedSize()
49-
.padding(.leading, 8)
50-
Spacer()
51-
}
52-
.frame(width: 56, height: notchHeight)
53-
.background(Color.black)
54-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
55-
.offset(x: notchWidth / 2 + 28 - 8)
56-
} else {
57-
HStack(spacing: 8) {
58-
Image(systemName: "timer")
59-
.font(.system(size: 10, weight: .medium))
60-
.foregroundColor(Theme.tertiary)
61-
Text(vm.timer.formattedTime)
62-
.font(.system(size: 10, weight: .medium).monospacedDigit())
63-
.foregroundColor(Color.white)
6444
}
65-
.padding(.horizontal, 12)
66-
.frame(height: notchHeight)
67-
.background(Color.black)
68-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
69-
}
70-
}
71-
72-
@ViewBuilder
73-
private var mediaPill: some View {
74-
if vm.geometry.isPhysical {
75-
// Left Wing (Equalizer / Icon)
76-
HStack {
77-
Spacer() // push to right edge of the left wing
45+
case .media:
46+
pillBody {
7847
EqualizerBars(isAnimating: true)
79-
.padding(.trailing, 10)
80-
}
81-
.frame(width: 44, height: notchHeight)
82-
.background(Color.black)
83-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
84-
.offset(x: -notchWidth / 2 - 22 + 8) // Overlap slightly to merge with notch
85-
86-
// Right Wing (Timer / Source)
87-
HStack {
8848
if vm.media.track != nil {
8949
Text(formatTime(vm.media.position))
90-
.font(.system(size: 10, weight: .medium).monospacedDigit())
91-
.foregroundColor(Color.white)
92-
.padding(.leading, 10)
50+
.font(.system(size: 11, weight: .medium).monospacedDigit())
51+
.foregroundColor(.white)
52+
.lineLimit(1)
53+
.fixedSize()
9354
}
94-
Spacer()
9555
}
96-
.frame(width: 44, height: notchHeight)
97-
.background(Color.black)
98-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
99-
.offset(x: notchWidth / 2 + 22 - 8)
100-
} else {
101-
// Unified Pill for non-notched screens
102-
HStack(spacing: 8) {
103-
EqualizerBars(isAnimating: true)
104-
105-
if vm.media.track != nil {
106-
Text(formatTime(vm.media.position))
107-
.font(.system(size: 10, weight: .medium).monospacedDigit())
108-
.foregroundColor(Color.white)
56+
case .weather:
57+
if let w = vm.weather.weather {
58+
pillBody {
59+
Image(systemName: weatherSymbol(for: w.condition))
60+
.font(.system(size: 10, weight: .semibold))
61+
.foregroundColor(.white)
62+
Text(String(format: "%.0f°", w.temperature))
63+
.font(.system(size: 11, weight: .semibold))
64+
.foregroundColor(.white)
65+
.lineLimit(1)
66+
.fixedSize()
10967
}
11068
}
111-
.padding(.horizontal, 12)
112-
.frame(height: notchHeight)
113-
.background(Color.black)
114-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
69+
case .none:
70+
EmptyView()
11571
}
11672
}
117-
73+
74+
// MARK: - Shared Pill Chrome
75+
76+
private func pillBody<Content: View>(@ViewBuilder content: () -> Content) -> some View {
77+
HStack(spacing: 6) {
78+
content()
79+
}
80+
.padding(.horizontal, 10)
81+
.frame(height: notchHeight)
82+
.background(Color.black)
83+
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
84+
}
85+
86+
// MARK: - Pill Priority
87+
88+
private enum PillKind: Equatable {
89+
case timer, media, weather
90+
}
91+
92+
private var pillKind: PillKind? {
93+
if vm.timer.state == .running { return .timer }
94+
if vm.media.isPlaying { return .media }
95+
if vm.weather.weather != nil { return .weather }
96+
return nil
97+
}
98+
99+
// MARK: - Helpers
100+
118101
private func formatTime(_ seconds: Double) -> String {
119102
let mins = Int(seconds) / 60
120103
let secs = Int(seconds) % 60
121104
return String(format: "%d:%02d", mins, secs)
122105
}
123106

124-
@ViewBuilder
125-
private func weatherPill(_ weather: WeatherData) -> some View {
126-
if vm.geometry.isPhysical {
127-
// Right Wing (Weather)
128-
HStack {
129-
Text(String(format: "%.0f°", weather.temperature))
130-
.font(.system(size: 11, weight: .semibold))
131-
.foregroundColor(Color.white)
132-
.padding(.leading, 12)
133-
Spacer()
134-
}
135-
.frame(width: 44, height: notchHeight)
136-
.background(Color.black)
137-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
138-
.offset(x: notchWidth / 2 + 22 - 8)
139-
} else {
140-
// Unified Pill
141-
HStack(spacing: 4) {
142-
Image(systemName: "cloud.sun.fill")
143-
.font(.system(size: 10, weight: .medium))
144-
.foregroundColor(.white)
145-
Text(String(format: "%.0f°", weather.temperature))
146-
.font(.system(size: 11, weight: .semibold))
147-
.foregroundColor(Color.white)
148-
}
149-
.padding(.horizontal, 12)
150-
.frame(height: notchHeight)
151-
.background(Color.black)
152-
.clipShape(RoundedRectangle(cornerRadius: notchHeight / 2, style: .continuous))
107+
private func weatherSymbol(for code: Int) -> String {
108+
switch code {
109+
case 0: return "sun.max.fill"
110+
case 1, 2: return "cloud.sun.fill"
111+
case 3: return "cloud.fill"
112+
case 45, 48: return "cloud.fog.fill"
113+
case 51...57: return "cloud.drizzle.fill"
114+
case 61...67: return "cloud.rain.fill"
115+
case 71...77: return "cloud.snow.fill"
116+
case 80...82: return "cloud.heavyrain.fill"
117+
case 85, 86: return "cloud.snow.fill"
118+
case 95, 96, 99: return "cloud.bolt.fill"
119+
default: return "cloud.sun.fill"
153120
}
154121
}
155122
}

0 commit comments

Comments
 (0)