Turn document ยท 2026-05-25 05:28 EDT

Fix Native Player Controls Tap-to-Show

Native player controls can now be brought back after they auto-hide or are hidden by tapping the player. The fix gives player taps a reliable full-screen gesture surface above the VLC video view while keeping visible controls interactive.

Issue: dreamio-wgk File: Dreamio/NativePlayerViewController.swift Validation: Xcode build passed

Summary

Fixed the native playback overlay so hidden controls are not effectively gone forever. A transparent tap surface now receives taps over the video, and hidden control views stop intercepting touches until they are visible again.

Changes Made

Context

The native player uses MobileVLCKit for video rendering and an overlay built in UIKit for playback controls. Before this change, the gesture recognizer was attached to the root view. Once controls faded out, the visible controls had alpha zero but still occupied their layout area, and the video drawable could also interfere with root-level tap handling. That left some taps with no route back to revealControls().

Important Implementation Details

The tap surface is inserted immediately after backend.view, which keeps it above the video but below the actual controls. This preserves normal button and slider behavior when controls are visible while making the rest of the player a reliable tap target.

When hideControls() runs, the controls and close button are also made non-interactive. This matters because alpha-zero UIKit views can still participate in hit testing unless interaction is disabled or the views are hidden.

Relevant Diff Snippets

The diff below is rendered with @pierre/diffs/ssr using preloadPatchDiff.

Dreamio/NativePlayerViewController.swift
-1+18
35 unmodified lines
36
37
38
39
40
41
125 unmodified lines
167
168
169
170
171
172
9 unmodified lines
182
183
184
185
186
187
188
19 unmodified lines
208
209
210
211
212
213
124 unmodified lines
338
339
340
341
342
343
2 unmodified lines
346
347
348
349
350
351
35 unmodified lines
return view
}()
private let playPauseButton = NativePlayerViewController.iconButton(systemName: "pause.fill", label: "Play or Pause")
private let jumpBackButton = NativePlayerViewController.iconButton(systemName: "gobackward.15", label: "Jump Back 15 Seconds")
private let jumpForwardButton = NativePlayerViewController.iconButton(systemName: "goforward.15", label: "Jump Forward 15 Seconds")
125 unmodified lines
private func configureLayout() {
view.addSubview(backend.view)
view.addSubview(loadingView)
view.addSubview(failureLabel)
view.addSubview(controlsContainer)
9 unmodified lines
let tap = UITapGestureRecognizer(target: self, action: #selector(toggleControlsVisibility))
tap.cancelsTouchesInView = false
view.addGestureRecognizer(tap)
let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
controlRow.translatesAutoresizingMaskIntoConstraints = false
19 unmodified lines
backend.view.topAnchor.constraint(equalTo: view.topAnchor),
backend.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
124 unmodified lines
}
private func revealControls() {
UIView.animate(withDuration: 0.18) {
self.controlsContainer.alpha = 1
self.closeButton.alpha = 1
2 unmodified lines
}
private func hideControls() {
UIView.animate(withDuration: 0.24) {
self.controlsContainer.alpha = 0
self.closeButton.alpha = 0
35 unmodified lines
36
37
38
39
40
41
42
43
44
45
46
47
48
125 unmodified lines
174
175
176
177
178
179
180
9 unmodified lines
190
191
192
193
194
195
196
19 unmodified lines
216
217
218
219
220
221
222
223
224
225
226
124 unmodified lines
351
352
353
354
355
356
357
358
2 unmodified lines
361
362
363
364
365
366
367
368
35 unmodified lines
return view
}()
private let tapSurfaceView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
return view
}()
private let playPauseButton = NativePlayerViewController.iconButton(systemName: "pause.fill", label: "Play or Pause")
private let jumpBackButton = NativePlayerViewController.iconButton(systemName: "gobackward.15", label: "Jump Back 15 Seconds")
private let jumpForwardButton = NativePlayerViewController.iconButton(systemName: "goforward.15", label: "Jump Forward 15 Seconds")
125 unmodified lines
private func configureLayout() {
view.addSubview(backend.view)
view.addSubview(tapSurfaceView)
view.addSubview(loadingView)
view.addSubview(failureLabel)
view.addSubview(controlsContainer)
9 unmodified lines
let tap = UITapGestureRecognizer(target: self, action: #selector(toggleControlsVisibility))
tap.cancelsTouchesInView = false
tapSurfaceView.addGestureRecognizer(tap)
let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
controlRow.translatesAutoresizingMaskIntoConstraints = false
19 unmodified lines
backend.view.topAnchor.constraint(equalTo: view.topAnchor),
backend.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
tapSurfaceView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tapSurfaceView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tapSurfaceView.topAnchor.constraint(equalTo: view.topAnchor),
tapSurfaceView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
124 unmodified lines
}
private func revealControls() {
controlsContainer.isUserInteractionEnabled = true
closeButton.isUserInteractionEnabled = true
UIView.animate(withDuration: 0.18) {
self.controlsContainer.alpha = 1
self.closeButton.alpha = 1
2 unmodified lines
}
private func hideControls() {
controlsContainer.isUserInteractionEnabled = false
closeButton.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.24) {
self.controlsContainer.alpha = 0
self.closeButton.alpha = 0

Expected Impact for End-Users

Users should be able to tap the native player to hide controls and tap the video again to bring them back. Auto-hidden controls should behave the same way, so playback is no longer trapped in a controls-hidden state.

Validation

Passed: xcodebuild build -workspace Dreamio.xcworkspace -scheme Dreamio -destination 'generic/platform=iOS'

The build succeeded for the Dreamio scheme against a generic iOS destination. Manual on-device interaction was not run in this turn, so the remaining risk is limited to real touch behavior across physical device sizes.

Issues, Limitations, and Mitigations

Follow-up Work