@@ -178,7 +195,7 @@ Every gear change waits for confirmation from the trainer.
- **ERG workouts are not supported.** ERG mode controls target power, while
Virtual Gears controls how hard a gear feels.
- **The riding app cannot display the selected gear.** Bluetooth FTMS has no
- message for reporting it, so the gear is shown on the iPhone.
+ message for reporting it, so the gear is shown on your iPhone or iPad.
- **This is not Zwift's native virtual shifting.** It works independently of the
riding app. Virtual Gears supplies and displays the gears itself.
- **Virtual Gears cannot read the trainer's current wheel circumference.** It
@@ -188,8 +205,8 @@ Every gear change waits for confirmation from the trainer.
custom value in the Wahoo app, enter the same value in Virtual Gears before
shifting.
- **A riding app on Windows may not reconnect on its own after a Bluetooth
- drop.** If the wireless link between your phone and the computer times out
- mid-ride, some Windows riding apps do not scan for the phone again by
+ drop.** If the wireless link between your Apple device and the computer times
+ out mid-ride, some Windows riding apps do not scan for the device again by
themselves; you may need to restart the riding app to see Virtual Gears once
more. This is the riding app's own reconnect behaviour, not something Virtual
Gears controls.
diff --git a/VirtualGears.xcodeproj/project.pbxproj b/VirtualGears.xcodeproj/project.pbxproj
index 44dd9ac..abd4221 100644
--- a/VirtualGears.xcodeproj/project.pbxproj
+++ b/VirtualGears.xcodeproj/project.pbxproj
@@ -260,7 +260,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_VERSION = 6.0;
- TARGETED_DEVICE_FAMILY = 1;
+ TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = VirtualGears;
};
name = Debug;
@@ -276,7 +276,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_VERSION = 6.0;
- TARGETED_DEVICE_FAMILY = 1;
+ TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = VirtualGears;
};
name = Release;
@@ -320,7 +320,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 18;
+ CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = MNW6SJT4V7;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = VirtualGearsProduct/Info.plist;
@@ -336,7 +336,7 @@
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
- TARGETED_DEVICE_FAMILY = 1;
+ TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
@@ -345,7 +345,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 18;
+ CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_TEAM = MNW6SJT4V7;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = VirtualGearsProduct/Info.plist;
@@ -361,7 +361,7 @@
SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 6.0;
- TARGETED_DEVICE_FAMILY = 1;
+ TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
diff --git a/VirtualGearsProduct/ClickCentralService.swift b/VirtualGearsProduct/ClickCentralService.swift
index 6a47e41..59fc6ff 100644
--- a/VirtualGearsProduct/ClickCentralService.swift
+++ b/VirtualGearsProduct/ClickCentralService.swift
@@ -567,6 +567,10 @@ extension ClickCentralService {
func stageScreenshotPressedButton(_ button: ZwiftClickButton) {
pressedButton = button
}
+
+ func stageDiscoveredCandidate(_ candidate: BluetoothCandidate) {
+ candidates = [candidate]
+ }
}
#endif
diff --git a/VirtualGearsProduct/Info.plist b/VirtualGearsProduct/Info.plist
index 223fa86..434fb9d 100644
--- a/VirtualGearsProduct/Info.plist
+++ b/VirtualGearsProduct/Info.plist
@@ -46,5 +46,12 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
diff --git a/VirtualGearsProduct/SetupView.swift b/VirtualGearsProduct/SetupView.swift
index 386ae04..32e1e35 100644
--- a/VirtualGearsProduct/SetupView.swift
+++ b/VirtualGearsProduct/SetupView.swift
@@ -1,3 +1,4 @@
+import Combine
import SwiftUI
import VirtualGearsCore
@@ -302,6 +303,7 @@ private struct NormalWheelSizeView: View {
@Bindable var store: ConfigurationStore
@State private var enteredValue: String
@State private var isApplyingDefault = false
+ @FocusState private var wheelSizeFieldIsFocused: Bool
init(store: ConfigurationStore) {
self.store = store
@@ -368,6 +370,7 @@ private struct NormalWheelSizeView: View {
Section {
TextField("Millimetres", text: $enteredValue)
.keyboardType(.numberPad)
+ .focused($wheelSizeFieldIsFocused)
.onChange(of: enteredValue) { _, value in
if isApplyingDefault {
isApplyingDefault = false
@@ -425,6 +428,15 @@ private struct NormalWheelSizeView: View {
}
.navigationTitle("Wheel circumference")
.navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItemGroup(placement: .keyboard) {
+ Spacer()
+ Button("Done") {
+ wheelSizeFieldIsFocused = false
+ }
+ .accessibilityIdentifier("wheel.dismissKeyboard")
+ }
+ }
}
private var defaultMillimeters: Int {
@@ -635,6 +647,16 @@ private struct ShiftingSetupView: View {
if store.configuration.usesClick {
click.autoConnectSavedDevice()
}
+#if DEBUG
+ if ScreenshotFixture.current == .settingsClickSingleCandidate {
+ try? await Task.sleep(for: .seconds(1))
+ guard !Task.isCancelled else { return }
+ click.stageDiscoveredCandidate(BluetoothCandidate(
+ id: ScreenshotFixture.clickID,
+ name: "Zwift Click"
+ ))
+ }
+#endif
}
.onChange(of: click.latestButtonEvent) { _, event in
guard click.identificationCandidateID != nil, let event else { return }
@@ -1670,8 +1692,14 @@ private struct DeviceDiscoverySection: View {
let select: (BluetoothCandidate) -> Void
@State private var discovery = DeviceDiscoveryState()
+ @State private var searchDeadline: Date?
private let searchDuration = DeviceDiscoveryPolicy.searchDuration
+ private let searchTicks = Timer.publish(
+ every: 0.25,
+ on: .main,
+ in: .common
+ ).autoconnect()
init(
deviceName: String,
@@ -1805,17 +1833,16 @@ private struct DeviceDiscoverySection: View {
beginSearch()
}
}
- .task(id: DiscoveryClock(
+ .onChange(of: DiscoveryClock(
scanGeneration: scanGeneration,
isScanning: isScanning
- )) {
- guard isScanning else { return }
- do {
- try await Task.sleep(for: searchDuration)
- } catch {
- return
- }
- guard !Task.isCancelled, isScanning else { return }
+ ), initial: true) { _, clock in
+ updateSearchClock(clock)
+ }
+ .onReceive(searchTicks) { now in
+ guard let searchDeadline, now >= searchDeadline,
+ isScanning else { return }
+ self.searchDeadline = nil
finishSearch()
}
.onChange(of: candidates.count) { _, count in
@@ -1830,6 +1857,7 @@ private struct DeviceDiscoverySection: View {
}
}
.onDisappear {
+ searchDeadline = nil
if discovery.phase != .idle || isScanning {
cancelScanning()
}
@@ -1864,6 +1892,14 @@ private struct DeviceDiscoverySection: View {
}
}
+ private func updateSearchClock(_ clock: DiscoveryClock) {
+ searchDeadline = clock.isScanning
+ ? Date.now.addingTimeInterval(
+ TimeInterval(searchDuration.components.seconds)
+ )
+ : nil
+ }
+
private func finishSearch() {
stopScanning()
if candidates.count == 1, let candidate = candidates.first,
diff --git a/VirtualGearsProduct/VirtualGearsApp.swift b/VirtualGearsProduct/VirtualGearsApp.swift
index 4c4c6b4..ae6186e 100644
--- a/VirtualGearsProduct/VirtualGearsApp.swift
+++ b/VirtualGearsProduct/VirtualGearsApp.swift
@@ -329,13 +329,9 @@ private struct ScreenshotFixtureView: View {
name: "Zwift Click"
),
]
- let clickCandidates = scenario == .settingsClickSingleCandidate
- ? [BluetoothCandidate(
- id: ScreenshotFixture.clickID,
- name: "Zwift Click"
- )]
- : (scenario == .settingsClickDuplicates
- || scenario == .settingsClickIdentifying ? duplicateClicks : [])
+ let clickCandidates =
+ scenario == .settingsClickDuplicates
+ || scenario == .settingsClickIdentifying ? duplicateClicks : []
click.stageScreenshot(
name: configuration.clickName,
batteryLevel: scenario == .settingsClickLowBattery ? 15 : 82,
diff --git a/VirtualGearsProduct/VirtualGearsHomeView.swift b/VirtualGearsProduct/VirtualGearsHomeView.swift
index bac5e8e..0d1f297 100644
--- a/VirtualGearsProduct/VirtualGearsHomeView.swift
+++ b/VirtualGearsProduct/VirtualGearsHomeView.swift
@@ -1171,6 +1171,8 @@ struct ShiftingView: View {
}
.padding(.horizontal, landscape ? 18 : 14)
.padding(.bottom, 4)
+ .frame(maxWidth: 1_100, maxHeight: .infinity)
+ .frame(maxWidth: .infinity)
}
.navigationTitle(configuration.drivetrainName)
.navigationBarTitleDisplayMode(.inline)
@@ -1368,7 +1370,8 @@ struct ShiftingView: View {
gearHero(
fontSize: min(
geometry.size.width * 0.61,
- geometry.size.height * 0.38
+ geometry.size.height * 0.38,
+ 320
)
)
HStack(spacing: 12) {
@@ -1378,7 +1381,8 @@ struct ShiftingView: View {
.frame(
height: min(
max(160, geometry.size.height * 0.32),
- geometry.size.height * 0.36
+ geometry.size.height * 0.36,
+ 300
)
)
.accessibilityElement(children: .contain)
@@ -1387,14 +1391,15 @@ struct ShiftingView: View {
}
private func landscapeControls(_ geometry: GeometryProxy) -> some View {
- let buttonWidth = max(150, geometry.size.width * 0.28)
+ let buttonWidth = min(320, max(150, geometry.size.width * 0.28))
return HStack(spacing: 12) {
shiftButton(easier: true)
.frame(width: buttonWidth)
gearHero(
fontSize: min(
geometry.size.width * 0.18,
- geometry.size.height * 0.46
+ geometry.size.height * 0.46,
+ 300
)
)
shiftButton(easier: false)
diff --git a/VirtualGearsUITests/VirtualGearsUITests.swift b/VirtualGearsUITests/VirtualGearsUITests.swift
index db2902a..5554fb1 100644
--- a/VirtualGearsUITests/VirtualGearsUITests.swift
+++ b/VirtualGearsUITests/VirtualGearsUITests.swift
@@ -184,6 +184,11 @@ final class VirtualGearsUITests: XCTestCase {
assertVisible("screen.settings")
app.staticTexts["Zwift Click"].firstMatch.tap()
+ XCTAssertTrue(
+ app.staticTexts["Found one. Checking for others…"]
+ .waitForExistence(timeout: 4),
+ "The fixture did not discover its sole Click after scanning began"
+ )
XCTAssertTrue(
app.staticTexts["Your Click"].waitForExistence(timeout: 12),
"A sole Click was not selected when the discovery window ended"
@@ -353,6 +358,60 @@ final class VirtualGearsUITests: XCTestCase {
XCTAssertTrue(app.buttons["Stop Shifting"].exists)
}
+ func testIPadReadyAndRideFlowsRemainUsableAcrossOrientations() throws {
+ guard UIDevice.current.userInterfaceIdiom == .pad else {
+ throw XCTSkip("iPad layout coverage")
+ }
+
+ launch("-shotReady")
+ assertVisible("screen.startup")
+ assertVisibleElement(app.buttons["Start Shifting"])
+ app.buttons["Settings"].tap()
+ assertVisible("screen.settings")
+ assertVisibleElement(app.buttons["Done"])
+ app.buttons["Done"].tap()
+
+ launch("-shotRide", orientation: .landscapeLeft)
+ waitForLandscapeLayout()
+ let window = app.windows.firstMatch.frame
+ let easier = app.buttons["Shift easier"]
+ let harder = app.buttons["Shift harder"]
+ assertVisibleElement(easier)
+ assertVisibleElement(harder)
+ XCTAssertLessThanOrEqual(
+ easier.frame.width,
+ 320,
+ "The easier control grows beyond its deliberate iPad width."
+ )
+ XCTAssertLessThanOrEqual(
+ harder.frame.width,
+ 320,
+ "The harder control grows beyond its deliberate iPad width."
+ )
+ XCTAssertLessThan(easier.frame.maxX, harder.frame.minX)
+ XCTAssertTrue(window.contains(easier.frame))
+ XCTAssertTrue(window.contains(harder.frame))
+
+ XCUIDevice.shared.orientation = .portraitUpsideDown
+ let deadline = Date().addingTimeInterval(3)
+ while Date() < deadline,
+ app.windows.firstMatch.frame.width > app.windows.firstMatch.frame.height {
+ RunLoop.current.run(until: Date().addingTimeInterval(0.05))
+ }
+ XCTAssertEqual(
+ XCUIDevice.shared.orientation,
+ .portraitUpsideDown,
+ "The simulated iPad did not enter upside-down portrait."
+ )
+ XCTAssertGreaterThan(
+ app.windows.firstMatch.frame.height,
+ app.windows.firstMatch.frame.width,
+ "The iPad did not rotate to upside-down portrait."
+ )
+ assertVisibleElement(app.buttons["Shift easier"])
+ assertVisibleElement(app.buttons["Shift harder"])
+ }
+
func testStoppingRequiresConfirmationBeforeRideControlsDisappear() {
launch("-shotRide")
@@ -489,6 +548,9 @@ final class VirtualGearsUITests: XCTestCase {
XCTAssertFalse(
app.buttons["MTB, 29×2.25, 2326 millimetres"].isSelected
)
+ let dismissKeyboard = app.buttons["wheel.dismissKeyboard"]
+ assertVisibleElement(dismissKeyboard)
+ dismissKeyboard.tap()
let useDefault = app.buttons["wheel.useDefault"]
XCTAssertTrue(useDefault.isEnabled)
@@ -857,8 +919,7 @@ final class VirtualGearsUITests: XCTestCase {
app.buttons["Fan"].tap()
assertVisible("screen.demo-headwind")
- app.buttons["Manual"].tap()
- assertVisibleElement(app.buttons["50 percent"])
+ selectDemoManualFanControl()
}
func testDemoShiftButtonsAreDrawnLikeTheRideScreensAreWithDistinctWeight() {
@@ -1317,12 +1378,28 @@ final class VirtualGearsUITests: XCTestCase {
app.buttons["Fan"].tap()
assertVisible("screen.demo-headwind")
capture(.demoHeadwindAutomatic)
- app.buttons["Manual"].tap()
- assertVisibleElement(app.buttons["50 percent"])
+ selectDemoManualFanControl()
capture(.demoHeadwindManual)
assertJourneyCoverage()
}
+ private func selectDemoManualFanControl() {
+ let manual = app.buttons["Manual"]
+ XCTAssertTrue(manual.waitForExistence(timeout: 3))
+ let deadline = Date().addingTimeInterval(3)
+ while Date() < deadline, !manual.isHittable {
+ RunLoop.current.run(until: Date().addingTimeInterval(0.05))
+ }
+ XCTAssertTrue(manual.isHittable, "Manual fan control is not tappable")
+ manual.tap()
+ expectation(
+ for: NSPredicate(format: "isSelected == true"),
+ evaluatedWith: manual
+ )
+ waitForExpectations(timeout: 3)
+ assertVisibleElement(app.buttons["50 percent"])
+ }
+
private func openSettingsDestination(_ title: String, fixture: String) {
launch(fixture)
assertVisible("screen.settings")
diff --git a/docs/APP_STORE.md b/docs/APP_STORE.md
index 1af2afa..6e2e878 100644
--- a/docs/APP_STORE.md
+++ b/docs/APP_STORE.md
@@ -3,6 +3,14 @@
Everything Apple will ask you for, written out so you can copy and paste it.
Work top to bottom.
+!!! warning "Universal build uploaded; not submitted for review"
+
+ The universal iPhone/iPad target, iPad screenshots and shared-device wording
+ are complete. Build 19 was uploaded on 25 August 2026, is available to the internal
+ `Insiders` TestFlight group, and is attached to App Store version 1.0. The
+ physical iPad Bluetooth gate in Stage 5 passed. The version has not been
+ submitted for App Review.
+
---
## Stage 1 — Join the Apple Developer Program
@@ -43,7 +51,7 @@ itself whenever `main` changes, so the policy cannot drift out of date.
| User access | Full Access |
Apple accepted `Virtual Gears` as the public name. The app uses the same name on
-the iPhone and when advertising itself as a trainer.
+iPhone and iPad and when advertising itself as a trainer.
## Stage 4 — Fill in the listing
@@ -53,125 +61,81 @@ the iPhone and when advertising itself as a trainer.
### Promotional text (170 characters max, editable any time without review)
- Give your Wahoo KICKR virtual gears in compatible FTMS riding apps. Shift on your iPhone with 24 virtual gears or real groupset gearing.
+ Give your Wahoo KICKR virtual gears in compatible FTMS riding apps. Shift on iPhone or iPad with 24 virtual gears or real groupset gearing.
### Description
Your trainer has no gears. Your riding app may not offer virtual shifting.
Virtual Gears fixes both.
- Newer trainers gained virtual shifting in 2024. The Wahoo KICKR V5 did not, and
- Wahoo has confirmed it never will — the older hardware cannot support it. Many
- riding apps do not provide virtual shifting either.
+ Virtual Gears sits between your trainer and riding app. It appears as a normal
+ Bluetooth FTMS trainer, so compatible riding apps need no special virtual-shifting
+ support. Your riding app still controls the route and hills; Virtual Gears adds
+ the gears.
- Virtual Gears was built and physically tested with a KICKR V5. Other direct-drive
- KICKR models are expected to work but have not yet been physically tested. KICKR
- SNAP and KICKR BIKE are not supported.
+ Built and physically tested with Wahoo KICKR V5. Other direct-drive KICKR models
+ are expected to work but have not been physically tested. KICKR SNAP, KICKR BIKE
+ and trainers from other brands are not supported.
- Virtual Gears sits between both. It appears to compatible riding apps as a normal
- FTMS trainer, so they need no special virtual-shifting support. Your riding app still
- controls the route and its hills; Virtual Gears adds the gears.
-
- NOT ZWIFT-NATIVE VIRTUAL SHIFTING
- Virtual Gears supplies and displays its own gears on the iPhone through an ordinary
- FTMS trainer connection; it does not support Zwift's native gear system.
+ START WITH THE BIKE
+ First-run setup asks what is physically on your bike: its chainrings and cassette,
+ Zwift Cog or other single sprocket. Virtual Gears recommends where to leave the
+ chain and starts with ready-made Standard 24 virtual gears. Named Shimano, SRAM
+ and Campagnolo groupsets and custom gearing remain available in Settings.
- You get a full set of gears you can shift through mid-ride — either 24 evenly
- spaced gears with an extra-low climbing range or the gearing of a real
- groupset from Shimano, SRAM or Campagnolo.
+ SHIFT WITHOUT DISCONNECTING
+ Open the app and it finds your KICKR, connects, and makes the trainer available to
+ your riding app automatically. Tap Start Shifting to engage the gears. Tap Stop
+ Shifting to restore normal trainer gearing without disconnecting or stopping the
+ ride in your riding app.
- START WITH THE BIKE
- Required first-run setup asks only what is physically on the bike: the
- chainrings and either its cassette or a Zwift Cog/other single sprocket. Virtual
- Gears then recommends where to leave the chain and starts with the ready-made
- Standard 24 virtual gears. Named groupsets and custom gears remain available
- later in Settings.
-
- ONE PHYSICAL FACT CANNOT BE GUESSED
- Your bike never shifts. It stays in one gear and Virtual Gears changes gear by
- changing the wheel size the trainer works from, so what your legs feel is that
- parked gear multiplied by the wheel size the app sets. Virtual Gears therefore
- needs to know which gear your bike is parked in. It works out the quietest gear
- that still keeps every gear reachable and recommends it. If setup is unfinished,
- Settings gives one next action: fix gearing first, then confirm the parked gear.
-
- A TRAINER PROXY, WITH SHIFTING WHEN YOU WANT IT
- Open the app and it finds your trainer, connects to it and appears to your riding
- app on its own. Tap Start Shifting to engage the gears and Stop Shifting to remove
- them without disconnecting or stopping the ride in your riding app. Optional original Zwift Click and
- Wahoo Headwind accessories are found and remembered automatically too. If more than
- one matching device is found, Virtual Gears asks rather than guessing. If Clicks have
- identical names, pressing a button identifies the one you want.
+ This is not Zwift-native virtual shifting. Virtual Gears supplies and displays its
+ own gears on your iPhone or iPad through an ordinary FTMS connection.
GEARS YOU CAN SEE
- Your gears are drawn, not listed as numbers — one bar per gear, short bars for
- small steps and tall bars for the ones your legs will notice. Choose the 24
- virtual gears, or pick the groupset your bike has — Shimano, SRAM or Campagnolo,
- with your own chainrings and cassette available if it is not listed — and get the
- gears you would actually ride. The ladder is walked the way an electronic
- groupset shifts: one cog at a time, changing chainring at the right moment, so
- cross-chained and repeated combinations never appear and no shift is too small
- to feel.
-
- Virtual Gears is not affiliated with or endorsed by Zwift, Wahoo, Shimano, SRAM
- or Campagnolo. Those names describe only the gearing being simulated.
-
- BUILT FOR RIDING, NOT FOR READING
- Two large shift buttons stay easy to hit without looking down or sitting up,
- while the current gear is the biggest thing on screen. Hold one to keep shifting.
- The screen stays awake while the trainer proxy is available, including before
- shifting starts and after shifting stops.
-
- ACCESSIBLE RIDE CONTROLS
+ The gear ladder is drawn rather than shown as a bare number: short bars for small
+ steps and taller bars for changes your legs will notice. Use 24 wide-range virtual
+ gears or copy a real groupset. Cross-chained and duplicate combinations are left
+ out.
+
+ BUILT FOR RIDING
+ Two large shift buttons are easy to hit without looking down, while the current
+ gear is the largest thing on screen. Hold a button to keep shifting. A gear appears
+ only after the trainer confirms it, and every gear stays inside a range tested on
+ a real KICKR V5.
+
VoiceOver reads the current gear and announces confirmed changes. The gear readout
- is adjustable with VoiceOver gestures: swipe up for a harder gear and down for an
- easier one. The app uses standard iPhone controls and respects larger text, Reduce
+ supports adjustable VoiceOver gestures. The app respects larger text, Reduce
Motion and Differentiate Without Color.
TRY IT WITHOUT A TRAINER
- Tap Try Demo for a clearly marked simulated ride without Bluetooth. Shift through
- the gear ladder and watch the trainer's wheel size change with every gear. Compare
- virtual and real-bike gearing, and try example Click, Headwind and riding-app
- controls. Demo choices never replace saved equipment.
-
- WORKS WITH THE APPS YOU ALREADY USE
- Virtual Gears appears as an ordinary FTMS indoor trainer. That adds virtual shifting
- to compatible apps that have none of their own, without requiring a plugin or
- account. Apps that set their own wheel size are honoured — the gears are rebuilt
- around whatever size the app asks for. If an app sends no size, Virtual Gears uses
- the optional Wheel circumference saved in Settings, or the 2105 mm (700×25 road)
- default.
-
- OPTIONAL SHIFT BUTTONS
- Wake an original Zwift Click before opening Virtual Gears and it connects
- automatically. It shifts alongside the on-screen buttons and visibly presses
- the matching control, but is never required.
-
- OPTIONAL HEADWIND CONTROL
- Turn on a Wahoo KICKR HEADWIND before opening Virtual Gears and it connects
- automatically. Leave fan speed with the Headwind's own sensor, or choose a manual
- speed from the ride screen. The fan is optional and never blocks a ride.
-
- CAREFUL WITH YOUR TRAINER
- A gear is only shown after your trainer confirms it. Every gear stays inside a
- range tested on a real trainer.
+ Tap Try Demo for a clearly marked simulated ride without Bluetooth. Explore the
+ production gear picker, large ride controls, virtual and real-bike gearing, and
+ example equipment status. Demo choices never replace saved equipment.
- NOT AN ERG APP
- Workouts that set a target power are refused. Virtual Gears controls how hard a
- gear feels, not your wattage.
+ OPTIONAL ZWIFT CLICK
+ Wake an original Zwift Click and Virtual Gears finds and remembers it automatically.
+ It shifts alongside the on-screen buttons and visibly presses the matching control,
+ but is never required.
- NOTE ON WHEEL CIRCUMFERENCE
- Virtual Gears cannot read a wheel circumference previously set in the Wahoo app.
- If you use a custom value, enter the same value as Wheel circumference in
- Virtual Gears Settings before shifting. Common-size shortcuts and direct entry
- support values from 1800 to 2400 mm. A value sent by the riding app takes precedence.
+ OPTIONAL KICKR HEADWIND
+ Connect a Wahoo KICKR HEADWIND and leave control with its own sensor or choose a
+ manual speed from the ride screen. The fan is optional and never blocks a ride.
- NO ACCOUNTS, NO INTERNET, NO TRACKING
- The app has no networking code in it at all. Nothing about your ride leaves your
- iPhone.
+ COMPATIBLE RIDING APPS
+ Virtual Gears works as an ordinary FTMS indoor trainer. Apps that set a wheel size
+ are honoured: gears are rebuilt around that size. Otherwise the app uses your
+ optional saved circumference or a 2105 mm default.
- Requires a compatible Wahoo KICKR. Built and physically tested with KICKR V5.
- Virtual Gears is not made by, endorsed by or affiliated with Wahoo Fitness.
+ NOT AN ERG APP
+ Workouts that set target power are refused. Virtual Gears controls how hard a gear
+ feels, not your wattage.
+
+ NO ACCOUNTS, INTERNET OR TRACKING
+ The app has no networking code. Nothing about your ride leaves your device.
+
+ Virtual Gears is not affiliated with or endorsed by Wahoo, Zwift, Shimano, SRAM or
+ Campagnolo. Those names describe compatible hardware or simulated gearing only.
### Keywords (100 characters max, comma separated, no spaces)
@@ -200,7 +164,7 @@ Issues and Discussions must stay enabled on the repository.
### Screenshots
-Required: **6.9-inch iPhone**. The six portrait images listed below are
+Required iPhone set: **6.9-inch iPhone**. The six portrait images listed below are
1320 × 2868, captured on an iPhone 17 Pro Max simulator, so they can be
uploaded as they are. Apple scales them down for smaller phones; one set is
enough. The two landscape images are 2868 × 1320 documentation views; they are
@@ -219,6 +183,20 @@ Upload these six, in this order:
6. `headwind-control.png` — optional Automatic/Manual Headwind control with
one-tap speeds.
+Required iPad set: **13-inch iPad**. The matching portrait images under
+`docs/app-store/ipad-13/` are 2064 × 2752 simulator captures:
+
+1. `riding.png`
+2. `setup.png`
+3. `bike-setup.png`
+4. `parked-gear.png`
+5. `gears.png`
+6. `headwind-control.png`
+
+These assets prove the app-owned layout at App Store dimensions. They do not
+prove Bluetooth behavior. All six were uploaded to the 13-inch iPad screenshot
+set with build 19 after the physical iPad gate below passed.
+
### Privacy answers ("App Privacy" section)
Answer **"No, we do not collect data from this app."** That is accurate: nothing is
@@ -227,6 +205,34 @@ the app.
## Stage 5 — Upload the build
+Before changing `CURRENT_PROJECT_VERSION`, archiving or uploading:
+
+1. Confirm `xcrun devicectl list devices` shows a physical iPad.
+2. Install the development build on that iPad.
+3. Connect to the physical KICKR, complete setup and advertise Virtual Gears to a
+ riding app.
+4. Start shifting, confirm easier and harder changes, stop shifting, and confirm
+ the normal wheel circumference is restored without dropping the riding app.
+5. If available, also connect the original Zwift Click and Headwind. Record
+ exactly what was tested in `DEVELOPMENT.md`.
+
+An iPad Air (`iPad14,10`, iPadOS 26.6.1) was connected by USB and paired on
+25 August 2026. After Developer Mode was enabled, automatic provisioning
+registered the device and the signed 1.0 (18) development build installed and
+launched successfully. Physical XCTest still times out while enabling automation
+mode. The corrected build physically found and selected the sole original Zwift
+Click automatically on the startup screen. The same iPad then passed the complete
+KICKR flow above, including connected Click shifts, normal wheel-size restoration
+and riding-app continuity across Stop. The physical Bluetooth gate is complete;
+simulator coverage remains the evidence for the automated layout matrix.
+
+Build 19 was archived and uploaded successfully on 25 August 2026. App Store
+Connect reports it as `Validated`, with minimum iOS 17.0, arm64 architecture and
+device family `iPhone, iPad`. It is `Ready to Submit` in TestFlight, belongs to
+the internal `Insiders` group, and is attached to App Store version 1.0. The
+universal promotional text, description, review notes and six iPad screenshots
+were saved live. The version remains unsubmitted for review.
+
In Xcode:
1. Choose **Any iOS Device** as the destination (not a simulator).
@@ -234,22 +240,23 @@ In Xcode:
3. When the Organizer opens: **Distribute App** → **App Store Connect** → **Upload**.
4. Accept the automatic signing offers.
-The build appears in App Store Connect after 5–15 minutes of processing. Attach it
-to the version, then **Add for Review** → **Submit**.
+The build appears in App Store Connect after processing. Attach it to the
+version, then use **Add for Review** → **Submit** only when the release should be
+sent to App Review.
## Stage 6 — The review notes (do not skip this)
The reviewer does not need a smart trainer to inspect the app, but App Review
still asked for a screen recording. Both matter.
-This is what is live in **App Review Information → Notes**. It answers, in
-order, the seven questions App Review asked when they rejected 1.0 (5) under
-Guideline 2.1:
+This note block was saved live with build 19 after the physical iPad gate. It
+answers, in order, the seven questions App Review asked when they rejected
+1.0 (5) under Guideline 2.1:
VIRTUAL GEARS - APP REVIEW NOTES
WHAT THE APP DOES, AND FOR WHOM (point 3)
- A Wahoo KICKR indoor bicycle trainer has no gears. Virtual Gears adds them. The iPhone connects to the trainer as a Bluetooth client and at the same time presents itself to a riding app on a computer as a standard FTMS indoor bike. It passes the riding app's resistance instructions through to the trainer and relays the trainer's data back, and applies the rider's chosen gear by rescaling the trainer's wheel-circumference setting. That is what produces the gears. Audience: indoor cyclists whose trainer or riding app offers no gear shifting. It solves that without buying a new trainer.
+ A Wahoo KICKR indoor bicycle trainer has no gears. Virtual Gears adds them. The iPhone or iPad connects to the trainer as a Bluetooth client and at the same time presents itself to a riding app on a computer as a standard FTMS indoor bike. It passes the riding app's resistance instructions through to the trainer and relays the trainer's data back, and applies the rider's chosen gear by rescaling the trainer's wheel-circumference setting. That is what produces the gears. Audience: indoor cyclists whose trainer or riding app offers no gear shifting. It solves that without buying a new trainer.
HOW TO SET UP AND REACH THE MAIN FEATURES (point 4)
No account, login, credentials or sample files are needed. No hardware is needed either.
@@ -257,11 +264,12 @@ Guideline 2.1:
With hardware: switch the KICKR on and open the app. It finds and connects on its own. Then point any FTMS riding app on a computer at "Virtual Gears" and shift with the on-screen buttons or an original Zwift Click.
DEVICES AND SYSTEMS TESTED ON (point 2)
- iPhone 17 Pro, iOS 26.6.
+ Physically tested: iPhone 17 Pro, iOS 26.6; iPad Air (iPad14,10), iPadOS 26.6.1.
+ Simulator-tested interface: iPad mini (A17 Pro) and iPad Pro 13-inch (M5), portrait and landscape.
Trainer: Wahoo KICKR V5. Accessories: original Zwift Click, Wahoo KICKR HEADWIND. Riding apps driven end to end: FulGaz on macOS and Windows, plus RealVelo and MyWhoosh on Windows.
EXTERNAL SERVICES, TOOLS AND PLATFORMS (point 5)
- None. The app contains no networking code at all. There are no servers, accounts, analytics, adverts, tracking, payment processors, data providers or AI services. Everything happens on the device and over local Bluetooth. Nothing leaves the phone.
+ None. The app contains no networking code at all. There are no servers, accounts, analytics, adverts, tracking, payment processors, data providers or AI services. Everything happens on the device and over local Bluetooth. Nothing leaves the device.
REGIONAL DIFFERENCES (point 6)
None. The app behaves identically in every region. It has no region-specific features, content or pricing behaviour.
@@ -298,7 +306,7 @@ For each update, raise `MARKETING_VERSION` (1.0 → 1.1) and
upload again. `CURRENT_PROJECT_VERSION` must increase on every single upload, even
a re-upload of the same version.
-The current TestFlight build is 1.0 (18). Build 5 added the Demo Mode that shows
+The current TestFlight build is 1.0 (19). Build 5 added the Demo Mode that shows
the wheel size and command bytes changing. Build 6 removed a wheel-size limit
that was never real: a physical KICKR V5 accepts every value the command can
express, so the app now states the range of riding-app wheel sizes it supports
@@ -389,9 +397,12 @@ saved Click. The corrected development build was physically checked on the
iPhone 17 Pro with the original Click that exposed the regression. Build 17
should not be submitted for App Review; use build 18.
-The live App Store description still carries the old "starts the session"
-sentence. It is corrected in this file and needs the same edit in App Store
-Connect on the next metadata change.
+Build 19 makes the target universal for iPhone and iPad while
+keeping iOS/iPadOS 17 as the minimum. It adds all four iPad orientations without
+requiring full screen, caps ride controls at deliberate iPad sizes, and runs the
+UI suite on iPhone 17 Pro, iPad mini and iPad Pro 13-inch simulators. The
+physical iPad gate above passed before build 19 was uploaded. Its universal
+description and screenshots are now live in the editable App Store version.
Uploading without opening Xcode:
diff --git a/docs/DEMO_VIDEO.md b/docs/DEMO_VIDEO.md
index 7417ffe..32cf396 100644
--- a/docs/DEMO_VIDEO.md
+++ b/docs/DEMO_VIDEO.md
@@ -35,12 +35,17 @@ Their wording, and what it means here:
The remaining items in their list — account registration, purchases,
user-generated content — do not exist in this app. There is nothing to film.
+The existing recording remains valid evidence for the physically tested iPhone
+path. The core connect/advertise/shift/stop flow was also repeated on a physical
+iPad Air running iPadOS 26.6.1 before build 19; that evidence is recorded in
+`DEVELOPMENT.md`. Simulator footage is not a substitute for that Bluetooth check.
+
## Before you start
**Delete the app from the phone first.** Deleting it takes its Bluetooth
permission with it, so the prompt appears again on the next launch, and the app
starts with no remembered trainer — exactly what an App Review device sees.
-Then install the latest TestFlight build, currently 1.0 (18), but **do not open
+Then install the latest TestFlight build, currently 1.0 (19), but **do not open
it**. The
recording has to start from the Home screen.
diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md
index 72ee370..04e9f68 100644
--- a/docs/PRIVACY.md
+++ b/docs/PRIVACY.md
@@ -1,6 +1,6 @@
# Privacy Policy for Virtual Gears
-Last updated: 8 August 2026
+Last updated: 25 August 2026
## The short version
@@ -9,14 +9,14 @@ the app has no way to send anything anywhere.
## What the app stores
-Virtual Gears remembers a few things on your iPhone so you don't have to set them up
+Virtual Gears remembers a few things on your device so you don't have to set them up
again each ride:
- Which trainer you last used, so it can reconnect on its own.
- Which gears you chose.
- Which optional shift controller or Headwind fan it should reconnect to.
-These stay on your iPhone. Deleting the app deletes them.
+These stay on your iPhone or iPad. Deleting the app deletes them.
Demo Mode uses a separate in-memory setup. Its simulated equipment and gear
choices are discarded when you leave the demo and are not written over the
diff --git a/docs/accessibility.md b/docs/accessibility.md
index e14f403..61af836 100644
--- a/docs/accessibility.md
+++ b/docs/accessibility.md
@@ -25,16 +25,16 @@ and haptic, which occur only after the trainer confirms the shift.
The ride controls use large targets in portrait and landscape. At Accessibility
Dynamic Type sizes, the ride screen uses its vertical layout even when the
-phone is sideways so controls have room to reflow instead of being compressed.
+device is sideways so controls have room to reflow instead of being compressed.
Equipment statuses reflow into a two-column grid at those sizes, keeping short
names on one line instead of breaking words apart. The toolbar shortens the gear
menu label so it cannot overlap the equipment controls.
-Virtual Gears respects the iPhone's Reduce Motion setting for the gear-number
+Virtual Gears respects the device's Reduce Motion setting for the gear-number
transition. Connection and status states use names and symbols alongside
colour; the gear rail adds an outline when Differentiate Without Color is on.
-## Other iPhone accessibility features
+## Other iPhone and iPad accessibility features
The app uses standard SwiftUI buttons, menus, pickers, sliders and navigation
controls. These retain their normal support for Switch Control, Voice Control,
@@ -47,7 +47,7 @@ Try these settings with **Try Demo** before a real ride:
1. Turn on VoiceOver in **Settings > Accessibility > VoiceOver** and adjust
the simulated gear readout.
2. Choose an Accessibility text size in **Settings > Accessibility > Display &
- Text Size > Larger Text**, then rotate the phone.
+ Text Size > Larger Text**, then rotate the device.
3. Turn on **Reduce Motion** and **Differentiate Without Color** in **Settings
> Accessibility > Display & Text Size**.
diff --git a/docs/app-store/ipad-13/bike-setup.png b/docs/app-store/ipad-13/bike-setup.png
new file mode 100644
index 0000000..7f5634c
Binary files /dev/null and b/docs/app-store/ipad-13/bike-setup.png differ
diff --git a/docs/app-store/ipad-13/gears.png b/docs/app-store/ipad-13/gears.png
new file mode 100644
index 0000000..5e40610
Binary files /dev/null and b/docs/app-store/ipad-13/gears.png differ
diff --git a/docs/app-store/ipad-13/headwind-control.png b/docs/app-store/ipad-13/headwind-control.png
new file mode 100644
index 0000000..dcc7d43
Binary files /dev/null and b/docs/app-store/ipad-13/headwind-control.png differ
diff --git a/docs/app-store/ipad-13/parked-gear.png b/docs/app-store/ipad-13/parked-gear.png
new file mode 100644
index 0000000..5e2b422
Binary files /dev/null and b/docs/app-store/ipad-13/parked-gear.png differ
diff --git a/docs/app-store/ipad-13/riding.png b/docs/app-store/ipad-13/riding.png
new file mode 100644
index 0000000..ebbfb7f
Binary files /dev/null and b/docs/app-store/ipad-13/riding.png differ
diff --git a/docs/app-store/ipad-13/setup.png b/docs/app-store/ipad-13/setup.png
new file mode 100644
index 0000000..6443117
Binary files /dev/null and b/docs/app-store/ipad-13/setup.png differ
diff --git a/docs/how-it-works.md b/docs/how-it-works.md
index fd3e79d..7d172a9 100644
--- a/docs/how-it-works.md
+++ b/docs/how-it-works.md
@@ -1,6 +1,6 @@
# How the gears work
-
+
Virtual Gears was built and physically tested with a KICKR V5. Other
direct-drive KICKR models are expected to work but have not yet been physically
@@ -9,7 +9,7 @@ size: a smaller wheel covers less ground per pedal stroke, which feels like an
easier gear. The riding app's own terrain command is left untouched, so the two
never fight.
-Virtual Gears chooses and displays the gear on the iPhone. The riding app sees an
+Virtual Gears chooses and displays the gear on your iPhone or iPad. The riding app sees an
ordinary FTMS trainer and does not need a virtual-shifting feature of its own.
Opening Virtual Gears connects this transparent trainer proxy. **Start Shifting**
@@ -17,8 +17,8 @@ adds the gears; **Stop Shifting** removes the virtual gear and restores the
normal wheel circumference. Neither button starts or stops the ride in the
riding app, and stopping shifting leaves that app connected.
-The iPhone screen stays awake for as long as this trainer proxy is available.
-iOS changes Bluetooth advertising after the phone locks, which can make a
+The device screen stays awake for as long as this trainer proxy is available.
+iOS and iPadOS change Bluetooth advertising after the device locks, which can make a
waiting trainer disappear from riding apps on Windows and other computers.
## The gear your bike is parked in
diff --git a/docs/index.md b/docs/index.md
index a91ed40..a9bf944 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -8,9 +8,9 @@
none.**
Use the smaller front ring if your bike has one, then pick a rear gear that keeps
-the chain straight and leave it there. Shifting happens on your iPhone instead,
-which changes how hard the trainer feels. Nothing on the bike moves, so there is
-no chain noise, no dropped chain, and no wear.
+the chain straight and leave it there. Shifting happens on your iPhone or iPad
+instead, which changes how hard the trainer feels. Nothing on the bike moves, so
+there is no chain noise, no dropped chain, and no wear.
!!! quote "Why this exists"
@@ -39,10 +39,11 @@ no chain noise, no dropped chain, and no wear.
## What it is
-Your riding app connects to the iPhone instead of to the trainer. Virtual Gears
-passes everything through in both directions, and applies your chosen gear on
-top. The riding app still controls the road: hills feel like hills. It does not
-need special virtual-shifting support; ordinary FTMS trainer support is enough.
+Your riding app connects to your iPhone or iPad instead of to the trainer.
+Virtual Gears passes everything through in both directions, and applies your
+chosen gear on top. The riding app still controls the road: hills feel like
+hills. It does not need special virtual-shifting support; ordinary FTMS trainer
+support is enough.

@@ -57,6 +58,15 @@ speed. Both remain optional and never hold up a ride.
+
+
+
+
+
+The iPad layout uses the same app-owned screens and actions rather than a
+separate tablet workflow. Controls stay bounded in full screen and reflow through
+the existing compact layout as Split View or Stage Manager narrows the window.
+
## What you get
### Start with the bike
@@ -133,16 +143,16 @@ list of tooth counts can tell you.
### Shifting
-Two large buttons on the phone, placed for sweaty hands and a locked-out gaze.
+Two large buttons on the screen, placed for sweaty hands and a locked-out gaze.
An original Zwift Click can be added and shifts the same gears, but it is never
required and nothing ever waits for it. Its physical press is mirrored on the
-matching phone button; the gear number still changes only after the trainer
+matching on-screen button; the gear number still changes only after the trainer
confirms the shift.
The current gear is also an adjustable VoiceOver control: swipe up for a harder
gear and down for an easier one. Confirmed gear changes are announced, so the
ride can be controlled without looking at the screen. [Accessibility
-details](accessibility.md) cover larger text and other iPhone settings.
+details](accessibility.md) cover larger text and other iPhone and iPad settings.
### Headwind control
diff --git a/docs/requirements.md b/docs/requirements.md
index 225960b..854d4a9 100644
--- a/docs/requirements.md
+++ b/docs/requirements.md
@@ -2,10 +2,10 @@
| | |
|---|---|
-| **iPhone** | Running iOS 17 or later — any iPhone from the XS onwards |
+| **Apple device** | An iPhone or iPad running iOS/iPadOS 17 or later |
| **Trainer** | A compatible direct-drive Wahoo KICKR. [See below](#which-trainers-work). |
| **Riding app** | An app that can connect to an FTMS trainer. |
-| **Shifting** | Large buttons on the iPhone. An original Zwift Click is optional. |
+| **Shifting** | Large on-screen buttons. An original Zwift Click is optional. |
| **Fan** | Optional. A Wahoo KICKR HEADWIND. |
## Which trainers work
@@ -64,12 +64,12 @@ That is the gap Virtual Gears fills. Three differences worth knowing:
- **Your riding app will not display the gear.** Zwift can show it because Zwift
decides it. Virtual Gears decides it, and there is no way to tell a riding app
what gear you are in — the Bluetooth standard has no such message. The gear is
- shown large on the phone instead.
+ shown large on your iPhone or iPad instead.
- **Virtual Gears does not require virtual-shifting support in the riding app.**
It appears as an ordinary FTMS trainer, bringing gears to compatible apps that
do not provide their own.
- **Virtual Gears does not support Zwift-native virtual shifting.** It supplies
- and displays its own gears on the iPhone.
+ and displays its own gears on your iPhone or iPad.
[wahoo]: https://support.wahoofitness.com/hc/en-us/articles/16865097915666-Virtual-shifting-with-Wahoo-smart-trainers
@@ -105,16 +105,21 @@ MyWhoosh ignored FTMS ride data in direct testing and read power and cadence
from Cycling Power Service instead. Virtual Gears now publishes that service,
and the complete iPhone-to-Windows path has been ridden end to end.
+The iPad interface is exercised on iPad mini and 13-inch iPad simulators. That
+proves the layouts and navigation, not Bluetooth. The first universal release
+remains blocked until a physical iPad has connected to the KICKR, advertised the
+trainer proxy, shifted, stopped and restored the normal wheel size.
+
## Your first ride
1. Put your bike on the trainer and wake the trainer by turning the pedals.
-2. Open Virtual Gears on your iPhone. It finds the trainer and connects on its
- own, then appears as a trainer. If it finds more than one trainer, it asks
- you which one.
+2. Open Virtual Gears on your iPhone or iPad. It finds the trainer and connects
+ on its own, then appears as a trainer. If it finds more than one trainer, it
+ asks you which one.
3. On your computer, open your riding app and pair with the trainer it offers.
- That will be your iPhone, appearing as a trainer.
+ That will be your Apple device, appearing as a trainer.
4. Tap **Start Shifting** when you want gears. Shift with the two large buttons
- on the phone.
+ on the device.
The two-step first-run setup is required. It asks what is physically on the bike
and then confirms the recommended parked chain position. Standard 24 virtual
@@ -130,23 +135,23 @@ exercise equipment. A real ride still requires the compatible hardware above.
### What name to look for
Virtual Gears tells your riding app it is called **Virtual Gears**, and most apps
-show that. Some show your iPhone's own name instead — "iPhone Anna", say —
+show that. Some show your Apple device's own name instead — "iPhone Anna", say —
and a few add a short code after it.
Either name is the right one to pick. There is only ever one of them in the
list, because your trainer itself is already busy talking to Virtual Gears and
so does not appear.
-This is not something the app can fix. iPhones report their own name over
+This is not something the app can fix. Apple devices report their own name over
Bluetooth, and Apple does not let an app change it. If seeing "Virtual Gears"
-in the list matters to you, rename the phone itself in Settings → General →
+in the list matters to you, rename the device itself in Settings → General →
About → Name — but that changes its name everywhere else too.
## Adding a Zwift Click
Wake the Click by pressing a button before opening Virtual Gears. The app finds,
connects and remembers it automatically. It shifts the same gears as the
-on-screen buttons, and pressing it visibly presses the matching phone button.
+on-screen buttons, and pressing it visibly presses the matching on-screen button.
The gear number still changes only after the trainer confirms the shift.
Nothing ever waits for the Click, so one that runs out of battery mid-ride
costs you nothing but the buttons on the handlebar.
diff --git a/docs/support.md b/docs/support.md
index 954198b..d13aefc 100644
--- a/docs/support.md
+++ b/docs/support.md
@@ -20,7 +20,7 @@ Most problems have a known answer already:
- **The trainer is not found.** Turn the pedals to wake it, and close any other
app that may already be connected to it. A trainer can only talk to one thing
at a time.
-- **The riding app shows your iPhone's name instead of "Virtual Gears".** That
+- **The riding app shows your Apple device's name instead of "Virtual Gears".** That
is normal and it is the right device to pick. See
[What name to look for](requirements.md#what-name-to-look-for).
- **Your trainer is not a KICKR V5.** Check
@@ -40,7 +40,7 @@ Most problems have a known answer already:
[If you set a custom wheel circumference](requirements.md#if-you-set-a-custom-wheel-circumference).
- **A Windows riding app disconnected mid-ride and won't come back.** A weak
Bluetooth link can time out, and some riding apps on Windows do not scan for
- your phone again on their own afterwards. Restart the riding app to make it
+ your Apple device again on their own afterwards. Restart the riding app to make it
look again. This has been confirmed to be the riding app's own reconnect
behaviour, not a Virtual Gears fault — the trainer stayed connected and
working the whole time.
@@ -49,7 +49,7 @@ Most problems have a known answer already:
The more of this you can give, the better the odds of a fix:
-- Your trainer model, and your iPhone and iOS version
+- Your trainer model, Apple device model, and iOS or iPadOS version
- The riding app you were using, and on what computer
- What you did, what you expected, and what happened instead
- Whether the app showed an error, and its exact wording
diff --git a/mkdocs.yml b/mkdocs.yml
index fe1647e..d98a392 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,6 +1,6 @@
site_name: Virtual Gears
site_description: >-
- Virtual gears for a smart trainer. An iPhone app that sits between your
+ Virtual gears for a smart trainer. An iPhone and iPad app that sits between your
riding app and a Wahoo KICKR V5 and gives you gears the trainer does not have.
site_url: https://sbroenne.github.io/VirtualGears/
repo_url: https://github.com/sbroenne/VirtualGears
|