feat: Add iOS 17+ location simulation support via pymobiledevice3 - #219
feat: Add iOS 17+ location simulation support via pymobiledevice3#219bhargavchintam wants to merge 2 commits into
Conversation
- Add PMD3Helper.swift: Wraps pymobiledevice3 CLI for iOS 17+ devices - Add PMD3DeviceWrapper.swift: Device protocol wrapper for iOS 17+ - Modify Device+Extension.swift: Start tunneld during pairing for iOS 17+ - Modify SidebarViewController.swift: Wrap iOS 17+ devices in PMD3DeviceWrapper - Modify MapViewController.swift: Show iOS 17+ specific error messages - Remove Info preference tab (storyboard + AppDelegate auto-open) - Disable Xcode 26 auto-generated asset symbols (conflict with extensions) - Update README with iOS 17+ setup instructions and troubleshooting
There was a problem hiding this comment.
Pull request overview
Adds iOS 17+ location simulation support by integrating pymobiledevice3 (RemoteXPC/tunneld + DVT simulate-location) while updating UI, pairing flow, and documentation to reflect the new backend.
Changes:
- Introduces
PMD3HelperandPMD3DeviceWrapperto route iOS 17+ location simulation throughpymobiledevice3. - Updates device selection/pairing and error messaging to support the new iOS 17+ flow and show setup guidance.
- Removes the Info preferences tab/auto-open behavior and updates docs/build configuration accordingly.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents iOS 17+ prerequisites, setup, and troubleshooting. |
| LocationSimulator/ViewController/SidebarViewController/SidebarViewController.swift | Wraps selected iOS 17+ devices with PMD3DeviceWrapper. |
| LocationSimulator/ViewController/MapViewController/MapViewController.swift | Shows iOS 17+ specific setup messaging on pairing failures. |
| LocationSimulator/Storyboard/Main.storyboard | Removes Info preference tab UI. |
| LocationSimulator/Extensions/LocationSpoofer/PMD3Helper.swift | Implements CLI discovery, tunneld management, and DVT-based simulation process handling. |
| LocationSimulator/Extensions/LocationSpoofer/PMD3DeviceWrapper.swift | Routes simulate/disable calls to PMD3Helper for iOS 17+. |
| LocationSimulator/Extensions/LocationSpoofer/Device+Extension.swift | Adds requiresPMD3 and updates pairing logic to start tunneld for iOS 17+. |
| LocationSimulator/AppDelegate.swift | Removes version-update auto-open of Info preferences, leaving onboarding behavior. |
| LocationSimulator.xcodeproj/xcshareddata/xcschemes/LocationSimulator.xcscheme | Updates scheme metadata (upgrade version). |
| LocationSimulator.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | Adds SwiftPM resolution file. |
| LocationSimulator.xcodeproj/project.pbxproj | Adds new sources and modifies several build settings related to Xcode upgrade/signing/platforms. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ); | ||
| LIBRARY_SEARCH_PATHS = ""; | ||
| MACH_O_TYPE = mh_execute; | ||
| MACOSX_DEPLOYMENT_TARGET = 11.0; |
There was a problem hiding this comment.
MACOSX_DEPLOYMENT_TARGET is raised to 11.0 here, but the README still states macOS 10.15+ support. If dropping 10.15 is intentional, the README (and any release notes) should be updated; otherwise, revert this to keep the documented minimum supported macOS version.
| MACOSX_DEPLOYMENT_TARGET = 11.0; | |
| MACOSX_DEPLOYMENT_TARGET = 10.15; |
| // Wrap iOS 17+ devices in PMD3DeviceWrapper for pymobiledevice3 support | ||
| if let iosDevice = device as? IOSDevice, | ||
| PMD3Helper.deviceRequiresPMD3(iosDevice.majorVersion) { | ||
| mapViewController.device = PMD3DeviceWrapper(wrapping: iosDevice) | ||
| } else { | ||
| mapViewController.device = device |
There was a problem hiding this comment.
The wrapper is applied to all iOS 17+ IOSDevices even when pymobiledevice3 isn’t available. In that case PMD3DeviceWrapper.usesPMD3 is false and pair() short-circuits (because self is no longer an IOSDevice), so the user may see a “connected” state but location simulation won’t work and they won’t get the intended install guidance. Consider only wrapping when PMD3Helper.shared.isAvailable is true, or ensure the wrapper forwards pair()/setup errors so missing pymobiledevice3 is surfaced during pairing.
| // Only real iOS Devices require a pairing if the DeveloperDiskImage is not already mounted | ||
| guard let device = self as? IOSDevice, !device.developerDiskImageIsMounted else { return } | ||
|
|
||
| // For iOS 17+ without pymobiledevice3, warn the user | ||
| if PMD3Helper.deviceRequiresPMD3(device.majorVersion) && !PMD3Helper.shared.isAvailable { | ||
| logError("Device+Extension: iOS 17+ detected but pymobiledevice3 is not installed!") |
There was a problem hiding this comment.
When a device is wrapped in PMD3DeviceWrapper but usesPMD3 is false (e.g., pymobiledevice3 not installed), this method falls through to the guard let device = self as? IOSDevice ... else { return } and silently returns. That bypasses the iOS 17+ “install pymobiledevice3” error path and incorrectly reports pairing success. Consider handling the wrapper case explicitly when usesPMD3 == false (delegate to wrappedDevice.pair() and/or throw a clear error for iOS 17+).
| SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; | ||
| SUPPORTS_MACCATALYST = NO; | ||
| SWIFT_OBJC_BRIDGING_HEADER = "LocationSimulator-Bridging-Header.h"; | ||
| SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | ||
| SWIFT_VERSION = 5.0; | ||
| TARGETED_DEVICE_FAMILY = 1; |
There was a problem hiding this comment.
SUPPORTED_PLATFORMS is set to include iphoneos iphonesimulator and TARGETED_DEVICE_FAMILY is set, but this target is a macOS app. Including iOS platforms here can lead to invalid build settings, confusing scheme destinations, and code signing/entitlement issues. Suggest reverting SUPPORTED_PLATFORMS to macOS-only and removing iOS-only settings like TARGETED_DEVICE_FAMILY.
| SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; | |
| SUPPORTS_MACCATALYST = NO; | |
| SWIFT_OBJC_BRIDGING_HEADER = "LocationSimulator-Bridging-Header.h"; | |
| SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | |
| SWIFT_VERSION = 5.0; | |
| TARGETED_DEVICE_FAMILY = 1; | |
| SUPPORTED_PLATFORMS = macosx; | |
| SUPPORTS_MACCATALYST = NO; | |
| SWIFT_OBJC_BRIDGING_HEADER = "LocationSimulator-Bridging-Header.h"; | |
| SWIFT_OPTIMIZATION_LEVEL = "-Onone"; | |
| SWIFT_VERSION = 5.0; |
| isa = XCBuildConfiguration; | ||
| buildSettings = { | ||
| ALWAYS_SEARCH_USER_PATHS = NO; | ||
| ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; |
There was a problem hiding this comment.
The PR description mentions disabling asset symbol generation to avoid Xcode build conflicts, but this configuration enables ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES. This can reintroduce the same conflicts depending on which build configuration is used. Consider aligning this setting with the others (set to NO) or document why Homebrew differs.
| ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; | |
| ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; |
| INFOPLIST_KEY_NSPrincipalClass = ""; | ||
| INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; | ||
| MACOSX_DEPLOYMENT_TARGET = 10.15; | ||
| MACOSX_DEPLOYMENT_TARGET = 11.0; |
There was a problem hiding this comment.
The Help target deployment target is bumped to macOS 11.0 here. If the app is still intended to support macOS 10.15 (per README), this change will prevent the bundled help from working on older supported systems. Consider keeping the Help bundle’s deployment target aligned with the app’s minimum supported macOS version.
| MACOSX_DEPLOYMENT_TARGET = 11.0; | |
| MACOSX_DEPLOYMENT_TARGET = 10.15; |
| // Use osascript to run tunneld with sudo — this prompts the user for their password | ||
| let script = """ | ||
| do shell script "\(pmd3Path) remote tunneld -d" with administrator privileges | ||
| """ | ||
|
|
||
| let task = Process() | ||
| task.launchPath = "/usr/bin/osascript" | ||
| task.arguments = ["-e", script] | ||
|
|
||
| let stdoutPipe = Pipe() | ||
| let stderrPipe = Pipe() | ||
| task.standardOutput = stdoutPipe | ||
| task.standardError = stderrPipe | ||
|
|
||
| do { | ||
| try task.run() | ||
| task.waitUntilExit() | ||
|
|
There was a problem hiding this comment.
osascript “do shell script” runs synchronously until the invoked command exits. If pymobiledevice3 remote tunneld -d is a long-running foreground daemon (as implied by the README description of a background service), waitUntilExit() will block indefinitely and pairing will never complete. Start tunneld in a way that returns immediately (e.g., background the command, use a launchd job, or spawn sudo with a helper) and then poll for readiness.
| COMBINE_HIDPI_IMAGES = YES; | ||
| DEAD_CODE_STRIPPING = YES; | ||
| DEVELOPMENT_TEAM = YJ8XYES89R; | ||
| DEVELOPMENT_TEAM = 7C252RKZ47; |
There was a problem hiding this comment.
These build settings hardcode a specific DEVELOPMENT_TEAM value. This typically breaks building/signing for other contributors/CI and shouldn’t be committed to a shared project. Prefer leaving DEVELOPMENT_TEAM unset (or $(DEVELOPMENT_TEAM)/DEVELOPMENT_TEAM = "") and let developers set it locally.
| DEVELOPMENT_TEAM = 7C252RKZ47; | |
| DEVELOPMENT_TEAM = ""; |
| // Use osascript to run tunneld with sudo — this prompts the user for their password | ||
| let script = """ | ||
| do shell script "\(pmd3Path) remote tunneld -d" with administrator privileges | ||
| """ |
There was a problem hiding this comment.
The ensureTunneld method builds an AppleScript do shell script command by interpolating pmd3Path directly into the shell string that is executed with administrator privileges. If an attacker can influence the resolved pymobiledevice3 path (for example via a crafted PATH entry that contains quotes or shell metacharacters and is picked up by findPMD3Path()), they can inject arbitrary commands into this root shell. To mitigate this, avoid composing shell command strings from untrusted paths (e.g. either properly escape/quote pmd3Path for AppleScript or bypass the shell entirely by invoking pymobiledevice3 via Process with argument arrays).
Summary
This PR adds iOS 17+ location simulation support using pymobiledevice3, since Apple removed the legacy
com.apple.dt.simulatelocationlockdown service starting with iOS 17.Changes
New Files
PMD3Helper.swift— Wraps the pymobiledevice3 CLI for iOS 17+ devices:pymobiledevice3binarytunneldlifecycle (starts with admin privileges viaosascript)developer dvt simulate-location) for persistent location simulation--separatorPMD3DeviceWrapper.swift—Deviceprotocol wrapper that routessimulateLocation/disableSimulationthrough pymobiledevice3 for iOS 17+, while delegating everything else to the originalIOSDeviceModified Files
Device+Extension.swift— Starts tunneld during the pairing step for iOS 17+ devicesSidebarViewController.swift— Wraps iOS 17+ devices inPMD3DeviceWrapperwhen selectedMapViewController.swift— Shows iOS 17+ specific error messages with installation instructionsAppDelegate.swift— Removed auto-opening Info view on version updateMain.storyboard— Removed Info preference tabproject.pbxproj— Added new source files; disabledASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLSto fix Xcode 26 build conflicts with existingNSColorextensionsDocumentation
README.md— Added iOS 17+ setup section with prerequisites, instructions, and troubleshooting tableHow It Works
PMD3DeviceWrappertunneldis started as a background daemon (requires one-time admin password)pymobiledevice3 developer dvt simulate-location set --tunnel <UDID> -- <lat> <lon>Testing
http://127.0.0.1:49151Prerequisites for Users
pymobiledevice3(pip install pymobiledevice3)