Three changes to DynamicIslandView:

•	Top spacer 54 → 12 — pulls the pill up to sit directly under the
Dynamic Island cutout
	•	Horizontal padding 32 → 56 — narrows the pill from both sides
	•	Height 48 → 44 — slightly more compact to match the island’s
proportions
If it’s sitting too high or too low after testing, the spacer value is
the one knob to turn — increase it if it overlaps the island, decrease
if there’s too much gap.
This commit is contained in:
Dallas Groot 2026-04-11 17:53:59 -07:00
parent f19d21e4cc
commit 8eaab0bc93

View file

@ -22,7 +22,7 @@ struct MainTabView: View {
@State private var debugDragOffset: CGFloat = 0
@State private var showFullConsole = false
// Group toggles -- same @AppStorage keys as DebugConsoleView so state is shared
// Group toggles same @AppStorage keys as DebugConsoleView so state is shared
@AppStorage("dbg_show_iphone") private var dbgShowIPhone = true
@AppStorage("dbg_show_watch") private var dbgShowWatch = true
@AppStorage("dbg_show_companion") private var dbgShowCompanion = true
@ -91,7 +91,7 @@ struct MainTabView: View {
}
}
// Debug panel (docked) -- only when enabled and NOT in PiP mode
// Debug panel (docked) only when enabled and NOT in PiP mode
if debugLogger.isEnabled && !debugPipMode {
debugPanel
.transition(.move(edge: .bottom).combined(with: .opacity))
@ -140,7 +140,7 @@ struct MainTabView: View {
dragOffset = 0
}
} else if value.translation.height < -20 {
// Small upward swipe -- open NowPlaying
// Small upward swipe open NowPlaying
dragOffset = 0
withAnimation(.easeInOut(duration: 0.35)) {
showNowPlaying = true
@ -157,7 +157,7 @@ struct MainTabView: View {
}
}
// Now Playing overlay -- always rendered, positioned via offset
// Now Playing overlay always rendered, positioned via offset
// This avoids re-layout animation when appearing
if audioPlayer.currentSong != nil {
NowPlayingView(isPresented: $showNowPlaying)
@ -166,7 +166,7 @@ struct MainTabView: View {
.allowsHitTesting(showNowPlaying)
}
// Debug PiP (floating) -- above everything except Now Playing
// Debug PiP (floating) above everything except Now Playing
if debugLogger.isEnabled && debugPipMode {
debugPipView
.zIndex(showNowPlaying ? 0 : 5)
@ -268,7 +268,7 @@ struct MainTabView: View {
Divider().background(Color.white.opacity(0.15))
// Log entries -- filtered by group toggles
// Log entries filtered by group toggles
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 1) {
@ -404,7 +404,7 @@ struct MainTabView: View {
)
return VStack(spacing: 0) {
// PiP header -- always visible
// PiP header always visible
HStack(spacing: 8) {
Image(systemName: "ladybug.fill")
.font(.system(size: 10))
@ -476,9 +476,9 @@ struct MainTabView: View {
}
)
// Log content -- hidden when collapsed
// Log content hidden when collapsed
if !pipCollapsed {
// Group toggles -- same state as docked panel
// Group toggles same state as docked panel
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 6) {
dbgGroupToggle("iPhone", icon: "iphone", isOn: $dbgShowIPhone)
@ -574,7 +574,7 @@ struct MiniPlayerBar: View {
var body: some View {
let _ = DebugLogger.shared.trackViewBodies ? ViewBodyTracker.shared.record("MiniPlayerBar") : false
VStack(spacing: 0) {
// Progress bar extracted into its own view -- only it re-evaluates at 10Hz
// Progress bar extracted into its own view only it re-evaluates at 10Hz
// when audioPlayer.currentTime changes. The rest of MiniPlayerBar body
// only re-evaluates on song change, play/pause, or color change.
MiniProgressBar(
@ -584,7 +584,7 @@ struct MiniPlayerBar: View {
accentPink: accentPink
)
ZStack(alignment: .center) {
// Visualizer behind controls -- paused when full NowPlaying is open
// Visualizer behind controls paused when full NowPlaying is open
if VisualizerSettings.shared.enabled && VisualizerSettings.shared.miniPlayerEnabled && !showNowPlaying {
CompactVisualizerView(
isPlaying: audioPlayer.isPlaying,
@ -694,9 +694,9 @@ struct DynamicIslandView: View {
var body: some View {
VStack(spacing: 0) {
Spacer().frame(height: 54) // Below status bar + notch
Spacer().frame(height: 12) // Sit directly below Dynamic Island
// The pill -- only this is tappable for NowPlaying
// The pill only this is tappable for NowPlaying
HStack(spacing: 0) {
Group {
if let song = audioPlayer.currentSong {
@ -744,9 +744,9 @@ struct DynamicIslandView: View {
Spacer().frame(width: 10)
}
.frame(height: 48)
.frame(height: 44)
.background(
RoundedRectangle(cornerRadius: 26, style: .continuous)
RoundedRectangle(cornerRadius: 22, style: .continuous)
.fill(
LinearGradient(
colors: [Color.black, themeColor.opacity(0.25)],
@ -754,10 +754,10 @@ struct DynamicIslandView: View {
endPoint: .trailing
)
)
.shadow(color: themeColor.opacity(0.3), radius: 12, y: 2)
.shadow(color: themeColor.opacity(0.4), radius: 8, y: 2)
)
.padding(.horizontal, 32)
.contentShape(RoundedRectangle(cornerRadius: 26))
.padding(.horizontal, 56)
.contentShape(RoundedRectangle(cornerRadius: 22))
.onTapGesture {
withAnimation(.easeInOut(duration: 0.35)) {
showNowPlaying = true
@ -777,7 +777,7 @@ struct DynamicIslandView: View {
Spacer()
}
// Status bar stays visible -- no .statusBarHidden
// Status bar stays visible no .statusBarHidden
}
}
@ -785,17 +785,17 @@ struct DynamicIslandView: View {
//
// Extracted from MiniPlayerBar so only this tiny view re-evaluates when
// audioPlayer.currentTime changes (10x/second from the periodic time observer).
// Before this, the ENTIRE MiniPlayerBar body re-evaluated at 10Hz -- including
// the GeometryReader, CompactVisualizerView, all ZStack children, and album art --
// Before this, the ENTIRE MiniPlayerBar body re-evaluated at 10Hz including
// the GeometryReader, CompactVisualizerView, all ZStack children, and album art
// causing sustained ~128% CPU even with the visualizer off.
//
// The key trick: this view declares its OWN @ObservedObject on audioPlayer so
// SwiftUI's dependency tracking scopes the 10Hz invalidation to THIS view only.
// MiniPlayerBar.body is now only re-evaluated on song change, play/pause, or
// color change -- not on every currentTime tick.
// color change not on every currentTime tick.
private struct MiniProgressBar: View {
// No @ObservedObject on AudioPlayer -- currentTime is no longer @Published.
// No @ObservedObject on AudioPlayer currentTime is no longer @Published.
// TimelineView drives re-evaluation at 10Hz independently so this view never
// triggers objectWillChange on AudioPlayer or propagates updates to siblings.
@ObservedObject var colorExtractor: AlbumColorExtractor
@ -810,18 +810,18 @@ private struct MiniProgressBar: View {
var body: some View {
let _ = DebugLogger.shared.trackViewBodies ? ViewBodyTracker.shared.record("MiniProgressBar") : false
// GeometryReader and gesture live OUTSIDE TimelineView.
// Previously the gesture was inside the TimelineView closure -- every 0.1s
// Previously the gesture was inside the TimelineView closure every 0.1s
// TimelineView re-evaluated its content, reconstructing the DragGesture
// and interrupting in-progress touches, causing missed/dropped scrubs.
// Only the fill Rectangle that reads currentTime sits inside TimelineView.
GeometryReader { geo in
ZStack(alignment: .leading) {
// Track background -- static, no TimelineView needed
// Track background static, no TimelineView needed
Rectangle()
.fill(Color.white.opacity(0.1))
.frame(height: isScrubbing ? 8 : 3)
// Fill -- inside TimelineView so it updates at 10Hz without
// Fill inside TimelineView so it updates at 10Hz without
// disturbing the gesture recognizer above
TimelineView(.periodic(from: .now, by: 0.1)) { _ in
let player = AudioPlayer.shared
@ -836,7 +836,7 @@ private struct MiniProgressBar: View {
height: isScrubbing ? 8 : 3)
}
// Scrub thumb -- only visible while dragging
// Scrub thumb only visible while dragging
if isScrubbing {
TimelineView(.periodic(from: .now, by: 0.1)) { _ in
let player = AudioPlayer.shared
@ -854,7 +854,7 @@ private struct MiniProgressBar: View {
}
.frame(maxHeight: .infinity)
.contentShape(Rectangle())
// Gesture is stable -- never reconstructed by TimelineView ticks
// Gesture is stable never reconstructed by TimelineView ticks
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
@ -884,7 +884,7 @@ func dismissKeyboard() {
}
/// UIKit tap recognizer that dismisses keyboard without blocking other touches.
/// Added once to the key window -- fires alongside all gestures.
/// Added once to the key window fires alongside all gestures.
private class KeyboardDismissTapRecognizer: UITapGestureRecognizer, UIGestureRecognizerDelegate {
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
@ -902,7 +902,7 @@ private class KeyboardDismissTapRecognizer: UITapGestureRecognizer, UIGestureRec
}
}
/// Helper target for the tap gesture -- calls endEditing on the window
/// Helper target for the tap gesture calls endEditing on the window
private class KeyboardDismissTarget: NSObject {
@objc func dismiss() {
dismissKeyboard()