Skip to content

Conversation

@Hirobreak
Copy link
Contributor

Bugfix

  • Add a bottom inset to the contents displayed on the tab so the mini player does not block any component behind it
  • Bug known for iPad and MacOS versions of the app

Related tasks

  • Links to related tasks being addressed in this PR

Approach

  • Calculate the height of the space the mini player occupies and use its value to add an inset to the tabs

Things to be aware of / Things to focus on

  • This changes are applied to any device, not just iPad and MacOS

Screenshots

Screenshot 2026-02-04 at 5 32 22 PM

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a bug where the mini player was blocking content on iPad and MacOS by dynamically calculating the mini player's height and using it to add appropriate bottom insets to tab contents.

Changes:

  • Added dynamic height calculation for the mini player using GeometryReader and PreferenceKey
  • Changed mini player positioning from safeAreaInset to overlay with environment-based inset propagation
  • Added unused playerHeight and playerHeightBinding properties to PlayerState

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
BookPlayer/Utils/PlayerState.swift Adds playerHeight property and binding (unused)
BookPlayer/Utils/Extensions/View+BookPlayer.swift Refactors mini player positioning to use overlay with dynamic height calculation and environment-based inset propagation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 242 to 253
.overlay(alignment: .bottom) {
regular()
.fixedSize(horizontal: false, vertical: true)
.overlay(
GeometryReader { geo in
Color.clear.preference(
key: MiniPlayerBottomInsetPreferenceKey.self,
value: geo.size.height
)
}
)
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from safeAreaInset(edge: .bottom, spacing: 0, content: regular) to overlay(alignment: .bottom) fundamentally changes how the mini player is positioned. With the old approach, the mini player was placed in the safe area which automatically pushed content up. With the new approach, the mini player is overlaid on top of content, and you're relying on MiniPlayerSafeAreaInsetModifier to separately push content up via the miniPlayerBottomInset environment value.

This approach is more fragile because:

  1. It creates a dependency between two separate modifiers that must stay in sync
  2. Views that don't use .miniPlayerSafeAreaInset() will have content obscured by the overlay
  3. There could be timing issues if the preference change fires after layout

If this change is necessary to work around a platform-specific issue with safeAreaInset on iPad/MacOS, consider adding a comment explaining why the overlay approach is needed and what the tradeoffs are.

Copilot uses AI. Check for mistakes.
Comment on lines 254 to 257
.onPreferenceChange(MiniPlayerBottomInsetPreferenceKey.self) {
let topPadding: CGFloat = 20
let reduceSize = hSize == .compact ? 44.0 : 0
let reduceTopPadding = $0 > reduceSize ? reduceSize : 0
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The calculation logic here contains magic numbers (20, 44.0) and unclear variable names that make it difficult to understand the intent. Consider adding comments explaining:

  1. Why topPadding is 20
  2. Why reduceSize is 44.0 for compact size classes
  3. What the overall formula is trying to achieve (i.e., why we add topPadding then subtract reduceTopPadding)

Additionally, the condition $0 > reduceSize ? reduceSize : 0 means reduceTopPadding is either 44 or 0, which seems like it should be documented.

Suggested change
.onPreferenceChange(MiniPlayerBottomInsetPreferenceKey.self) {
let topPadding: CGFloat = 20
let reduceSize = hSize == .compact ? 44.0 : 0
let reduceTopPadding = $0 > reduceSize ? reduceSize : 0
.onPreferenceChange(MiniPlayerBottomInsetPreferenceKey.self) {
// Extra spacing between the mini player and the safe area / tab bar.
// This value was chosen to match the visual design of the mini player.
let topPadding: CGFloat = 20
// Amount to reduce the effective bottom inset when the horizontal size class is compact.
// 44.0 corresponds to the standard tab bar height in compact environments; in regular
// size classes we do not need this reduction.
let reduceSize = hSize == .compact ? 44.0 : 0
// When the mini player height exceeds the reduction threshold, subtract the full
// reduction amount (44 or 0). This ensures the mini player does not "double count"
// the tab bar height and keeps the layout visually aligned.
let reduceTopPadding = $0 > reduceSize ? reduceSize : 0
// Final inset = measured mini player height + design padding - any compact-size reduction.

Copilot uses AI. Check for mistakes.
Comment on lines 228 to 236
if #available(iOS 26.1, *) {
defaultBody(content)
} else if #available(iOS 26.0, *) {
content
.tabBarMinimizeBehavior(.onScrollDown)
.tabViewBottomAccessory(content: accessory)
} else {
defaultBody(content)
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation detected. The if/else if/else branches should align at the same indentation level (2 spaces from the function body), but lines 228, 231, and 232-233 use extra indentation. This deviates from the codebase's consistent 2-space indentation style.

Copilot uses AI. Check for mistakes.
Comment on lines 241 to 261
content
.safeAreaInset(edge: .bottom, spacing: 0, content: regular)
}
.overlay(alignment: .bottom) {
regular()
.fixedSize(horizontal: false, vertical: true)
.overlay(
GeometryReader { geo in
Color.clear.preference(
key: MiniPlayerBottomInsetPreferenceKey.self,
value: geo.size.height
)
}
)
}
.onPreferenceChange(MiniPlayerBottomInsetPreferenceKey.self) {
let topPadding: CGFloat = 20
let reduceSize = hSize == .compact ? 44.0 : 0
let reduceTopPadding = $0 > reduceSize ? reduceSize : 0
miniPlayerBottomInset = $0 + topPadding - reduceTopPadding
}
.environment(\.miniPlayerBottomInset, miniPlayerBottomInset)
}
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation detected. The content inside this function should follow the codebase's 2-space indentation style, but line 241 starts with extra indentation. Lines 242-260 continue with inconsistent indentation that doesn't match the codebase convention.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 255 to 259
let miniPlayerHeight = $0.height
let topPadding: CGFloat = 20
let reduceSize = hSize == .compact ? 44.0 : 0
let reduceTopPadding = miniPlayerHeight > reduceSize ? reduceSize : 0
miniPlayerBottomInset = miniPlayerHeight + topPadding - reduceTopPadding
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topPadding (20), reduceSize (44), and the derived reduceTopPadding introduce magic-number layout behavior that’s hard to reason about and keep consistent with MiniPlayerView padding/tab bar sizing. Consider deriving these values from existing layout constants/safe-area metrics (or centralizing them as named constants with a brief rationale) so the inset logic remains correct across devices.

Copilot uses AI. Check for mistakes.
Comment on lines 200 to 204
extension EnvironmentValues {
var miniPlayerBottomInset: CGFloat {
get { self[MiniPlayerBottomInsetKey.self] }
set { self[MiniPlayerBottomInsetKey.self] = newValue }
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly added EnvironmentValues key uses 4-space indentation, while the rest of this file consistently uses 2-space indentation. Please reformat this block to match the established style for readability and to reduce noisy diffs going forward.

Copilot uses AI. Check for mistakes.
Comment on lines 247 to 250
Color.clear.preference(
key: MiniPlayerSizePreferenceKey.self,
value: geo.size
)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GeometryReader/Color.clear overlay used to emit the mini-player size is hit-testable by default and can end up intercepting taps meant for the mini player’s buttons/gestures. Make the measuring overlay non-interactive (e.g., disable hit testing on the overlay) or move the measurement into a background that won’t block interaction.

Suggested change
Color.clear.preference(
key: MiniPlayerSizePreferenceKey.self,
value: geo.size
)
Color.clear
.allowsHitTesting(false)
..preference(
key: MiniPlayerSizePreferenceKey.self,
value: geo.size
)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

key: MiniPlayerSizePreferenceKey.self,
value: geo.size
)
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GeometryReader/Color.clear overlay used for size measurement will sit on top of regular() and can intercept taps/gestures, which may break mini player interactions. Ensure the measuring overlay does not participate in hit-testing (e.g., disable hit testing on the overlay view).

Suggested change
}
}
.allowsHitTesting(false)

Copilot uses AI. Check for mistakes.
Comment on lines +196 to +221
private struct MiniPlayerBottomInsetKey: EnvironmentKey {
static let defaultValue: CGFloat = 80
}

extension EnvironmentValues {
var miniPlayerBottomInset: CGFloat {
get { self[MiniPlayerBottomInsetKey.self] }
set { self[MiniPlayerBottomInsetKey.self] = newValue }
}
}

// MARK: - Toolbar utils
struct MiniPlayerSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}

struct MiniPlayerModifier<Regular: View, Accessory: View>: ViewModifier {
@ViewBuilder let regular: () -> Regular
@ViewBuilder let accessory: () -> Accessory


@Environment(\.horizontalSizeClass) var hSize
@State private var miniPlayerBottomInset: CGFloat = 80

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bottom inset defaults are duplicated as hard-coded 80 values (MiniPlayerBottomInsetKey.defaultValue and MiniPlayerModifier.miniPlayerBottomInset initial state), and additional constants like topPadding = 20 are unexplained. Extract these into a single named constant (or derive from safe-area/tab bar metrics) to avoid drift and make the intent clearer.

Copilot uses AI. Check for mistakes.
@GianniCarlo GianniCarlo merged commit 64f9d66 into TortugaPower:develop Feb 7, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants