[CP] New tooling for iOS 17 physical devices (#132283)

Two CP commits in a single PR.

Original PRs: 
https://github.com/flutter/flutter/pull/131865
https://github.com/flutter/flutter/pull/132491
diff --git a/.ci.yaml b/.ci.yaml
index cd162a0..9116008 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -3664,6 +3664,16 @@
         ["devicelab", "ios", "mac"]
       task_name: flutter_gallery_ios__start_up
 
+  - name: Mac_ios flutter_gallery_ios__start_up_xcode_debug
+    recipe: devicelab/devicelab_drone
+    presubmit: false
+    timeout: 60
+    properties:
+      tags: >
+        ["devicelab", "ios", "mac"]
+      task_name: flutter_gallery_ios__start_up_xcode_debug
+    bringup: true
+
   - name: Mac_ios flutter_view_ios__start_up
     recipe: devicelab/devicelab_drone
     presubmit: false
@@ -3731,6 +3741,16 @@
         ["devicelab", "ios", "mac"]
       task_name: integration_ui_ios_driver
 
+  - name: Mac_ios integration_ui_ios_driver_xcode_debug
+    recipe: devicelab/devicelab_drone
+    presubmit: false
+    timeout: 60
+    properties:
+      tags: >
+        ["devicelab", "ios", "mac"]
+      task_name: integration_ui_ios_driver_xcode_debug
+    bringup: true
+
   - name: Mac_ios integration_ui_ios_frame_number
     recipe: devicelab/devicelab_drone
     presubmit: false
diff --git a/TESTOWNERS b/TESTOWNERS
index 49f579e..81fdb38 100644
--- a/TESTOWNERS
+++ b/TESTOWNERS
@@ -164,6 +164,7 @@
 /dev/devicelab/bin/tasks/flutter_gallery__transition_perf_e2e_ios.dart @zanderso @flutter/engine
 /dev/devicelab/bin/tasks/flutter_gallery_ios__compile.dart @vashworth @flutter/engine
 /dev/devicelab/bin/tasks/flutter_gallery_ios__start_up.dart @vashworth @flutter/engine
+/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up_xcode_debug.dart @vashworth @flutter/engine
 /dev/devicelab/bin/tasks/flutter_gallery_ios_sksl_warmup__transition_perf.dart @zanderso @flutter/engine
 /dev/devicelab/bin/tasks/flutter_view_ios__start_up.dart @zanderso @flutter/engine
 /dev/devicelab/bin/tasks/fullscreen_textfield_perf_ios__e2e_summary.dart @cyanglaz @flutter/engine
@@ -174,6 +175,7 @@
 /dev/devicelab/bin/tasks/imagefiltered_transform_animation_perf_ios__timeline_summary.dart @cyanglaz @flutter/engine
 /dev/devicelab/bin/tasks/integration_test_test_ios.dart @cyanglaz @flutter/engine
 /dev/devicelab/bin/tasks/integration_ui_ios_driver.dart @cyanglaz @flutter/tool
+/dev/devicelab/bin/tasks/integration_ui_ios_driver_xcode_debug.dart @vashworth @flutter/tool
 /dev/devicelab/bin/tasks/integration_ui_ios_frame_number.dart @iskakaushik @flutter/engine
 /dev/devicelab/bin/tasks/integration_ui_ios_keyboard_resize.dart @cyanglaz @flutter/engine
 /dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart @cyanglaz @flutter/tool
diff --git a/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up_xcode_debug.dart b/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up_xcode_debug.dart
new file mode 100644
index 0000000..a17c45d
--- /dev/null
+++ b/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up_xcode_debug.dart
@@ -0,0 +1,21 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/framework/devices.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/perf_tests.dart';
+
+Future<void> main() async {
+  // TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
+  // XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+). Use
+  // FORCE_XCODE_DEBUG environment variable to force the use of XcodeDebug
+  // workflow in CI to test from older versions since devicelab has not yet been
+  // updated to iOS 17 and Xcode 15.
+  deviceOperatingSystem = DeviceOperatingSystem.ios;
+  await task(createFlutterGalleryStartupTest(
+    runEnvironment: <String, String>{
+      'FORCE_XCODE_DEBUG': 'true',
+    },
+  ));
+}
diff --git a/dev/devicelab/bin/tasks/integration_ui_ios_driver_xcode_debug.dart b/dev/devicelab/bin/tasks/integration_ui_ios_driver_xcode_debug.dart
new file mode 100644
index 0000000..f51b231
--- /dev/null
+++ b/dev/devicelab/bin/tasks/integration_ui_ios_driver_xcode_debug.dart
@@ -0,0 +1,21 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/framework/devices.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/tasks/integration_tests.dart';
+
+Future<void> main() async {
+  // TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
+  // XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+). Use
+  // FORCE_XCODE_DEBUG environment variable to force the use of XcodeDebug
+  // workflow in CI to test from older versions since devicelab has not yet been
+  // updated to iOS 17 and Xcode 15.
+  deviceOperatingSystem = DeviceOperatingSystem.ios;
+  await task(createEndToEndDriverTest(
+    environment: <String, String>{
+      'FORCE_XCODE_DEBUG': 'true',
+    },
+  ));
+}
diff --git a/dev/devicelab/lib/tasks/integration_tests.dart b/dev/devicelab/lib/tasks/integration_tests.dart
index 0172d57..c06e717 100644
--- a/dev/devicelab/lib/tasks/integration_tests.dart
+++ b/dev/devicelab/lib/tasks/integration_tests.dart
@@ -106,10 +106,11 @@
   ).call;
 }
 
-TaskFunction createEndToEndDriverTest() {
+TaskFunction createEndToEndDriverTest({Map<String, String>? environment}) {
   return DriverTest(
     '${flutterDirectory.path}/dev/integration_tests/ui',
     'lib/driver.dart',
+    environment: environment,
   ).call;
 }
 
@@ -173,6 +174,7 @@
     this.testTarget, {
       this.extraOptions = const <String>[],
       this.deviceIdOverride,
+      this.environment,
     }
   );
 
@@ -180,6 +182,7 @@
   final String testTarget;
   final List<String> extraOptions;
   final String? deviceIdOverride;
+  final Map<String, String>? environment;
 
   Future<TaskResult> call() {
     return inDirectory<TaskResult>(testDirectory, () async {
@@ -202,7 +205,7 @@
         deviceId,
         ...extraOptions,
       ];
-      await flutter('drive', options: options);
+      await flutter('drive', options: options, environment: environment);
 
       return TaskResult.success(null);
     });
diff --git a/dev/devicelab/lib/tasks/perf_tests.dart b/dev/devicelab/lib/tasks/perf_tests.dart
index b35bb41..193e4e5 100644
--- a/dev/devicelab/lib/tasks/perf_tests.dart
+++ b/dev/devicelab/lib/tasks/perf_tests.dart
@@ -232,10 +232,11 @@
   ).run;
 }
 
-TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart'}) {
+TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart', Map<String, String>? runEnvironment}) {
   return StartupTest(
     '${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
     target: target,
+    runEnvironment: runEnvironment,
   ).run;
 }
 
@@ -693,11 +694,17 @@
 
 /// Measure application startup performance.
 class StartupTest {
-  const StartupTest(this.testDirectory, { this.reportMetrics = true, this.target = 'lib/main.dart' });
+  const StartupTest(
+    this.testDirectory, {
+    this.reportMetrics = true,
+    this.target = 'lib/main.dart',
+    this.runEnvironment,
+  });
 
   final String testDirectory;
   final bool reportMetrics;
   final String target;
+  final Map<String, String>? runEnvironment;
 
   Future<TaskResult> run() async {
     return inDirectory<TaskResult>(testDirectory, () async {
@@ -771,18 +778,23 @@
       const int maxFailures = 3;
       int currentFailures = 0;
       for (int i = 0; i < iterations; i += 1) {
-        final int result = await flutter('run', options: <String>[
-          '--no-android-gradle-daemon',
-          '--no-publish-port',
-          '--verbose',
-          '--profile',
-          '--trace-startup',
-          '--target=$target',
-          '-d',
-          device.deviceId,
-          if (applicationBinaryPath != null)
-            '--use-application-binary=$applicationBinaryPath',
-         ], canFail: true);
+        final int result = await flutter(
+          'run',
+          options: <String>[
+            '--no-android-gradle-daemon',
+            '--no-publish-port',
+            '--verbose',
+            '--profile',
+            '--trace-startup',
+            '--target=$target',
+            '-d',
+            device.deviceId,
+            if (applicationBinaryPath != null)
+              '--use-application-binary=$applicationBinaryPath',
+          ],
+          environment: runEnvironment,
+          canFail: true,
+        );
         if (result == 0) {
           final Map<String, dynamic> data = json.decode(
             file('${_testOutputDirectory(testDirectory)}/start_up_info.json').readAsStringSync(),
diff --git a/packages/flutter_tools/bin/xcode_debug.js b/packages/flutter_tools/bin/xcode_debug.js
new file mode 100644
index 0000000..25f16a2
--- /dev/null
+++ b/packages/flutter_tools/bin/xcode_debug.js
@@ -0,0 +1,530 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview OSA Script to interact with Xcode. Functionality includes
+ * checking if a given project is open in Xcode, starting a debug session for
+ * a given project, and stopping a debug session for a given project.
+ */
+
+'use strict';
+
+/**
+ * OSA Script `run` handler that is called when the script is run. When ran
+ * with `osascript`, arguments are passed from the command line to the direct
+ * parameter of the `run` handler as a list of strings.
+ *
+ * @param {?Array<string>=} args_array
+ * @returns {!RunJsonResponse} The validated command.
+ */
+function run(args_array = []) {
+  let args;
+  try {
+    args = new CommandArguments(args_array);
+  } catch (e) {
+    return new RunJsonResponse(false, `Failed to parse arguments: ${e}`).stringify();
+  }
+
+  const xcodeResult = getXcode(args);
+  if (xcodeResult.error != null) {
+    return new RunJsonResponse(false, xcodeResult.error).stringify();
+  }
+  const xcode = xcodeResult.result;
+
+  if (args.command === 'check-workspace-opened') {
+    const result = getWorkspaceDocument(xcode, args);
+    return new RunJsonResponse(result.error == null, result.error).stringify();
+  } else if (args.command === 'debug') {
+    const result = debugApp(xcode, args);
+    return new RunJsonResponse(result.error == null, result.error, result.result).stringify();
+  } else if (args.command === 'stop') {
+    const result = stopApp(xcode, args);
+    return new RunJsonResponse(result.error == null, result.error).stringify();
+  } else {
+    return new RunJsonResponse(false, 'Unknown command').stringify();
+  }
+}
+
+/**
+ * Parsed and validated arguments passed from the command line.
+ */
+class CommandArguments {
+  /**
+   *
+   * @param {!Array<string>} args List of arguments passed from the command line.
+   */
+  constructor(args) {
+    this.command = this.validatedCommand(args[0]);
+
+    const parsedArguments = this.parseArguments(args);
+
+    this.xcodePath = this.validatedStringArgument('--xcode-path', parsedArguments['--xcode-path']);
+    this.projectPath = this.validatedStringArgument('--project-path', parsedArguments['--project-path']);
+    this.workspacePath = this.validatedStringArgument('--workspace-path', parsedArguments['--workspace-path']);
+    this.targetDestinationId = this.validatedStringArgument('--device-id', parsedArguments['--device-id']);
+    this.targetSchemeName = this.validatedStringArgument('--scheme', parsedArguments['--scheme']);
+    this.skipBuilding = this.validatedBoolArgument('--skip-building', parsedArguments['--skip-building']);
+    this.launchArguments = this.validatedJsonArgument('--launch-args', parsedArguments['--launch-args']);
+    this.closeWindowOnStop = this.validatedBoolArgument('--close-window', parsedArguments['--close-window']);
+    this.promptToSaveBeforeClose = this.validatedBoolArgument('--prompt-to-save', parsedArguments['--prompt-to-save']);
+    this.verbose = this.validatedBoolArgument('--verbose', parsedArguments['--verbose']);
+
+    if (this.verbose === true) {
+      console.log(JSON.stringify(this));
+    }
+  }
+
+  /**
+   * Validates the command is available.
+   *
+   * @param {?string} command
+   * @returns {!string} The validated command.
+   * @throws Will throw an error if command is not recognized.
+   */
+  validatedCommand(command) {
+    const allowedCommands = ['check-workspace-opened', 'debug', 'stop'];
+    if (allowedCommands.includes(command) === false) {
+      throw `Unrecognized Command: ${command}`;
+    }
+
+    return command;
+  }
+
+  /**
+   * Validates the flag is allowed for the current command.
+   *
+   * @param {!string} flag
+   * @param {?string} value
+   * @returns {!bool}
+   * @throws Will throw an error if the flag is not allowed for the current
+   *     command and the value is not null, undefined, or empty.
+   */
+  isArgumentAllowed(flag, value) {
+    const allowedArguments = {
+      'common': {
+        '--xcode-path': true,
+        '--project-path': true,
+        '--workspace-path': true,
+        '--verbose': true,
+      },
+      'check-workspace-opened': {},
+      'debug': {
+        '--device-id': true,
+        '--scheme': true,
+        '--skip-building': true,
+        '--launch-args': true,
+      },
+      'stop': {
+        '--close-window': true,
+        '--prompt-to-save': true,
+      },
+    }
+
+    const isAllowed = allowedArguments['common'][flag] === true || allowedArguments[this.command][flag] === true;
+    if (isAllowed === false && (value != null && value !== '')) {
+      throw `The flag ${flag} is not allowed for the command ${this.command}.`;
+    }
+    return isAllowed;
+  }
+
+  /**
+   * Parses the command line arguments into an object.
+   *
+   * @param {!Array<string>} args List of arguments passed from the command line.
+   * @returns {!Object.<string, string>} Object mapping flag to value.
+   * @throws Will throw an error if flag does not begin with '--'.
+   */
+  parseArguments(args) {
+    const valuesPerFlag = {};
+    for (let index = 1; index < args.length; index++) {
+      const entry = args[index];
+      let flag;
+      let value;
+      const splitIndex = entry.indexOf('=');
+      if (splitIndex === -1) {
+        flag = entry;
+        value = args[index + 1];
+
+        // If the flag is allowed for the command, and the next value in the
+        // array is null/undefined or also a flag, treat the flag like a boolean
+        // flag and set the value to 'true'.
+        if (this.isArgumentAllowed(flag) && (value == null || value.startsWith('--'))) {
+          value = 'true';
+        } else {
+          index++;
+        }
+      } else {
+        flag = entry.substring(0, splitIndex);
+        value = entry.substring(splitIndex + 1, entry.length + 1);
+      }
+      if (flag.startsWith('--') === false) {
+        throw `Unrecognized Flag: ${flag}`;
+      }
+
+      valuesPerFlag[flag] = value;
+    }
+    return valuesPerFlag;
+  }
+
+
+  /**
+   * Validates the flag is allowed and `value` is valid. If the flag is not
+   * allowed for the current command, return `null`.
+   *
+   * @param {!string} flag
+   * @param {?string} value
+   * @returns {!string}
+   * @throws Will throw an error if the flag is allowed and `value` is null,
+   *     undefined, or empty.
+   */
+  validatedStringArgument(flag, value) {
+    if (this.isArgumentAllowed(flag, value) === false) {
+      return null;
+    }
+    if (value == null || value === '') {
+      throw `Missing value for ${flag}`;
+    }
+    return value;
+  }
+
+  /**
+   * Validates the flag is allowed, validates `value` is valid, and converts
+   * `value` to a boolean. A `value` of null, undefined, or empty, it will
+   * return true. If the flag is not allowed for the current command, will
+   * return `null`.
+   *
+   * @param {?string} value
+   * @returns {?boolean}
+   * @throws Will throw an error if the flag is allowed and `value` is not
+   *     null, undefined, empty, 'true', or 'false'.
+   */
+  validatedBoolArgument(flag, value) {
+    if (this.isArgumentAllowed(flag, value) === false) {
+      return null;
+    }
+    if (value == null || value === '') {
+      return false;
+    }
+    if (value !== 'true' && value !== 'false') {
+      throw `Invalid value for ${flag}`;
+    }
+    return value === 'true';
+  }
+
+  /**
+   * Validates the flag is allowed, `value` is valid, and parses `value` as JSON.
+   * If the flag is not allowed for the current command, will return `null`.
+   *
+   * @param {!string} flag
+   * @param {?string} value
+   * @returns {!Object}
+   * @throws Will throw an error if the flag is allowed and the value is
+   *     null, undefined, or empty. Will also throw an error if parsing fails.
+   */
+  validatedJsonArgument(flag, value) {
+    if (this.isArgumentAllowed(flag, value) === false) {
+      return null;
+    }
+    if (value == null || value === '') {
+      throw `Missing value for ${flag}`;
+    }
+    try {
+      return JSON.parse(value);
+    } catch (e) {
+      throw `Error parsing ${flag}: ${e}`;
+    }
+  }
+}
+
+/**
+ * Response to return in `run` function.
+ */
+class RunJsonResponse {
+  /**
+   *
+   * @param {!bool} success Whether the command was successful.
+   * @param {?string=} errorMessage Defaults to null.
+   * @param {?DebugResult=} debugResult Curated results from Xcode's debug
+   *     function. Defaults to null.
+   */
+  constructor(success, errorMessage = null, debugResult = null) {
+    this.status = success;
+    this.errorMessage = errorMessage;
+    this.debugResult = debugResult;
+  }
+
+  /**
+   * Converts this object to a JSON string.
+   *
+   * @returns {!string}
+   * @throws Throws an error if conversion fails.
+   */
+  stringify() {
+    return JSON.stringify(this);
+  }
+}
+
+/**
+ * Utility class to return a result along with a potential error.
+ */
+class FunctionResult {
+  /**
+   *
+   * @param {?Object} result
+   * @param {?string=} error Defaults to null.
+   */
+  constructor(result, error = null) {
+    this.result = result;
+    this.error = error;
+  }
+}
+
+/**
+ * Curated results from Xcode's debug function. Mirrors parts of
+ * `scheme action result` from Xcode's Script Editor dictionary.
+ */
+class DebugResult {
+  /**
+   *
+   * @param {!Object} result
+   */
+  constructor(result) {
+    this.completed = result.completed();
+    this.status = result.status();
+    this.errorMessage = result.errorMessage();
+  }
+}
+
+/**
+ * Get the Xcode application from the given path. Since macs can have multiple
+ * Xcode version, we use the path to target the specific Xcode application.
+ * If the Xcode app is not running, return null with an error.
+ *
+ * @param {!CommandArguments} args
+ * @returns {!FunctionResult} Return either an `Application` (Mac Scripting class)
+ *     or null as the `result`.
+ */
+function getXcode(args) {
+  try {
+    const xcode = Application(args.xcodePath);
+    const isXcodeRunning = xcode.running();
+
+    if (isXcodeRunning === false) {
+      return new FunctionResult(null, 'Xcode is not running');
+    }
+
+    return new FunctionResult(xcode);
+  } catch (e) {
+    return new FunctionResult(null, `Failed to get Xcode application: ${e}`);
+  }
+}
+
+/**
+ * After setting the active run destination to the targeted device, uses Xcode
+ * debug function from Mac Scripting for Xcode to install the app on the device
+ * and start a debugging session using the 'run' or 'run without building' scheme
+ * action (depending on `args.skipBuilding`). Waits for the debugging session
+ * to start running.
+ *
+ * @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
+ * @param {!CommandArguments} args
+ * @returns {!FunctionResult} Return either a `DebugResult` or null as the `result`.
+ */
+function debugApp(xcode, args) {
+  const workspaceResult = waitForWorkspaceToLoad(xcode, args);
+  if (workspaceResult.error != null) {
+    return new FunctionResult(null, workspaceResult.error);
+  }
+  const targetWorkspace = workspaceResult.result;
+
+  const destinationResult = getTargetDestination(
+    targetWorkspace,
+    args.targetDestinationId,
+    args.verbose,
+  );
+  if (destinationResult.error != null) {
+    return new FunctionResult(null, destinationResult.error)
+  }
+
+  try {
+    // Documentation from the Xcode Script Editor dictionary indicates that the
+    // `debug` function has a parameter called `runDestinationSpecifier` which
+    // is used to specify which device to debug the app on. It also states that
+    // it should be the same as the xcodebuild -destination specifier. It also
+    // states that if not specified, the `activeRunDestination` is used instead.
+    //
+    // Experimentation has shown that the `runDestinationSpecifier` does not work.
+    // It will always use the `activeRunDestination`. To mitigate this, we set
+    // the `activeRunDestination` to the targeted device prior to starting the debug.
+    targetWorkspace.activeRunDestination = destinationResult.result;
+
+    const actionResult = targetWorkspace.debug({
+      scheme: args.targetSchemeName,
+      skipBuilding: args.skipBuilding,
+      commandLineArguments: args.launchArguments,
+    });
+
+    // Wait until scheme action has started up to a max of 10 minutes.
+    // This does not wait for app to install, launch, or start debug session.
+    // Potential statuses include: not yet started/‌running/‌cancelled/‌failed/‌error occurred/‌succeeded.
+    const checkFrequencyInSeconds = 0.5;
+    const maxWaitInSeconds = 10 * 60; // 10 minutes
+    const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
+    const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
+    for (let i = 0; i < iterations; i++) {
+      if (actionResult.status() !== 'not yet started') {
+        break;
+      }
+      if (args.verbose === true && i % verboseLogInterval === 0) {
+        console.log(`Action result status: ${actionResult.status()}`);
+      }
+      delay(checkFrequencyInSeconds);
+    }
+
+    return new FunctionResult(new DebugResult(actionResult));
+  } catch (e) {
+    return new FunctionResult(null, `Failed to start debugging session: ${e}`);
+  }
+}
+
+/**
+ * Iterates through available run destinations looking for one with a matching
+ * `deviceId`. If device is not found, return null with an error.
+ *
+ * @param {!WorkspaceDocument} targetWorkspace A `WorkspaceDocument` (Xcode Mac
+ *     Scripting class).
+ * @param {!string} deviceId
+ * @param {?bool=} verbose Defaults to false.
+ * @returns {!FunctionResult} Return either a `RunDestination` (Xcode Mac
+ *     Scripting class) or null as the `result`.
+ */
+function getTargetDestination(targetWorkspace, deviceId, verbose = false) {
+  try {
+    for (let destination of targetWorkspace.runDestinations()) {
+      const device = destination.device();
+      if (verbose === true && device != null) {
+        console.log(`Device: ${device.name()} (${device.deviceIdentifier()})`);
+      }
+      if (device != null && device.deviceIdentifier() === deviceId) {
+        return new FunctionResult(destination);
+      }
+    }
+    return new FunctionResult(
+      null,
+      'Unable to find target device. Ensure that the device is paired, ' +
+      'unlocked, connected, and has an iOS version at least as high as the ' +
+      'Minimum Deployment.',
+    );
+  } catch (e) {
+    return new FunctionResult(null, `Failed to get target destination: ${e}`);
+  }
+}
+
+/**
+ * Waits for the workspace to load. If the workspace is not loaded or in the
+ * process of opening, it will wait up to 10 minutes.
+ *
+ * @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
+ * @param {!CommandArguments} args
+ * @returns {!FunctionResult} Return either a `WorkspaceDocument` (Xcode Mac
+ *     Scripting class) or null as the `result`.
+ */
+function waitForWorkspaceToLoad(xcode, args) {
+  try {
+    const checkFrequencyInSeconds = 0.5;
+    const maxWaitInSeconds = 10 * 60; // 10 minutes
+    const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
+    const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
+    for (let i = 0; i < iterations; i++) {
+      // Every 10 seconds, print the list of workspaces if verbose
+      const verbose = args.verbose && i % verboseLogInterval === 0;
+
+      const workspaceResult = getWorkspaceDocument(xcode, args, verbose);
+      if (workspaceResult.error == null) {
+        const document = workspaceResult.result;
+        if (document.loaded() === true) {
+          return new FunctionResult(document, null);
+        }
+      } else if (verbose === true) {
+        console.log(workspaceResult.error);
+      }
+      delay(checkFrequencyInSeconds);
+    }
+    return new FunctionResult(null, 'Timed out waiting for workspace to load');
+  } catch (e) {
+    return new FunctionResult(null, `Failed to wait for workspace to load: ${e}`);
+  }
+}
+
+/**
+ * Gets workspace opened in Xcode matching the projectPath or workspacePath
+ * from the command line arguments. If workspace is not found, return null with
+ * an error.
+ *
+ * @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
+ * @param {!CommandArguments} args
+ * @param {?bool=} verbose Defaults to false.
+ * @returns {!FunctionResult} Return either a `WorkspaceDocument` (Xcode Mac
+ *     Scripting class) or null as the `result`.
+ */
+function getWorkspaceDocument(xcode, args, verbose = false) {
+  const privatePrefix = '/private';
+
+  try {
+    const documents = xcode.workspaceDocuments();
+    for (let document of documents) {
+      const filePath = document.file().toString();
+      if (verbose === true) {
+        console.log(`Workspace: ${filePath}`);
+      }
+      if (filePath === args.projectPath || filePath === args.workspacePath) {
+        return new FunctionResult(document);
+      }
+      // Sometimes when the project is in a temporary directory, it'll be
+      // prefixed with `/private` but the args will not. Remove the
+      // prefix before matching.
+      if (filePath.startsWith(privatePrefix) === true) {
+        const filePathWithoutPrefix = filePath.slice(privatePrefix.length);
+        if (filePathWithoutPrefix === args.projectPath || filePathWithoutPrefix === args.workspacePath) {
+          return new FunctionResult(document);
+        }
+      }
+    }
+  } catch (e) {
+    return new FunctionResult(null, `Failed to get workspace: ${e}`);
+  }
+  return new FunctionResult(null, `Failed to get workspace.`);
+}
+
+/**
+ * Stops all debug sessions in the target workspace.
+ *
+ * @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
+ * @param {!CommandArguments} args
+ * @returns {!FunctionResult} Always returns null as the `result`.
+ */
+function stopApp(xcode, args) {
+  const workspaceResult = getWorkspaceDocument(xcode, args);
+  if (workspaceResult.error != null) {
+    return new FunctionResult(null, workspaceResult.error);
+  }
+  const targetDocument = workspaceResult.result;
+
+  try {
+    targetDocument.stop();
+
+    if (args.closeWindowOnStop === true) {
+      // Wait a couple seconds before closing Xcode, otherwise it'll prompt the
+      // user to stop the app.
+      delay(2);
+
+      targetDocument.close({
+        saving: args.promptToSaveBeforeClose === true ? 'ask' : 'no',
+      });
+    }
+  } catch (e) {
+    return new FunctionResult(null, `Failed to stop app: ${e}`);
+  }
+  return new FunctionResult(null, null);
+}
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index 08cefe5..a5a4f92 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -359,6 +359,7 @@
         platform: globals.platform,
         fileSystem: globals.fs,
         xcodeProjectInterpreter: globals.xcodeProjectInterpreter!,
+        userMessages: globals.userMessages,
       ),
       XCDevice: () => XCDevice(
         processManager: globals.processManager,
@@ -375,6 +376,7 @@
           processManager: globals.processManager,
           dyLdLibEntry: globals.cache.dyLdLibEntry,
         ),
+        fileSystem: globals.fs,
       ),
       XcodeProjectInterpreter: () => XcodeProjectInterpreter(
         logger: globals.logger,
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index a919109..091bac0 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -1156,6 +1156,7 @@
     Map<String, Object?> platformArgs, {
     bool ipv6 = false,
     DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
+    bool isCoreDevice = false,
   }) {
     final String dartVmFlags = computeDartVmFlags(this);
     return <String>[
@@ -1169,7 +1170,10 @@
       if (environmentType == EnvironmentType.simulator && dartVmFlags.isNotEmpty)
         '--dart-flags=$dartVmFlags',
       if (useTestFonts) '--use-test-fonts',
-      if (debuggingEnabled) ...<String>[
+      // Core Devices (iOS 17 devices) are debugged through Xcode so don't
+      // include these flags, which are used to check if the app was launched
+      // via Flutter CLI and `ios-deploy`.
+      if (debuggingEnabled && !isCoreDevice) ...<String>[
         '--enable-checked-mode',
         '--verify-entry-points',
       ],
diff --git a/packages/flutter_tools/lib/src/ios/core_devices.dart b/packages/flutter_tools/lib/src/ios/core_devices.dart
new file mode 100644
index 0000000..aa39adb
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/core_devices.dart
@@ -0,0 +1,854 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/logger.dart';
+import '../base/process.dart';
+import '../convert.dart';
+import '../device.dart';
+import '../macos/xcode.dart';
+
+/// A wrapper around the `devicectl` command line tool.
+///
+/// CoreDevice is a device connectivity stack introduced in Xcode 15. Devices
+/// with iOS 17 or greater are CoreDevices.
+///
+/// `devicectl` (CoreDevice Device Control) is an Xcode CLI tool used for
+/// interacting with CoreDevices.
+class IOSCoreDeviceControl {
+  IOSCoreDeviceControl({
+    required Logger logger,
+    required ProcessManager processManager,
+    required Xcode xcode,
+    required FileSystem fileSystem,
+  })  : _logger = logger,
+        _processUtils = ProcessUtils(logger: logger, processManager: processManager),
+        _xcode = xcode,
+        _fileSystem = fileSystem;
+
+  final Logger _logger;
+  final ProcessUtils _processUtils;
+  final Xcode _xcode;
+  final FileSystem _fileSystem;
+
+  /// When the `--timeout` flag is used with `devicectl`, it must be at
+  /// least 5 seconds. If lower than 5 seconds, `devicectl` will error and not
+  /// run the command.
+  static const int _minimumTimeoutInSeconds = 5;
+
+  /// Executes `devicectl` command to get list of devices. The command will
+  /// likely complete before [timeout] is reached. If [timeout] is reached,
+  /// the command will be stopped as a failure.
+  Future<List<Object?>> _listCoreDevices({
+    Duration timeout = const Duration(seconds: _minimumTimeoutInSeconds),
+  }) async {
+    if (!_xcode.isDevicectlInstalled) {
+      _logger.printError('devicectl is not installed.');
+      return <Object?>[];
+    }
+
+    // Default to minimum timeout if needed to prevent error.
+    Duration validTimeout = timeout;
+    if (timeout.inSeconds < _minimumTimeoutInSeconds) {
+      _logger.printError(
+          'Timeout of ${timeout.inSeconds} seconds is below the minimum timeout value '
+          'for devicectl. Changing the timeout to the minimum value of $_minimumTimeoutInSeconds.');
+      validTimeout = const Duration(seconds: _minimumTimeoutInSeconds);
+    }
+
+    final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
+    final File output = tempDirectory.childFile('core_device_list.json');
+    output.createSync();
+
+    final List<String> command = <String>[
+      ..._xcode.xcrunCommand(),
+      'devicectl',
+      'list',
+      'devices',
+      '--timeout',
+      validTimeout.inSeconds.toString(),
+      '--json-output',
+      output.path,
+    ];
+
+    try {
+      await _processUtils.run(command, throwOnError: true);
+
+      final String stringOutput = output.readAsStringSync();
+      _logger.printTrace(stringOutput);
+
+      try {
+        final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['result'];
+        if (decodeResult is Map<String, Object?>) {
+          final Object? decodeDevices = decodeResult['devices'];
+          if (decodeDevices is List<Object?>) {
+            return decodeDevices;
+          }
+        }
+        _logger.printError('devicectl returned unexpected JSON response: $stringOutput');
+        return <Object?>[];
+      } on FormatException {
+        // We failed to parse the devicectl output, or it returned junk.
+        _logger.printError('devicectl returned non-JSON response: $stringOutput');
+        return <Object?>[];
+      }
+    } on ProcessException catch (err) {
+      _logger.printError('Error executing devicectl: $err');
+      return <Object?>[];
+    } finally {
+      tempDirectory.deleteSync(recursive: true);
+    }
+  }
+
+  Future<List<IOSCoreDevice>> getCoreDevices({
+    Duration timeout = const Duration(seconds: _minimumTimeoutInSeconds),
+  }) async {
+    final List<IOSCoreDevice> devices = <IOSCoreDevice>[];
+
+    final List<Object?> devicesSection = await _listCoreDevices(timeout: timeout);
+    for (final Object? deviceObject in devicesSection) {
+      if (deviceObject is Map<String, Object?>) {
+        devices.add(IOSCoreDevice.fromBetaJson(deviceObject, logger: _logger));
+      }
+    }
+    return devices;
+  }
+
+  /// Executes `devicectl` command to get list of apps installed on the device.
+  /// If [bundleId] is provided, it will only return apps matching the bundle
+  /// identifier exactly.
+  Future<List<Object?>> _listInstalledApps({
+    required String deviceId,
+    String? bundleId,
+  }) async {
+    if (!_xcode.isDevicectlInstalled) {
+      _logger.printError('devicectl is not installed.');
+      return <Object?>[];
+    }
+
+    final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
+    final File output = tempDirectory.childFile('core_device_app_list.json');
+    output.createSync();
+
+    final List<String> command = <String>[
+      ..._xcode.xcrunCommand(),
+      'devicectl',
+      'device',
+      'info',
+      'apps',
+      '--device',
+      deviceId,
+      if (bundleId != null)
+        '--bundle-id',
+        bundleId!,
+      '--json-output',
+      output.path,
+    ];
+
+    try {
+      await _processUtils.run(command, throwOnError: true);
+
+      final String stringOutput = output.readAsStringSync();
+
+      try {
+        final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['result'];
+        if (decodeResult is Map<String, Object?>) {
+          final Object? decodeApps = decodeResult['apps'];
+          if (decodeApps is List<Object?>) {
+            return decodeApps;
+          }
+        }
+        _logger.printError('devicectl returned unexpected JSON response: $stringOutput');
+        return <Object?>[];
+      } on FormatException {
+        // We failed to parse the devicectl output, or it returned junk.
+        _logger.printError('devicectl returned non-JSON response: $stringOutput');
+        return <Object?>[];
+      }
+    } on ProcessException catch (err) {
+      _logger.printError('Error executing devicectl: $err');
+      return <Object?>[];
+    } finally {
+      tempDirectory.deleteSync(recursive: true);
+    }
+  }
+
+  @visibleForTesting
+  Future<List<IOSCoreDeviceInstalledApp>> getInstalledApps({
+    required String deviceId,
+    String? bundleId,
+  }) async {
+    final List<IOSCoreDeviceInstalledApp> apps = <IOSCoreDeviceInstalledApp>[];
+
+    final List<Object?> appsData = await _listInstalledApps(deviceId: deviceId, bundleId: bundleId);
+    for (final Object? appObject in appsData) {
+      if (appObject is Map<String, Object?>) {
+        apps.add(IOSCoreDeviceInstalledApp.fromBetaJson(appObject));
+      }
+    }
+    return apps;
+  }
+
+  Future<bool> isAppInstalled({
+    required String deviceId,
+    required String bundleId,
+  }) async {
+    final List<IOSCoreDeviceInstalledApp> apps = await getInstalledApps(
+      deviceId: deviceId,
+      bundleId: bundleId,
+    );
+    if (apps.isNotEmpty) {
+      return true;
+    }
+    return false;
+  }
+
+  Future<bool> installApp({
+    required String deviceId,
+    required String bundlePath,
+  }) async {
+    if (!_xcode.isDevicectlInstalled) {
+      _logger.printError('devicectl is not installed.');
+      return false;
+    }
+
+    final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
+    final File output = tempDirectory.childFile('install_results.json');
+    output.createSync();
+
+    final List<String> command = <String>[
+      ..._xcode.xcrunCommand(),
+      'devicectl',
+      'device',
+      'install',
+      'app',
+      '--device',
+      deviceId,
+      bundlePath,
+      '--json-output',
+      output.path,
+    ];
+
+    try {
+      await _processUtils.run(command, throwOnError: true);
+      final String stringOutput = output.readAsStringSync();
+
+      try {
+        final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
+        if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
+          return true;
+        }
+        _logger.printError('devicectl returned unexpected JSON response: $stringOutput');
+        return false;
+      } on FormatException {
+        // We failed to parse the devicectl output, or it returned junk.
+        _logger.printError('devicectl returned non-JSON response: $stringOutput');
+        return false;
+      }
+    } on ProcessException catch (err) {
+      _logger.printError('Error executing devicectl: $err');
+      return false;
+    } finally {
+      tempDirectory.deleteSync(recursive: true);
+    }
+  }
+
+  /// Uninstalls the app from the device. Will succeed even if the app is not
+  /// currently installed on the device.
+  Future<bool> uninstallApp({
+    required String deviceId,
+    required String bundleId,
+  }) async {
+    if (!_xcode.isDevicectlInstalled) {
+      _logger.printError('devicectl is not installed.');
+      return false;
+    }
+
+    final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
+    final File output = tempDirectory.childFile('uninstall_results.json');
+    output.createSync();
+
+    final List<String> command = <String>[
+      ..._xcode.xcrunCommand(),
+      'devicectl',
+      'device',
+      'uninstall',
+      'app',
+      '--device',
+      deviceId,
+      bundleId,
+      '--json-output',
+      output.path,
+    ];
+
+    try {
+      await _processUtils.run(command, throwOnError: true);
+      final String stringOutput = output.readAsStringSync();
+
+      try {
+        final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
+        if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
+          return true;
+        }
+        _logger.printError('devicectl returned unexpected JSON response: $stringOutput');
+        return false;
+      } on FormatException {
+        // We failed to parse the devicectl output, or it returned junk.
+        _logger.printError('devicectl returned non-JSON response: $stringOutput');
+        return false;
+      }
+    } on ProcessException catch (err) {
+      _logger.printError('Error executing devicectl: $err');
+      return false;
+    } finally {
+      tempDirectory.deleteSync(recursive: true);
+    }
+  }
+
+  Future<bool> launchApp({
+    required String deviceId,
+    required String bundleId,
+    List<String> launchArguments = const <String>[],
+  }) async {
+    if (!_xcode.isDevicectlInstalled) {
+      _logger.printError('devicectl is not installed.');
+      return false;
+    }
+
+    final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
+    final File output = tempDirectory.childFile('launch_results.json');
+    output.createSync();
+
+    final List<String> command = <String>[
+      ..._xcode.xcrunCommand(),
+      'devicectl',
+      'device',
+      'process',
+      'launch',
+      '--device',
+      deviceId,
+      bundleId,
+      if (launchArguments.isNotEmpty) ...launchArguments,
+      '--json-output',
+      output.path,
+    ];
+
+    try {
+      await _processUtils.run(command, throwOnError: true);
+      final String stringOutput = output.readAsStringSync();
+
+      try {
+        final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
+        if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
+          return true;
+        }
+        _logger.printError('devicectl returned unexpected JSON response: $stringOutput');
+        return false;
+      } on FormatException {
+        // We failed to parse the devicectl output, or it returned junk.
+        _logger.printError('devicectl returned non-JSON response: $stringOutput');
+        return false;
+      }
+    } on ProcessException catch (err) {
+      _logger.printError('Error executing devicectl: $err');
+      return false;
+    } finally {
+      tempDirectory.deleteSync(recursive: true);
+    }
+  }
+}
+
+class IOSCoreDevice {
+  IOSCoreDevice._({
+    required this.capabilities,
+    required this.connectionProperties,
+    required this.deviceProperties,
+    required this.hardwareProperties,
+    required this.coreDeviceIdentifer,
+    required this.visibilityClass,
+  });
+
+  /// Parse JSON from `devicectl list devices --json-output` while it's in beta preview mode.
+  ///
+  /// Example:
+  /// {
+  ///   "capabilities" : [
+  ///   ],
+  ///   "connectionProperties" : {
+  ///   },
+  ///   "deviceProperties" : {
+  ///   },
+  ///   "hardwareProperties" : {
+  ///   },
+  ///   "identifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+  ///   "visibilityClass" : "default"
+  /// }
+  factory IOSCoreDevice.fromBetaJson(
+    Map<String, Object?> data, {
+    required Logger logger,
+  }) {
+    final List<_IOSCoreDeviceCapability> capabilitiesList = <_IOSCoreDeviceCapability>[];
+    if (data['capabilities'] is List<Object?>) {
+      final List<Object?> capabilitiesData = data['capabilities']! as List<Object?>;
+      for (final Object? capabilityData in capabilitiesData) {
+        if (capabilityData != null && capabilityData is Map<String, Object?>) {
+          capabilitiesList.add(_IOSCoreDeviceCapability.fromBetaJson(capabilityData));
+        }
+      }
+    }
+
+    _IOSCoreDeviceConnectionProperties? connectionProperties;
+    if (data['connectionProperties'] is Map<String, Object?>) {
+      final Map<String, Object?> connectionPropertiesData = data['connectionProperties']! as Map<String, Object?>;
+      connectionProperties = _IOSCoreDeviceConnectionProperties.fromBetaJson(
+        connectionPropertiesData,
+        logger: logger,
+      );
+    }
+
+    IOSCoreDeviceProperties? deviceProperties;
+    if (data['deviceProperties'] is Map<String, Object?>) {
+      final Map<String, Object?> devicePropertiesData = data['deviceProperties']! as Map<String, Object?>;
+      deviceProperties = IOSCoreDeviceProperties.fromBetaJson(devicePropertiesData);
+    }
+
+    _IOSCoreDeviceHardwareProperties? hardwareProperties;
+    if (data['hardwareProperties'] is Map<String, Object?>) {
+      final Map<String, Object?> hardwarePropertiesData = data['hardwareProperties']! as Map<String, Object?>;
+      hardwareProperties = _IOSCoreDeviceHardwareProperties.fromBetaJson(
+        hardwarePropertiesData,
+        logger: logger,
+      );
+    }
+
+    return IOSCoreDevice._(
+      capabilities: capabilitiesList,
+      connectionProperties: connectionProperties,
+      deviceProperties: deviceProperties,
+      hardwareProperties: hardwareProperties,
+      coreDeviceIdentifer: data['identifier']?.toString(),
+      visibilityClass: data['visibilityClass']?.toString(),
+    );
+  }
+
+  String? get udid => hardwareProperties?.udid;
+
+  DeviceConnectionInterface? get connectionInterface {
+    final String? transportType = connectionProperties?.transportType;
+    if (transportType != null) {
+      if (transportType.toLowerCase() == 'localnetwork') {
+        return DeviceConnectionInterface.wireless;
+      } else if (transportType.toLowerCase() == 'wired') {
+        return DeviceConnectionInterface.attached;
+      }
+    }
+    return null;
+  }
+
+  @visibleForTesting
+  final List<_IOSCoreDeviceCapability> capabilities;
+
+  @visibleForTesting
+  final _IOSCoreDeviceConnectionProperties? connectionProperties;
+
+  final IOSCoreDeviceProperties? deviceProperties;
+
+  @visibleForTesting
+  final _IOSCoreDeviceHardwareProperties? hardwareProperties;
+
+  final String? coreDeviceIdentifer;
+  final String? visibilityClass;
+}
+
+
+class _IOSCoreDeviceCapability {
+  _IOSCoreDeviceCapability._({
+    required this.featureIdentifier,
+    required this.name,
+  });
+
+  /// Parse `capabilities` section of JSON from `devicectl list devices --json-output`
+  /// while it's in beta preview mode.
+  ///
+  /// Example:
+  /// "capabilities" : [
+  ///   {
+  ///     "featureIdentifier" : "com.apple.coredevice.feature.spawnexecutable",
+  ///     "name" : "Spawn Executable"
+  ///   },
+  ///   {
+  ///     "featureIdentifier" : "com.apple.coredevice.feature.launchapplication",
+  ///     "name" : "Launch Application"
+  ///   }
+  /// ]
+  factory _IOSCoreDeviceCapability.fromBetaJson(Map<String, Object?> data) {
+    return _IOSCoreDeviceCapability._(
+      featureIdentifier: data['featureIdentifier']?.toString(),
+      name: data['name']?.toString(),
+    );
+  }
+
+  final String? featureIdentifier;
+  final String? name;
+}
+
+class _IOSCoreDeviceConnectionProperties {
+  _IOSCoreDeviceConnectionProperties._({
+    required this.authenticationType,
+    required this.isMobileDeviceOnly,
+    required this.lastConnectionDate,
+    required this.localHostnames,
+    required this.pairingState,
+    required this.potentialHostnames,
+    required this.transportType,
+    required this.tunnelIPAddress,
+    required this.tunnelState,
+    required this.tunnelTransportProtocol,
+  });
+
+  /// Parse `connectionProperties` section of JSON from `devicectl list devices --json-output`
+  /// while it's in beta preview mode.
+  ///
+  /// Example:
+  /// "connectionProperties" : {
+  ///   "authenticationType" : "manualPairing",
+  ///   "isMobileDeviceOnly" : false,
+  ///   "lastConnectionDate" : "2023-06-15T15:29:00.082Z",
+  ///   "localHostnames" : [
+  ///     "iPadName.coredevice.local",
+  ///     "00001234-0001234A3C03401E.coredevice.local",
+  ///     "12345BB5-AEDE-4A22-B653-6037262550DD.coredevice.local"
+  ///   ],
+  ///   "pairingState" : "paired",
+  ///   "potentialHostnames" : [
+  ///     "00001234-0001234A3C03401E.coredevice.local",
+  ///     "12345BB5-AEDE-4A22-B653-6037262550DD.coredevice.local"
+  ///   ],
+  ///   "transportType" : "wired",
+  ///   "tunnelIPAddress" : "fdf1:23c4:cd56::1",
+  ///   "tunnelState" : "connected",
+  ///   "tunnelTransportProtocol" : "tcp"
+  /// }
+  factory _IOSCoreDeviceConnectionProperties.fromBetaJson(
+    Map<String, Object?> data, {
+    required Logger logger,
+  }) {
+    List<String>? localHostnames;
+    if (data['localHostnames'] is List<Object?>) {
+      final List<Object?> values = data['localHostnames']! as List<Object?>;
+      try {
+        localHostnames = List<String>.from(values);
+      } on TypeError {
+        logger.printTrace('Error parsing localHostnames value: $values');
+      }
+    }
+
+    List<String>? potentialHostnames;
+    if (data['potentialHostnames'] is List<Object?>) {
+      final List<Object?> values = data['potentialHostnames']! as List<Object?>;
+      try {
+        potentialHostnames = List<String>.from(values);
+      } on TypeError {
+        logger.printTrace('Error parsing potentialHostnames value: $values');
+      }
+    }
+    return _IOSCoreDeviceConnectionProperties._(
+      authenticationType: data['authenticationType']?.toString(),
+      isMobileDeviceOnly: data['isMobileDeviceOnly'] is bool? ? data['isMobileDeviceOnly'] as bool? : null,
+      lastConnectionDate: data['lastConnectionDate']?.toString(),
+      localHostnames: localHostnames,
+      pairingState: data['pairingState']?.toString(),
+      potentialHostnames: potentialHostnames,
+      transportType: data['transportType']?.toString(),
+      tunnelIPAddress: data['tunnelIPAddress']?.toString(),
+      tunnelState: data['tunnelState']?.toString(),
+      tunnelTransportProtocol: data['tunnelTransportProtocol']?.toString(),
+    );
+  }
+
+  final String? authenticationType;
+  final bool? isMobileDeviceOnly;
+  final String? lastConnectionDate;
+  final List<String>? localHostnames;
+  final String? pairingState;
+  final List<String>? potentialHostnames;
+  final String? transportType;
+  final String? tunnelIPAddress;
+  final String? tunnelState;
+  final String? tunnelTransportProtocol;
+}
+
+@visibleForTesting
+class IOSCoreDeviceProperties {
+  IOSCoreDeviceProperties._({
+    required this.bootedFromSnapshot,
+    required this.bootedSnapshotName,
+    required this.bootState,
+    required this.ddiServicesAvailable,
+    required this.developerModeStatus,
+    required this.hasInternalOSBuild,
+    required this.name,
+    required this.osBuildUpdate,
+    required this.osVersionNumber,
+    required this.rootFileSystemIsWritable,
+    required this.screenViewingURL,
+  });
+
+  /// Parse `deviceProperties` section of JSON from `devicectl list devices --json-output`
+  /// while it's in beta preview mode.
+  ///
+  /// Example:
+  /// "deviceProperties" : {
+  ///   "bootedFromSnapshot" : true,
+  ///   "bootedSnapshotName" : "com.apple.os.update-B5336980824124F599FD39FE91016493A74331B09F475250BB010B276FE2439E3DE3537349A3A957D3FF2A4B623B4ECC",
+  ///   "bootState" : "booted",
+  ///   "ddiServicesAvailable" : true,
+  ///   "developerModeStatus" : "enabled",
+  ///   "hasInternalOSBuild" : false,
+  ///   "name" : "iPadName",
+  ///   "osBuildUpdate" : "21A5248v",
+  ///   "osVersionNumber" : "17.0",
+  ///   "rootFileSystemIsWritable" : false,
+  ///   "screenViewingURL" : "coredevice-devices:/viewDeviceByUUID?uuid=123456BB5-AEDE-7A22-B890-1234567890DD"
+  /// }
+  factory IOSCoreDeviceProperties.fromBetaJson(Map<String, Object?> data) {
+    return IOSCoreDeviceProperties._(
+      bootedFromSnapshot: data['bootedFromSnapshot'] is bool? ? data['bootedFromSnapshot'] as bool? : null,
+      bootedSnapshotName: data['bootedSnapshotName']?.toString(),
+      bootState: data['bootState']?.toString(),
+      ddiServicesAvailable: data['ddiServicesAvailable'] is bool? ? data['ddiServicesAvailable'] as bool? : null,
+      developerModeStatus: data['developerModeStatus']?.toString(),
+      hasInternalOSBuild: data['hasInternalOSBuild'] is bool? ? data['hasInternalOSBuild'] as bool? : null,
+      name: data['name']?.toString(),
+      osBuildUpdate: data['osBuildUpdate']?.toString(),
+      osVersionNumber: data['osVersionNumber']?.toString(),
+      rootFileSystemIsWritable: data['rootFileSystemIsWritable'] is bool? ? data['rootFileSystemIsWritable'] as bool? : null,
+      screenViewingURL: data['screenViewingURL']?.toString(),
+    );
+  }
+
+  final bool? bootedFromSnapshot;
+  final String? bootedSnapshotName;
+  final String? bootState;
+  final bool? ddiServicesAvailable;
+  final String? developerModeStatus;
+  final bool? hasInternalOSBuild;
+  final String? name;
+  final String? osBuildUpdate;
+  final String? osVersionNumber;
+  final bool? rootFileSystemIsWritable;
+  final String? screenViewingURL;
+}
+
+class _IOSCoreDeviceHardwareProperties {
+  _IOSCoreDeviceHardwareProperties._({
+    required this.cpuType,
+    required this.deviceType,
+    required this.ecid,
+    required this.hardwareModel,
+    required this.internalStorageCapacity,
+    required this.marketingName,
+    required this.platform,
+    required this.productType,
+    required this.serialNumber,
+    required this.supportedCPUTypes,
+    required this.supportedDeviceFamilies,
+    required this.thinningProductType,
+    required this.udid,
+  });
+
+  /// Parse `hardwareProperties` section of JSON from `devicectl list devices --json-output`
+  /// while it's in beta preview mode.
+  ///
+  /// Example:
+  /// "hardwareProperties" : {
+  ///   "cpuType" : {
+  ///     "name" : "arm64e",
+  ///     "subType" : 2,
+  ///     "type" : 16777228
+  ///   },
+  ///   "deviceType" : "iPad",
+  ///   "ecid" : 12345678903408542,
+  ///   "hardwareModel" : "J617AP",
+  ///   "internalStorageCapacity" : 128000000000,
+  ///   "marketingName" : "iPad Pro (11-inch) (4th generation)\"",
+  ///   "platform" : "iOS",
+  ///   "productType" : "iPad14,3",
+  ///   "serialNumber" : "HC123DHCQV",
+  ///   "supportedCPUTypes" : [
+  ///     {
+  ///       "name" : "arm64e",
+  ///       "subType" : 2,
+  ///       "type" : 16777228
+  ///     },
+  ///     {
+  ///       "name" : "arm64",
+  ///       "subType" : 0,
+  ///       "type" : 16777228
+  ///     }
+  ///   ],
+  ///   "supportedDeviceFamilies" : [
+  ///     1,
+  ///     2
+  ///   ],
+  ///   "thinningProductType" : "iPad14,3-A",
+  ///   "udid" : "00001234-0001234A3C03401E"
+  /// }
+  factory _IOSCoreDeviceHardwareProperties.fromBetaJson(
+    Map<String, Object?> data, {
+    required Logger logger,
+  }) {
+    _IOSCoreDeviceCPUType? cpuType;
+    if (data['cpuType'] is Map<String, Object?>) {
+      cpuType = _IOSCoreDeviceCPUType.fromBetaJson(data['cpuType']! as Map<String, Object?>);
+    }
+
+    List<_IOSCoreDeviceCPUType>? supportedCPUTypes;
+    if (data['supportedCPUTypes'] is List<Object?>) {
+      final List<Object?> values = data['supportedCPUTypes']! as List<Object?>;
+      final List<_IOSCoreDeviceCPUType> cpuTypes = <_IOSCoreDeviceCPUType>[];
+      for (final Object? cpuTypeData in values) {
+        if (cpuTypeData is Map<String, Object?>) {
+          cpuTypes.add(_IOSCoreDeviceCPUType.fromBetaJson(cpuTypeData));
+        }
+      }
+      supportedCPUTypes = cpuTypes;
+    }
+
+    List<int>? supportedDeviceFamilies;
+    if (data['supportedDeviceFamilies'] is List<Object?>) {
+      final List<Object?> values = data['supportedDeviceFamilies']! as List<Object?>;
+      try {
+        supportedDeviceFamilies = List<int>.from(values);
+      } on TypeError {
+        logger.printTrace('Error parsing supportedDeviceFamilies value: $values');
+      }
+    }
+
+    return _IOSCoreDeviceHardwareProperties._(
+      cpuType: cpuType,
+      deviceType: data['deviceType']?.toString(),
+      ecid: data['ecid'] is int? ? data['ecid'] as int? : null,
+      hardwareModel: data['hardwareModel']?.toString(),
+      internalStorageCapacity: data['internalStorageCapacity'] is int? ? data['internalStorageCapacity'] as int? : null,
+      marketingName: data['marketingName']?.toString(),
+      platform: data['platform']?.toString(),
+      productType: data['productType']?.toString(),
+      serialNumber: data['serialNumber']?.toString(),
+      supportedCPUTypes: supportedCPUTypes,
+      supportedDeviceFamilies: supportedDeviceFamilies,
+      thinningProductType: data['thinningProductType']?.toString(),
+      udid: data['udid']?.toString(),
+    );
+  }
+
+  final _IOSCoreDeviceCPUType? cpuType;
+  final String? deviceType;
+  final int? ecid;
+  final String? hardwareModel;
+  final int? internalStorageCapacity;
+  final String? marketingName;
+  final String? platform;
+  final String? productType;
+  final String? serialNumber;
+  final List<_IOSCoreDeviceCPUType>? supportedCPUTypes;
+  final List<int>? supportedDeviceFamilies;
+  final String? thinningProductType;
+  final String? udid;
+}
+
+class _IOSCoreDeviceCPUType {
+  _IOSCoreDeviceCPUType._({
+    this.name,
+    this.subType,
+    this.cpuType,
+  });
+
+  /// Parse `hardwareProperties.cpuType` and `hardwareProperties.supportedCPUTypes`
+  /// sections of JSON from `devicectl list devices --json-output` while it's in beta preview mode.
+  ///
+  /// Example:
+  /// "cpuType" : {
+  ///   "name" : "arm64e",
+  ///   "subType" : 2,
+  ///   "type" : 16777228
+  /// }
+  factory _IOSCoreDeviceCPUType.fromBetaJson(Map<String, Object?> data) {
+    return _IOSCoreDeviceCPUType._(
+      name: data['name']?.toString(),
+      subType: data['subType'] is int? ? data['subType'] as int? : null,
+      cpuType: data['type'] is int? ? data['type'] as int? : null,
+    );
+  }
+
+  final String? name;
+  final int? subType;
+  final int? cpuType;
+}
+
+@visibleForTesting
+class IOSCoreDeviceInstalledApp {
+  IOSCoreDeviceInstalledApp._({
+    required this.appClip,
+    required this.builtByDeveloper,
+    required this.bundleIdentifier,
+    required this.bundleVersion,
+    required this.defaultApp,
+    required this.hidden,
+    required this.internalApp,
+    required this.name,
+    required this.removable,
+    required this.url,
+    required this.version,
+  });
+
+  /// Parse JSON from `devicectl device info apps --json-output` while it's in
+  /// beta preview mode.
+  ///
+  /// Example:
+  /// {
+  ///   "appClip" : false,
+  ///   "builtByDeveloper" : true,
+  ///   "bundleIdentifier" : "com.example.flutterApp",
+  ///   "bundleVersion" : "1",
+  ///   "defaultApp" : false,
+  ///   "hidden" : false,
+  ///   "internalApp" : false,
+  ///   "name" : "Flutter App",
+  ///   "removable" : true,
+  ///   "url" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
+  ///   "version" : "1.0.0"
+  /// }
+  factory IOSCoreDeviceInstalledApp.fromBetaJson(Map<String, Object?> data) {
+    return IOSCoreDeviceInstalledApp._(
+      appClip: data['appClip'] is bool? ? data['appClip'] as bool? : null,
+      builtByDeveloper: data['builtByDeveloper'] is bool? ? data['builtByDeveloper'] as bool? : null,
+      bundleIdentifier: data['bundleIdentifier']?.toString(),
+      bundleVersion: data['bundleVersion']?.toString(),
+      defaultApp: data['defaultApp'] is bool? ? data['defaultApp'] as bool? : null,
+      hidden: data['hidden'] is bool? ? data['hidden'] as bool? : null,
+      internalApp: data['internalApp'] is bool? ? data['internalApp'] as bool? : null,
+      name: data['name']?.toString(),
+      removable: data['removable'] is bool? ? data['removable'] as bool? : null,
+      url: data['url']?.toString(),
+      version: data['version']?.toString(),
+    );
+  }
+
+  final bool? appClip;
+  final bool? builtByDeveloper;
+  final String? bundleIdentifier;
+  final String? bundleVersion;
+  final bool? defaultApp;
+  final bool? hidden;
+  final bool? internalApp;
+  final String? name;
+  final bool? removable;
+  final String? url;
+  final String? version;
+}
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 000529d..85d89ac 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -15,6 +15,7 @@
 import '../base/logger.dart';
 import '../base/os.dart';
 import '../base/platform.dart';
+import '../base/process.dart';
 import '../base/utils.dart';
 import '../base/version.dart';
 import '../build_info.dart';
@@ -28,10 +29,13 @@
 import '../protocol_discovery.dart';
 import '../vmservice.dart';
 import 'application_package.dart';
+import 'core_devices.dart';
 import 'ios_deploy.dart';
 import 'ios_workflow.dart';
 import 'iproxy.dart';
 import 'mac.dart';
+import 'xcode_debug.dart';
+import 'xcodeproj.dart';
 
 class IOSDevices extends PollingDeviceDiscovery {
   IOSDevices({
@@ -263,16 +267,21 @@
     required this.connectionInterface,
     required this.isConnected,
     required this.devModeEnabled,
+    required this.isCoreDevice,
     String? sdkVersion,
     required Platform platform,
     required IOSDeploy iosDeploy,
     required IMobileDevice iMobileDevice,
+    required IOSCoreDeviceControl coreDeviceControl,
+    required XcodeDebug xcodeDebug,
     required IProxy iProxy,
     required Logger logger,
   })
     : _sdkVersion = sdkVersion,
       _iosDeploy = iosDeploy,
       _iMobileDevice = iMobileDevice,
+      _coreDeviceControl = coreDeviceControl,
+      _xcodeDebug = xcodeDebug,
       _iproxy = iProxy,
       _fileSystem = fileSystem,
       _logger = logger,
@@ -294,6 +303,8 @@
   final Logger _logger;
   final Platform _platform;
   final IMobileDevice _iMobileDevice;
+  final IOSCoreDeviceControl _coreDeviceControl;
+  final XcodeDebug _xcodeDebug;
   final IProxy _iproxy;
 
   Version? get sdkVersion {
@@ -324,6 +335,10 @@
   @override
   bool isConnected;
 
+  /// CoreDevice is a device connectivity stack introduced in Xcode 15. Devices
+  /// with iOS 17 or greater are CoreDevices.
+  final bool isCoreDevice;
+
   final Map<IOSApp?, DeviceLogReader> _logReaders = <IOSApp?, DeviceLogReader>{};
 
   DevicePortForwarder? _portForwarder;
@@ -349,10 +364,17 @@
   }) async {
     bool result;
     try {
-      result = await _iosDeploy.isAppInstalled(
-        bundleId: app.id,
-        deviceId: id,
-      );
+      if (isCoreDevice) {
+        result = await _coreDeviceControl.isAppInstalled(
+          bundleId: app.id,
+          deviceId: id,
+        );
+      } else {
+        result = await _iosDeploy.isAppInstalled(
+          bundleId: app.id,
+          deviceId: id,
+        );
+      }
     } on ProcessException catch (e) {
       _logger.printError(e.message);
       return false;
@@ -376,13 +398,20 @@
 
     int installationResult;
     try {
-      installationResult = await _iosDeploy.installApp(
-        deviceId: id,
-        bundlePath: bundle.path,
-        appDeltaDirectory: app.appDeltaDirectory,
-        launchArguments: <String>[],
-        interfaceType: connectionInterface,
-      );
+      if (isCoreDevice) {
+        installationResult = await _coreDeviceControl.installApp(
+          deviceId: id,
+          bundlePath: bundle.path,
+        ) ? 0 : 1;
+      } else {
+        installationResult = await _iosDeploy.installApp(
+          deviceId: id,
+          bundlePath: bundle.path,
+          appDeltaDirectory: app.appDeltaDirectory,
+          launchArguments: <String>[],
+          interfaceType: connectionInterface,
+        );
+      }
     } on ProcessException catch (e) {
       _logger.printError(e.message);
       return false;
@@ -404,10 +433,17 @@
   }) async {
     int uninstallationResult;
     try {
-      uninstallationResult = await _iosDeploy.uninstallApp(
-        deviceId: id,
-        bundleId: app.id,
-      );
+      if (isCoreDevice) {
+        uninstallationResult = await _coreDeviceControl.uninstallApp(
+          deviceId: id,
+          bundleId: app.id,
+        ) ? 0 : 1;
+      } else {
+        uninstallationResult = await _iosDeploy.uninstallApp(
+          deviceId: id,
+          bundleId: app.id,
+        );
+      }
     } on ProcessException catch (e) {
       _logger.printError(e.message);
       return false;
@@ -434,6 +470,7 @@
     bool ipv6 = false,
     String? userIdentifier,
     @visibleForTesting Duration? discoveryTimeout,
+    @visibleForTesting ShutdownHooks? shutdownHooks,
   }) async {
     String? packageId;
     if (isWirelesslyConnected &&
@@ -441,6 +478,18 @@
         debuggingOptions.disablePortPublication) {
       throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
     }
+
+    // TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
+    // XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+).
+    // Force the use of XcodeDebug workflow in CI to test from older versions
+    // since devicelab has not yet been updated to iOS 17 and Xcode 15.
+    bool forceXcodeDebugWorkflow = false;
+    if (debuggingOptions.usingCISystem &&
+        debuggingOptions.debuggingEnabled &&
+        _platform.environment['FORCE_XCODE_DEBUG']?.toLowerCase() == 'true') {
+      forceXcodeDebugWorkflow = true;
+    }
+
     if (!prebuiltApplication) {
       _logger.printTrace('Building ${package.name} for $id');
 
@@ -451,6 +500,7 @@
           targetOverride: mainPath,
           activeArch: cpuArchitecture,
           deviceID: id,
+          isCoreDevice: isCoreDevice || forceXcodeDebugWorkflow,
       );
       if (!buildResult.success) {
         _logger.printError('Could not build the precompiled application for the device.');
@@ -477,6 +527,7 @@
       platformArgs,
       ipv6: ipv6,
       interfaceType: connectionInterface,
+      isCoreDevice: isCoreDevice,
     );
     Status startAppStatus = _logger.startProgress(
       'Installing and launching...',
@@ -516,7 +567,16 @@
           logger: _logger,
         );
       }
-      if (iosDeployDebugger == null) {
+
+      if (isCoreDevice || forceXcodeDebugWorkflow) {
+        installationResult = await _startAppOnCoreDevice(
+          debuggingOptions: debuggingOptions,
+          package: package,
+          launchArguments: launchArguments,
+          discoveryTimeout: discoveryTimeout,
+          shutdownHooks: shutdownHooks ?? globals.shutdownHooks,
+        ) ? 0 : 1;
+      } else if (iosDeployDebugger == null) {
         installationResult = await _iosDeploy.launchApp(
           deviceId: id,
           bundlePath: bundle.path,
@@ -543,10 +603,26 @@
 
       _logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');
 
-      final int defaultTimeout = isWirelesslyConnected ? 45 : 30;
+      final int defaultTimeout;
+      if ((isCoreDevice || forceXcodeDebugWorkflow) && debuggingOptions.debuggingEnabled) {
+        // Core devices with debugging enabled takes longer because this
+        // includes time to install and launch the app on the device.
+        defaultTimeout = isWirelesslyConnected ? 75 : 60;
+      } else if (isWirelesslyConnected) {
+        defaultTimeout = 45;
+      } else {
+        defaultTimeout = 30;
+      }
+
       final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
         _logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');
-
+        if (isCoreDevice && debuggingOptions.debuggingEnabled) {
+          _logger.printError(
+            'Open the Xcode window the project is opened in to ensure the app '
+            'is running. If the app is not running, try selecting "Product > Run" '
+            'to fix the problem.',
+          );
+        }
         // If debugging with a wireless device and the timeout is reached, remind the
         // user to allow local network permissions.
         if (isWirelesslyConnected) {
@@ -564,37 +640,71 @@
 
       Uri? localUri;
       if (isWirelesslyConnected) {
-        // Wait for Dart VM Service to start up.
-        final Uri? serviceURL = await vmServiceDiscovery?.uri;
-        if (serviceURL == null) {
-          await iosDeployDebugger?.stopAndDumpBacktrace();
-          await dispose();
-          return LaunchResult.failed();
-        }
-
-        // If Dart VM Service URL with the device IP is not found within 5 seconds,
-        // change the status message to prompt users to click Allow. Wait 5 seconds because it
-        // should only show this message if they have not already approved the permissions.
-        // MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
-        final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
-          startAppStatus.stop();
-          startAppStatus = _logger.startProgress(
-            'Waiting for approval of local network permissions...',
+        // When using a CoreDevice, device logs are unavailable and therefore
+        // cannot be used to get the Dart VM url. Instead, get the Dart VM
+        // Service by finding services matching the app bundle id and the
+        // device name.
+        //
+        // If not using a CoreDevice, wait for the Dart VM url to be discovered
+        // via logs and then get the Dart VM Service by finding services matching
+        // the app bundle id and the Dart VM port.
+        //
+        // Then in both cases, get the device IP from the Dart VM Service to
+        // construct the Dart VM url using the device IP as the host.
+        if (isCoreDevice) {
+          localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
+            packageId,
+            this,
+            usesIpv6: ipv6,
+            useDeviceIPAsHost: true,
           );
-        });
+        } else {
+          // Wait for Dart VM Service to start up.
+          final Uri? serviceURL = await vmServiceDiscovery?.uri;
+          if (serviceURL == null) {
+            await iosDeployDebugger?.stopAndDumpBacktrace();
+            await dispose();
+            return LaunchResult.failed();
+          }
 
-        // Get Dart VM Service URL with the device IP as the host.
-        localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
-          packageId,
-          this,
-          usesIpv6: ipv6,
-          deviceVmservicePort: serviceURL.port,
-          useDeviceIPAsHost: true,
-        );
+          // If Dart VM Service URL with the device IP is not found within 5 seconds,
+          // change the status message to prompt users to click Allow. Wait 5 seconds because it
+          // should only show this message if they have not already approved the permissions.
+          // MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
+          final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
+            startAppStatus.stop();
+            startAppStatus = _logger.startProgress(
+              'Waiting for approval of local network permissions...',
+            );
+          });
 
-        mDNSLookupTimer.cancel();
+          // Get Dart VM Service URL with the device IP as the host.
+          localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
+            packageId,
+            this,
+            usesIpv6: ipv6,
+            deviceVmservicePort: serviceURL.port,
+            useDeviceIPAsHost: true,
+          );
+
+          mDNSLookupTimer.cancel();
+        }
       } else {
-        localUri = await vmServiceDiscovery?.uri;
+        if ((isCoreDevice || forceXcodeDebugWorkflow) && vmServiceDiscovery != null) {
+          // When searching for the Dart VM url, search for it via ProtocolDiscovery
+          // (device logs) and mDNS simultaneously, since both can be flaky at times.
+          final Future<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
+            packageId,
+            this,
+            usesIpv6: ipv6,
+          );
+          final Future<Uri?> vmUrlFromLogs = vmServiceDiscovery.uri;
+          localUri = await Future.any(
+            <Future<Uri?>>[vmUrlFromMDns, vmUrlFromLogs]
+          );
+        } else {
+          localUri = await vmServiceDiscovery?.uri;
+        }
       }
       timer.cancel();
       if (localUri == null) {
@@ -613,6 +723,110 @@
     }
   }
 
+  /// Starting with Xcode 15 and iOS 17, `ios-deploy` stopped working due to
+  /// the new CoreDevice connectivity stack. Previously, `ios-deploy` was used
+  /// to install the app, launch the app, and start `debugserver`.
+  /// Xcode 15 introduced a new command line tool called `devicectl` that
+  /// includes much of the functionality supplied by `ios-deploy`. However,
+  /// `devicectl` lacks the ability to start a `debugserver` and therefore `ptrace`, which are needed
+  /// for debug mode due to using a JIT Dart VM.
+  ///
+  /// Therefore, when starting an app on a CoreDevice, use `devicectl` when
+  /// debugging is not enabled. Otherwise, use Xcode automation.
+  Future<bool> _startAppOnCoreDevice({
+    required DebuggingOptions debuggingOptions,
+    required IOSApp package,
+    required List<String> launchArguments,
+    required ShutdownHooks shutdownHooks,
+    @visibleForTesting Duration? discoveryTimeout,
+  }) async {
+    if (!debuggingOptions.debuggingEnabled) {
+      // Release mode
+
+      // Install app to device
+      final bool installSuccess = await _coreDeviceControl.installApp(
+        deviceId: id,
+        bundlePath: package.deviceBundlePath,
+      );
+      if (!installSuccess) {
+        return installSuccess;
+      }
+
+      // Launch app to device
+      final bool launchSuccess = await _coreDeviceControl.launchApp(
+        deviceId: id,
+        bundleId: package.id,
+        launchArguments: launchArguments,
+      );
+
+      return launchSuccess;
+    } else {
+      _logger.printStatus(
+        'You may be prompted to give access to control Xcode. Flutter uses Xcode '
+        'to run your app. If access is not allowed, you can change this through '
+        'your Settings > Privacy & Security > Automation.',
+      );
+      final int launchTimeout = isWirelesslyConnected ? 45 : 30;
+      final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: launchTimeout), () {
+        _logger.printError(
+          'Xcode is taking longer than expected to start debugging the app. '
+          'Ensure the project is opened in Xcode.',
+        );
+      });
+
+      XcodeDebugProject debugProject;
+
+      if (package is PrebuiltIOSApp) {
+        debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle(
+          package.deviceBundlePath,
+          templateRenderer: globals.templateRenderer,
+          verboseLogging: _logger.isVerbose,
+        );
+      } else if (package is BuildableIOSApp) {
+        final IosProject project = package.project;
+        final XcodeProjectInfo? projectInfo = await project.projectInfo();
+        if (projectInfo == null) {
+          globals.printError('Xcode project not found.');
+          return false;
+        }
+        if (project.xcodeWorkspace == null) {
+          globals.printError('Unable to get Xcode workspace.');
+          return false;
+        }
+        final String? scheme = projectInfo.schemeFor(debuggingOptions.buildInfo);
+        if (scheme == null) {
+          projectInfo.reportFlavorNotFoundAndExit();
+        }
+
+        debugProject = XcodeDebugProject(
+          scheme: scheme,
+          xcodeProject: project.xcodeProject,
+          xcodeWorkspace: project.xcodeWorkspace!,
+          verboseLogging: _logger.isVerbose,
+        );
+      } else {
+        // This should not happen. Currently, only PrebuiltIOSApp and
+        // BuildableIOSApp extend from IOSApp.
+        _logger.printError('IOSApp type ${package.runtimeType} is not recognized.');
+        return false;
+      }
+
+      final bool debugSuccess = await _xcodeDebug.debugApp(
+        project: debugProject,
+        deviceId: id,
+        launchArguments:launchArguments,
+      );
+      timer.cancel();
+
+      // Kill Xcode on shutdown when running from CI
+      if (debuggingOptions.usingCISystem) {
+        shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true));
+      }
+
+      return debugSuccess;
+    }
+  }
+
   @override
   Future<bool> stopApp(
     ApplicationPackage? app, {
@@ -623,6 +837,9 @@
     if (deployDebugger != null && deployDebugger.debuggerAttached) {
       return deployDebugger.exit();
     }
+    if (_xcodeDebug.debugStarted) {
+      return _xcodeDebug.exit();
+    }
     return false;
   }
 
@@ -669,7 +886,14 @@
   void clearLogs() { }
 
   @override
-  bool get supportsScreenshot => _iMobileDevice.isInstalled;
+  bool get supportsScreenshot {
+    if (isCoreDevice) {
+      // `idevicescreenshot` stopped working with iOS 17 / Xcode 15
+      // (https://github.com/flutter/flutter/issues/128598).
+      return false;
+    }
+    return _iMobileDevice.isInstalled;
+  }
 
   @override
   Future<void> takeScreenshot(File outputFile) async {
@@ -757,14 +981,18 @@
     this._majorSdkVersion,
     this._deviceId,
     this.name,
+    this._isWirelesslyConnected,
+    this._isCoreDevice,
     String appName,
-    bool usingCISystem,
-  ) : // Match for lines for the runner in syslog.
+    bool usingCISystem, {
+    bool forceXcodeDebug = false,
+  }) : // Match for lines for the runner in syslog.
       //
       // iOS 9 format:  Runner[297] <Notice>:
       // iOS 10 format: Runner(Flutter)[297] <Notice>:
       _runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '),
-      _usingCISystem = usingCISystem;
+      _usingCISystem = usingCISystem,
+      _forceXcodeDebug = forceXcodeDebug;
 
   /// Create a new [IOSDeviceLogReader].
   factory IOSDeviceLogReader.create({
@@ -779,8 +1007,11 @@
       device.majorSdkVersion,
       device.id,
       device.name,
+      device.isWirelesslyConnected,
+      device.isCoreDevice,
       appName,
       usingCISystem,
+      forceXcodeDebug: device._platform.environment['FORCE_XCODE_DEBUG']?.toLowerCase() == 'true',
     );
   }
 
@@ -790,6 +1021,8 @@
     bool useSyslog = true,
     bool usingCISystem = false,
     int? majorSdkVersion,
+    bool isWirelesslyConnected = false,
+    bool isCoreDevice = false,
   }) {
     final int sdkVersion;
     if (majorSdkVersion != null) {
@@ -798,16 +1031,22 @@
       sdkVersion = useSyslog ? 12 : 13;
     }
     return IOSDeviceLogReader._(
-      iMobileDevice, sdkVersion, '1234', 'test', 'Runner', usingCISystem);
+      iMobileDevice, sdkVersion, '1234', 'test', isWirelesslyConnected, isCoreDevice, 'Runner', usingCISystem);
   }
 
   @override
   final String name;
   final int _majorSdkVersion;
   final String _deviceId;
+  final bool _isWirelesslyConnected;
+  final bool _isCoreDevice;
   final IMobileDevice _iMobileDevice;
   final bool _usingCISystem;
 
+  // TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
+  /// Whether XcodeDebug workflow is being forced.
+  final bool _forceXcodeDebug;
+
   // Matches a syslog line from the runner.
   RegExp _runnerLineRegex;
 
@@ -832,7 +1071,8 @@
   // Sometimes (race condition?) we try to send a log after the controller has
   // been closed. See https://github.com/flutter/flutter/issues/99021 for more
   // context.
-  void _addToLinesController(String message, IOSDeviceLogSource source) {
+  @visibleForTesting
+  void addToLinesController(String message, IOSDeviceLogSource source) {
     if (!linesController.isClosed) {
       if (_excludeLog(message, source)) {
         return;
@@ -841,30 +1081,53 @@
     }
   }
 
-  /// Used to track messages prefixed with "flutter:" when [useBothLogDeviceReaders]
-  /// is true.
-  final List<String> _streamFlutterMessages = <String>[];
+  /// Used to track messages prefixed with "flutter:" from the fallback log source.
+  final List<String> _fallbackStreamFlutterMessages = <String>[];
 
-  /// When using both `idevicesyslog` and `ios-deploy`, exclude logs with the
-  /// "flutter:" prefix if they have already been added to the stream. This is
-  /// to prevent duplicates from being printed.
-  ///
-  /// If a message does not have the prefix, exclude it if the message's
-  /// source is `idevicesyslog`. This is done because `ios-deploy` and
-  /// `idevicesyslog` often have different prefixes on non-flutter messages
-  /// and are often not critical for CI tests.
+  /// Used to track if a message prefixed with "flutter:" has been received from the primary log.
+  bool primarySourceFlutterLogReceived = false;
+
+  /// There are three potential logging sources: `idevicesyslog`, `ios-deploy`,
+  /// and Unified Logging (Dart VM). When using more than one of these logging
+  /// sources at a time, prefer to use the primary source. However, if the
+  /// primary source is not working, use the fallback.
   bool _excludeLog(String message, IOSDeviceLogSource source) {
-    if (!useBothLogDeviceReaders) {
+    // If no fallback, don't exclude any logs.
+    if (logSources.fallbackSource == null) {
       return false;
     }
-    if (message.startsWith('flutter:')) {
-      if (_streamFlutterMessages.contains(message)) {
+
+    // If log is from primary source, don't exclude it unless the fallback was
+    // quicker and added the message first.
+    if (source == logSources.primarySource) {
+      if (!primarySourceFlutterLogReceived && message.startsWith('flutter:')) {
+        primarySourceFlutterLogReceived = true;
+      }
+
+      // If the message was already added by the fallback, exclude it to
+      // prevent duplicates.
+      final bool foundAndRemoved = _fallbackStreamFlutterMessages.remove(message);
+      if (foundAndRemoved) {
         return true;
       }
-      _streamFlutterMessages.add(message);
-    } else if (source == IOSDeviceLogSource.idevicesyslog) {
+      return false;
+    }
+
+    // If a flutter log was received from the primary source, that means it's
+    // working so don't use any messages from the fallback.
+    if (primarySourceFlutterLogReceived) {
       return true;
     }
+
+    // When using logs from fallbacks, skip any logs not prefixed with "flutter:".
+    // This is done because different sources often have different prefixes for
+    // non-flutter messages, which makes duplicate matching difficult. Also,
+    // non-flutter messages are not critical for CI tests.
+    if (!message.startsWith('flutter:')) {
+      return true;
+    }
+
+    _fallbackStreamFlutterMessages.add(message);
     return false;
   }
 
@@ -887,12 +1150,91 @@
 
   static const int minimumUniversalLoggingSdkVersion = 13;
 
-  /// Listen to Dart VM for logs on iOS 13 or greater.
+  /// Determine the primary and fallback source for device logs.
   ///
-  /// Only send logs to stream if [_iosDeployDebugger] is null or
-  /// the [_iosDeployDebugger] debugger is not attached.
-  Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
+  /// There are three potential logging sources: `idevicesyslog`, `ios-deploy`,
+  /// and Unified Logging (Dart VM).
+  @visibleForTesting
+  _IOSDeviceLogSources get logSources {
+    // `ios-deploy` stopped working with iOS 17 / Xcode 15, so use `idevicesyslog` instead.
+    // However, `idevicesyslog` is sometimes unreliable so use Dart VM as a fallback.
+    // Also, `idevicesyslog` does not work with iOS 17 wireless devices, so use the
+    // Dart VM for wireless devices.
+    if (_isCoreDevice || _forceXcodeDebug) {
+      if (_isWirelesslyConnected) {
+        return _IOSDeviceLogSources(
+          primarySource: IOSDeviceLogSource.unifiedLogging,
+        );
+      }
+      return _IOSDeviceLogSources(
+        primarySource: IOSDeviceLogSource.idevicesyslog,
+        fallbackSource: IOSDeviceLogSource.unifiedLogging,
+      );
+    }
+
+    // Use `idevicesyslog` for iOS 12 or less.
+    // Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
+    // However, from at least iOS 16, it has began working again. It's unclear
+    // why it started working again.
     if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
+      return _IOSDeviceLogSources(
+        primarySource: IOSDeviceLogSource.idevicesyslog,
+      );
+    }
+
+    // Use `idevicesyslog` as a fallback to `ios-deploy` when debugging from
+    // CI system since sometimes `ios-deploy` does not return the device logs:
+    // https://github.com/flutter/flutter/issues/121231
+    if (_usingCISystem && _majorSdkVersion >= 16) {
+      return _IOSDeviceLogSources(
+        primarySource: IOSDeviceLogSource.iosDeploy,
+        fallbackSource: IOSDeviceLogSource.idevicesyslog,
+      );
+    }
+
+    // Use `ios-deploy` to stream logs from the device when the device is not a
+    // CoreDevice and has iOS 13 or greater.
+    // When using `ios-deploy` and the Dart VM, prefer the more complete logs
+    // from the attached debugger, if available.
+    if (connectedVMService != null && (_iosDeployDebugger == null || !_iosDeployDebugger!.debuggerAttached)) {
+      return _IOSDeviceLogSources(
+        primarySource: IOSDeviceLogSource.unifiedLogging,
+        fallbackSource: IOSDeviceLogSource.iosDeploy,
+      );
+    }
+    return _IOSDeviceLogSources(
+      primarySource: IOSDeviceLogSource.iosDeploy,
+      fallbackSource: IOSDeviceLogSource.unifiedLogging,
+    );
+  }
+
+  /// Whether `idevicesyslog` is used as either the primary or fallback source for device logs.
+  @visibleForTesting
+  bool get useSyslogLogging {
+    return logSources.primarySource == IOSDeviceLogSource.idevicesyslog ||
+        logSources.fallbackSource == IOSDeviceLogSource.idevicesyslog;
+  }
+
+  /// Whether the Dart VM is used as either the primary or fallback source for device logs.
+  ///
+  /// Unified Logging only works after the Dart VM has been connected to.
+  @visibleForTesting
+  bool get useUnifiedLogging {
+    return logSources.primarySource == IOSDeviceLogSource.unifiedLogging ||
+        logSources.fallbackSource == IOSDeviceLogSource.unifiedLogging;
+  }
+
+
+  /// Whether `ios-deploy` is used as either the primary or fallback source for device logs.
+  @visibleForTesting
+  bool get useIOSDeployLogging {
+    return logSources.primarySource == IOSDeviceLogSource.iosDeploy ||
+        logSources.fallbackSource == IOSDeviceLogSource.iosDeploy;
+  }
+
+  /// Listen to Dart VM for logs on iOS 13 or greater.
+  Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
+    if (!useUnifiedLogging) {
       return;
     }
     try {
@@ -909,13 +1251,9 @@
     }
 
     void logMessage(vm_service.Event event) {
-      if (_iosDeployDebugger != null && _iosDeployDebugger!.debuggerAttached) {
-        // Prefer the more complete logs from the attached debugger.
-        return;
-      }
       final String message = processVmServiceMessage(event);
       if (message.isNotEmpty) {
-        _addToLinesController(message, IOSDeviceLogSource.unifiedLogging);
+        addToLinesController(message, IOSDeviceLogSource.unifiedLogging);
       }
     }
 
@@ -931,7 +1269,7 @@
   /// Send messages from ios-deploy debugger stream to device log reader stream.
   set debuggerStream(IOSDeployDebugger? debugger) {
     // Logging is gathered from syslog on iOS earlier than 13.
-    if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
+    if (!useIOSDeployLogging) {
       return;
     }
     _iosDeployDebugger = debugger;
@@ -940,7 +1278,7 @@
     }
     // Add the debugger logs to the controller created on initialization.
     _loggingSubscriptions.add(debugger.logLines.listen(
-      (String line) => _addToLinesController(
+      (String line) => addToLinesController(
         _debuggerLineHandler(line),
         IOSDeviceLogSource.iosDeploy,
       ),
@@ -954,22 +1292,10 @@
   // Strip off the logging metadata (leave the category), or just echo the line.
   String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line;
 
-  /// Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system
-  /// since sometimes `ios-deploy` does not return the device logs:
-  /// https://github.com/flutter/flutter/issues/121231
-  @visibleForTesting
-  bool get useBothLogDeviceReaders {
-    return _usingCISystem && _majorSdkVersion >= 16;
-  }
-
   /// Start and listen to idevicesyslog to get device logs for iOS versions
   /// prior to 13 or if [useBothLogDeviceReaders] is true.
   void _listenToSysLog() {
-    // Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
-    // However, from at least iOS 16, it has began working again. It's unclear
-    // why it started working again so only use syslogs for iOS versions prior
-    // to 13 unless [useBothLogDeviceReaders] is true.
-    if (!useBothLogDeviceReaders && _majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
+    if (!useSyslogLogging) {
       return;
     }
     _iMobileDevice.startLogger(_deviceId).then<void>((Process process) {
@@ -982,7 +1308,7 @@
         // When using both log readers, do not close the stream on exit.
         // This is to allow ios-deploy to be the source of authority to close
         // the stream.
-        if (useBothLogDeviceReaders && debuggerStream != null) {
+        if (useSyslogLogging && useIOSDeployLogging && debuggerStream != null) {
           return;
         }
         linesController.close();
@@ -1007,7 +1333,7 @@
     return (String line) {
       if (printing) {
         if (!_anyLineRegex.hasMatch(line)) {
-          _addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog);
+          addToLinesController(decodeSyslog(line), IOSDeviceLogSource.idevicesyslog);
           return;
         }
 
@@ -1019,7 +1345,7 @@
       if (match != null) {
         final String logLine = line.substring(match.end);
         // Only display the log line after the initial device and executable information.
-        _addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog);
+        addToLinesController(decodeSyslog(logLine), IOSDeviceLogSource.idevicesyslog);
         printing = true;
       }
     };
@@ -1044,6 +1370,16 @@
   unifiedLogging,
 }
 
+class _IOSDeviceLogSources {
+  _IOSDeviceLogSources({
+    required this.primarySource,
+    this.fallbackSource,
+  });
+
+  final IOSDeviceLogSource primarySource;
+  final IOSDeviceLogSource? fallbackSource;
+}
+
 /// A [DevicePortForwarder] specialized for iOS usage with iproxy.
 class IOSDevicePortForwarder extends DevicePortForwarder {
 
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index cbd1f89..ecca27b 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -116,6 +116,7 @@
   DarwinArch? activeArch,
   bool codesign = true,
   String? deviceID,
+  bool isCoreDevice = false,
   bool configOnly = false,
   XcodeBuildAction buildAction = XcodeBuildAction.build,
 }) async {
@@ -224,6 +225,7 @@
     project: project,
     targetOverride: targetOverride,
     buildInfo: buildInfo,
+    usingCoreDevice: isCoreDevice,
   );
   await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
   if (configOnly) {
diff --git a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
index 2ef75a2..0e7c42b 100644
--- a/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
+++ b/packages/flutter_tools/lib/src/ios/xcode_build_settings.dart
@@ -35,6 +35,7 @@
   String? targetOverride,
   bool useMacOSConfig = false,
   String? buildDirOverride,
+  bool usingCoreDevice = false,
 }) async {
   final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
     project: project,
@@ -42,6 +43,7 @@
     targetOverride: targetOverride,
     useMacOSConfig: useMacOSConfig,
     buildDirOverride: buildDirOverride,
+    usingCoreDevice: usingCoreDevice,
   );
 
   _updateGeneratedXcodePropertiesFile(
@@ -143,6 +145,7 @@
   String? targetOverride,
   bool useMacOSConfig = false,
   String? buildDirOverride,
+  bool usingCoreDevice = false,
 }) async {
   final List<String> xcodeBuildSettings = <String>[];
 
@@ -170,6 +173,12 @@
   final String buildNumber = parsedBuildNumber(manifest: project.manifest, buildInfo: buildInfo) ?? '1';
   xcodeBuildSettings.add('FLUTTER_BUILD_NUMBER=$buildNumber');
 
+  // CoreDevices in debug and profile mode are launched, but not built, via Xcode.
+  // Set the BUILD_DIR so Xcode knows where to find the app bundle to launch.
+  if (usingCoreDevice && !buildInfo.isRelease) {
+    xcodeBuildSettings.add('BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}');
+  }
+
   final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo;
   if (localEngineInfo != null) {
     final String engineOutPath = localEngineInfo.engineOutPath;
diff --git a/packages/flutter_tools/lib/src/ios/xcode_debug.dart b/packages/flutter_tools/lib/src/ios/xcode_debug.dart
new file mode 100644
index 0000000..e1b5036
--- /dev/null
+++ b/packages/flutter_tools/lib/src/ios/xcode_debug.dart
@@ -0,0 +1,485 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:meta/meta.dart';
+import 'package:process/process.dart';
+
+import '../base/error_handling_io.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/logger.dart';
+import '../base/process.dart';
+import '../base/template.dart';
+import '../convert.dart';
+import '../macos/xcode.dart';
+import '../template.dart';
+
+/// A class to handle interacting with Xcode via OSA (Open Scripting Architecture)
+/// Scripting to debug Flutter applications.
+class XcodeDebug {
+  XcodeDebug({
+    required Logger logger,
+    required ProcessManager processManager,
+    required Xcode xcode,
+    required FileSystem fileSystem,
+  })  : _logger = logger,
+        _processUtils = ProcessUtils(logger: logger, processManager: processManager),
+        _xcode = xcode,
+        _fileSystem = fileSystem;
+
+  final ProcessUtils _processUtils;
+  final Logger _logger;
+  final Xcode _xcode;
+  final FileSystem _fileSystem;
+
+  /// Process to start Xcode's debug action.
+  @visibleForTesting
+  Process? startDebugActionProcess;
+
+  /// Information about the project that is currently being debugged.
+  @visibleForTesting
+  XcodeDebugProject? currentDebuggingProject;
+
+  /// Whether the debug action has been started.
+  bool get debugStarted => currentDebuggingProject != null;
+
+  /// Install, launch, and start a debug session for app through Xcode interface,
+  /// automated by OSA scripting. First checks if the project is opened in
+  /// Xcode. If it isn't, open it with the `open` command.
+  ///
+  /// The OSA script waits until the project is opened and the debug action
+  /// has started. It does not wait for the app to install, launch, or start
+  /// the debug session.
+  Future<bool> debugApp({
+    required XcodeDebugProject project,
+    required String deviceId,
+    required List<String> launchArguments,
+  }) async {
+
+    // If project is not already opened in Xcode, open it.
+    if (!await _isProjectOpenInXcode(project: project)) {
+      final bool openResult = await _openProjectInXcode(xcodeWorkspace: project.xcodeWorkspace);
+      if (!openResult) {
+        return openResult;
+      }
+    }
+
+    currentDebuggingProject = project;
+    StreamSubscription<String>? stdoutSubscription;
+    StreamSubscription<String>? stderrSubscription;
+    try {
+      startDebugActionProcess = await _processUtils.start(
+        <String>[
+          ..._xcode.xcrunCommand(),
+          'osascript',
+          '-l',
+          'JavaScript',
+          _xcode.xcodeAutomationScriptPath,
+          'debug',
+          '--xcode-path',
+          _xcode.xcodeAppPath,
+          '--project-path',
+          project.xcodeProject.path,
+          '--workspace-path',
+          project.xcodeWorkspace.path,
+          '--device-id',
+          deviceId,
+          '--scheme',
+          project.scheme,
+          '--skip-building',
+          '--launch-args',
+          json.encode(launchArguments),
+          if (project.verboseLogging) '--verbose',
+        ],
+      );
+
+      final StringBuffer stdoutBuffer = StringBuffer();
+      stdoutSubscription = startDebugActionProcess!.stdout
+          .transform<String>(utf8.decoder)
+          .transform<String>(const LineSplitter())
+          .listen((String line) {
+            _logger.printTrace(line);
+            stdoutBuffer.write(line);
+      });
+
+      final StringBuffer stderrBuffer = StringBuffer();
+      bool permissionWarningPrinted = false;
+      // console.log from the script are found in the stderr
+      stderrSubscription = startDebugActionProcess!.stderr
+          .transform<String>(utf8.decoder)
+          .transform<String>(const LineSplitter())
+          .listen((String line) {
+            _logger.printTrace('stderr: $line');
+            stderrBuffer.write(line);
+
+          // This error may occur if Xcode automation has not been allowed.
+          // Example: Failed to get workspace: Error: An error occurred.
+          if (!permissionWarningPrinted && line.contains('Failed to get workspace') && line.contains('An error occurred')) {
+            _logger.printError(
+              'There was an error finding the project in Xcode. Ensure permission '
+              'has been given to control Xcode in Settings > Privacy & Security > Automation.',
+            );
+            permissionWarningPrinted = true;
+          }
+      });
+
+      final int exitCode = await startDebugActionProcess!.exitCode.whenComplete(() async {
+        await stdoutSubscription?.cancel();
+        await stderrSubscription?.cancel();
+        startDebugActionProcess = null;
+      });
+
+      if (exitCode != 0) {
+        _logger.printError('Error executing osascript: $exitCode\n$stderrBuffer');
+        return false;
+      }
+
+      final XcodeAutomationScriptResponse? response = parseScriptResponse(
+        stdoutBuffer.toString(),
+      );
+      if (response == null) {
+        return false;
+      }
+      if (response.status == false) {
+        _logger.printError('Error starting debug session in Xcode: ${response.errorMessage}');
+        return false;
+      }
+      if (response.debugResult == null) {
+        _logger.printError('Unable to get debug results from response: $stdoutBuffer');
+        return false;
+      }
+      if (response.debugResult?.status != 'running') {
+        _logger.printError(
+          'Unexpected debug results: \n'
+          '  Status: ${response.debugResult?.status}\n'
+          '  Completed: ${response.debugResult?.completed}\n'
+          '  Error Message: ${response.debugResult?.errorMessage}\n'
+        );
+        return false;
+      }
+      return true;
+    } on ProcessException catch (exception) {
+      _logger.printError('Error executing osascript: $exitCode\n$exception');
+      await stdoutSubscription?.cancel();
+      await stderrSubscription?.cancel();
+      startDebugActionProcess = null;
+
+      return false;
+    }
+  }
+
+  /// Kills [startDebugActionProcess] if it's still running. If [force] is true, it
+  /// will kill all Xcode app processes. Otherwise, it will stop the debug
+  /// session in Xcode. If the project is temporary, it will close the Xcode
+  /// window of the project and then delete the project.
+  Future<bool> exit({
+    bool force = false,
+    @visibleForTesting
+    bool skipDelay = false,
+  }) async {
+    final bool success = (startDebugActionProcess == null) || startDebugActionProcess!.kill();
+
+    if (force) {
+      await _forceExitXcode();
+      if (currentDebuggingProject != null) {
+        final XcodeDebugProject project = currentDebuggingProject!;
+        if (project.isTemporaryProject) {
+          // Only delete if it exists. This is to prevent crashes when racing
+          // with shutdown hooks to delete temporary files.
+          ErrorHandlingFileSystem.deleteIfExists(
+            project.xcodeProject.parent,
+            recursive: true,
+          );
+        }
+        currentDebuggingProject = null;
+      }
+    }
+
+    if (currentDebuggingProject != null) {
+      final XcodeDebugProject project = currentDebuggingProject!;
+      await stopDebuggingApp(
+        project: project,
+        closeXcode: project.isTemporaryProject,
+      );
+
+      if (project.isTemporaryProject) {
+        // Wait a couple seconds before deleting the project. If project is
+        // still opened in Xcode and it's deleted, it will prompt the user to
+        // restore it.
+        if (!skipDelay) {
+          await Future<void>.delayed(const Duration(seconds: 2));
+        }
+
+        try {
+          project.xcodeProject.parent.deleteSync(recursive: true);
+        } on FileSystemException {
+          _logger.printError('Failed to delete temporary Xcode project: ${project.xcodeProject.parent.path}');
+        }
+      }
+      currentDebuggingProject = null;
+    }
+
+    return success;
+  }
+
+  /// Kill all opened Xcode applications.
+  Future<bool> _forceExitXcode() async {
+    final RunResult result = await _processUtils.run(
+      <String>[
+        'killall',
+        '-9',
+        'Xcode',
+      ],
+    );
+
+    if (result.exitCode != 0) {
+      _logger.printError('Error killing Xcode: ${result.exitCode}\n${result.stderr}');
+      return false;
+    }
+    return true;
+  }
+
+  Future<bool> _isProjectOpenInXcode({
+    required XcodeDebugProject project,
+  }) async {
+
+    final RunResult result = await _processUtils.run(
+      <String>[
+        ..._xcode.xcrunCommand(),
+        'osascript',
+        '-l',
+        'JavaScript',
+        _xcode.xcodeAutomationScriptPath,
+        'check-workspace-opened',
+        '--xcode-path',
+        _xcode.xcodeAppPath,
+        '--project-path',
+        project.xcodeProject.path,
+        '--workspace-path',
+        project.xcodeWorkspace.path,
+        if (project.verboseLogging) '--verbose',
+      ],
+    );
+
+    if (result.exitCode != 0) {
+      _logger.printError('Error executing osascript: ${result.exitCode}\n${result.stderr}');
+      return false;
+    }
+
+    final XcodeAutomationScriptResponse? response = parseScriptResponse(result.stdout);
+    if (response == null) {
+      return false;
+    }
+    if (response.status == false) {
+      _logger.printTrace('Error checking if project opened in Xcode: ${response.errorMessage}');
+      return false;
+    }
+    return true;
+  }
+
+  @visibleForTesting
+  XcodeAutomationScriptResponse? parseScriptResponse(String results) {
+    try {
+      final Object decodeResult = json.decode(results) as Object;
+      if (decodeResult is Map<String, Object?>) {
+        final XcodeAutomationScriptResponse response = XcodeAutomationScriptResponse.fromJson(decodeResult);
+        // Status should always be found
+        if (response.status != null) {
+          return response;
+        }
+      }
+      _logger.printError('osascript returned unexpected JSON response: $results');
+      return null;
+    } on FormatException {
+      _logger.printError('osascript returned non-JSON response: $results');
+      return null;
+    }
+  }
+
+  Future<bool> _openProjectInXcode({
+    required Directory xcodeWorkspace,
+  }) async {
+    try {
+      await _processUtils.run(
+        <String>[
+          'open',
+          '-a',
+          _xcode.xcodeAppPath,
+          '-g', // Do not bring the application to the foreground.
+          '-j', // Launches the app hidden.
+          xcodeWorkspace.path
+        ],
+        throwOnError: true,
+      );
+      return true;
+    } on ProcessException catch (error, stackTrace) {
+      _logger.printError('$error', stackTrace: stackTrace);
+    }
+    return false;
+  }
+
+  /// Using OSA Scripting, stop the debug session in Xcode.
+  ///
+  /// If [closeXcode] is true, it will close the Xcode window that has the
+  /// project opened. If [promptToSaveOnClose] is true, it will ask the user if
+  /// they want to save any changes before it closes.
+  Future<bool> stopDebuggingApp({
+    required XcodeDebugProject project,
+    bool closeXcode = false,
+    bool promptToSaveOnClose = false,
+  }) async {
+    final RunResult result = await _processUtils.run(
+      <String>[
+        ..._xcode.xcrunCommand(),
+        'osascript',
+        '-l',
+        'JavaScript',
+        _xcode.xcodeAutomationScriptPath,
+        'stop',
+        '--xcode-path',
+        _xcode.xcodeAppPath,
+        '--project-path',
+        project.xcodeProject.path,
+        '--workspace-path',
+        project.xcodeWorkspace.path,
+        if (closeXcode) '--close-window',
+        if (promptToSaveOnClose) '--prompt-to-save',
+        if (project.verboseLogging) '--verbose',
+      ],
+    );
+
+    if (result.exitCode != 0) {
+      _logger.printError('Error executing osascript: ${result.exitCode}\n${result.stderr}');
+      return false;
+    }
+
+    final XcodeAutomationScriptResponse? response = parseScriptResponse(result.stdout);
+    if (response == null) {
+      return false;
+    }
+    if (response.status == false) {
+      _logger.printError('Error stopping app in Xcode: ${response.errorMessage}');
+      return false;
+    }
+    return true;
+  }
+
+  /// Create a temporary empty Xcode project with the application bundle
+  /// location explicitly set.
+  Future<XcodeDebugProject> createXcodeProjectWithCustomBundle(
+    String deviceBundlePath, {
+    required TemplateRenderer templateRenderer,
+    @visibleForTesting
+    Directory? projectDestination,
+    bool verboseLogging = false,
+  }) async {
+    final Directory tempXcodeProject = projectDestination ?? _fileSystem.systemTempDirectory.createTempSync('flutter_empty_xcode.');
+
+    final Template template = await Template.fromName(
+      _fileSystem.path.join('xcode', 'ios', 'custom_application_bundle'),
+      fileSystem: _fileSystem,
+      templateManifest: null,
+      logger: _logger,
+      templateRenderer: templateRenderer,
+    );
+
+    template.render(
+      tempXcodeProject,
+      <String, Object>{
+        'applicationBundlePath': deviceBundlePath
+      },
+      printStatusWhenWriting: false,
+    );
+
+    return XcodeDebugProject(
+      scheme: 'Runner',
+      xcodeProject: tempXcodeProject.childDirectory('Runner.xcodeproj'),
+      xcodeWorkspace: tempXcodeProject.childDirectory('Runner.xcworkspace'),
+      isTemporaryProject: true,
+      verboseLogging: verboseLogging,
+    );
+  }
+}
+
+@visibleForTesting
+class XcodeAutomationScriptResponse {
+  XcodeAutomationScriptResponse._({
+    this.status,
+    this.errorMessage,
+    this.debugResult,
+  });
+
+  factory XcodeAutomationScriptResponse.fromJson(Map<String, Object?> data) {
+    XcodeAutomationScriptDebugResult? debugResult;
+    if (data['debugResult'] != null && data['debugResult'] is Map<String, Object?>) {
+      debugResult = XcodeAutomationScriptDebugResult.fromJson(
+        data['debugResult']! as Map<String, Object?>,
+      );
+    }
+    return XcodeAutomationScriptResponse._(
+      status: data['status'] is bool? ? data['status'] as bool? : null,
+      errorMessage: data['errorMessage']?.toString(),
+      debugResult: debugResult,
+    );
+  }
+
+  final bool? status;
+  final String? errorMessage;
+  final XcodeAutomationScriptDebugResult? debugResult;
+}
+
+@visibleForTesting
+class XcodeAutomationScriptDebugResult {
+  XcodeAutomationScriptDebugResult._({
+    required this.completed,
+    required this.status,
+    required this.errorMessage,
+  });
+
+  factory XcodeAutomationScriptDebugResult.fromJson(Map<String, Object?> data) {
+    return XcodeAutomationScriptDebugResult._(
+      completed: data['completed'] is bool? ? data['completed'] as bool? : null,
+      status: data['status']?.toString(),
+      errorMessage: data['errorMessage']?.toString(),
+    );
+  }
+
+  /// Whether this scheme action has completed (sucessfully or otherwise). Will
+  /// be false if still running.
+  final bool? completed;
+
+  /// The status of the debug action. Potential statuses include:
+  /// `not yet started`, `‌running`, `‌cancelled`, `‌failed`, `‌error occurred`,
+  /// and `‌succeeded`.
+  ///
+  /// Only the status of `‌running` indicates the debug action has started successfully.
+  /// For example, `‌succeeded` often does not indicate success as if the action fails,
+  /// it will sometimes return `‌succeeded`.
+  final String? status;
+
+  /// When [status] is `‌error occurred`, an error message is provided.
+  /// Otherwise, this will be null.
+  final String? errorMessage;
+}
+
+class XcodeDebugProject {
+  XcodeDebugProject({
+    required this.scheme,
+    required this.xcodeWorkspace,
+    required this.xcodeProject,
+    this.isTemporaryProject = false,
+    this.verboseLogging = false,
+  });
+
+  final String scheme;
+  final Directory xcodeWorkspace;
+  final Directory xcodeProject;
+  final bool isTemporaryProject;
+
+  /// When [verboseLogging] is true, the xcode_debug.js script will log
+  /// additional information via console.log, which is sent to stderr.
+  final bool verboseLogging;
+}
diff --git a/packages/flutter_tools/lib/src/macos/xcdevice.dart b/packages/flutter_tools/lib/src/macos/xcdevice.dart
index c53fdca..a2c66ee 100644
--- a/packages/flutter_tools/lib/src/macos/xcdevice.dart
+++ b/packages/flutter_tools/lib/src/macos/xcdevice.dart
@@ -8,6 +8,7 @@
 import 'package:process/process.dart';
 
 import '../artifacts.dart';
+import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
 import '../base/platform.dart';
@@ -18,10 +19,12 @@
 import '../convert.dart';
 import '../device.dart';
 import '../globals.dart' as globals;
+import '../ios/core_devices.dart';
 import '../ios/devices.dart';
 import '../ios/ios_deploy.dart';
 import '../ios/iproxy.dart';
 import '../ios/mac.dart';
+import '../ios/xcode_debug.dart';
 import '../reporting/reporting.dart';
 import 'xcode.dart';
 
@@ -65,6 +68,10 @@
     required Xcode xcode,
     required Platform platform,
     required IProxy iproxy,
+    required FileSystem fileSystem,
+    @visibleForTesting
+    IOSCoreDeviceControl? coreDeviceControl,
+    XcodeDebug? xcodeDebug,
   }) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
       _logger = logger,
       _iMobileDevice = IMobileDevice(
@@ -80,6 +87,18 @@
         platform: platform,
         processManager: processManager,
       ),
+      _coreDeviceControl = coreDeviceControl ?? IOSCoreDeviceControl(
+        logger: logger,
+        processManager: processManager,
+        xcode: xcode,
+        fileSystem: fileSystem,
+      ),
+      _xcodeDebug = xcodeDebug ?? XcodeDebug(
+        logger: logger,
+        processManager: processManager,
+        xcode: xcode,
+        fileSystem: fileSystem,
+      ),
       _iProxy = iproxy,
       _xcode = xcode {
 
@@ -99,6 +118,8 @@
   final IOSDeploy _iosDeploy;
   final Xcode _xcode;
   final IProxy _iProxy;
+  final IOSCoreDeviceControl _coreDeviceControl;
+  final XcodeDebug _xcodeDebug;
 
   List<Object>? _cachedListResults;
 
@@ -457,6 +478,17 @@
       return const <IOSDevice>[];
     }
 
+    final Map<String, IOSCoreDevice> coreDeviceMap = <String, IOSCoreDevice>{};
+    if (_xcode.isDevicectlInstalled) {
+      final List<IOSCoreDevice> coreDevices = await _coreDeviceControl.getCoreDevices();
+      for (final IOSCoreDevice device in coreDevices) {
+        if (device.udid == null) {
+          continue;
+        }
+        coreDeviceMap[device.udid!] = device;
+      }
+    }
+
     // [
     //  {
     //    "simulator" : true,
@@ -565,11 +597,27 @@
           }
         }
 
+        DeviceConnectionInterface connectionInterface = _interfaceType(device);
+
+        // CoreDevices (devices with iOS 17 and greater) no longer reflect the
+        // correct connection interface or developer mode status in `xcdevice`.
+        // Use `devicectl` to get that information for CoreDevices.
+        final IOSCoreDevice? coreDevice = coreDeviceMap[identifier];
+        if (coreDevice != null) {
+          if (coreDevice.connectionInterface != null) {
+            connectionInterface = coreDevice.connectionInterface!;
+          }
+
+          if (coreDevice.deviceProperties?.developerModeStatus != 'enabled') {
+            devModeEnabled = false;
+          }
+        }
+
         deviceMap[identifier] = IOSDevice(
           identifier,
           name: name,
           cpuArchitecture: _cpuArchitecture(device),
-          connectionInterface: _interfaceType(device),
+          connectionInterface: connectionInterface,
           isConnected: isConnected,
           sdkVersion: sdkVersionString,
           iProxy: _iProxy,
@@ -577,8 +625,11 @@
           logger: _logger,
           iosDeploy: _iosDeploy,
           iMobileDevice: _iMobileDevice,
+          coreDeviceControl: _coreDeviceControl,
+          xcodeDebug: _xcodeDebug,
           platform: globals.platform,
           devModeEnabled: devModeEnabled,
+          isCoreDevice: coreDevice != null,
         );
       }
     }
diff --git a/packages/flutter_tools/lib/src/macos/xcode.dart b/packages/flutter_tools/lib/src/macos/xcode.dart
index 4784013..540f08d 100644
--- a/packages/flutter_tools/lib/src/macos/xcode.dart
+++ b/packages/flutter_tools/lib/src/macos/xcode.dart
@@ -14,8 +14,10 @@
 import '../base/logger.dart';
 import '../base/platform.dart';
 import '../base/process.dart';
+import '../base/user_messages.dart';
 import '../base/version.dart';
 import '../build_info.dart';
+import '../cache.dart';
 import '../ios/xcodeproj.dart';
 
 Version get xcodeRequiredVersion => Version(14, null, null);
@@ -44,9 +46,13 @@
     required Logger logger,
     required FileSystem fileSystem,
     required XcodeProjectInterpreter xcodeProjectInterpreter,
+    required UserMessages userMessages,
+    String? flutterRoot,
   })  : _platform = platform,
         _fileSystem = fileSystem,
         _xcodeProjectInterpreter = xcodeProjectInterpreter,
+        _userMessage = userMessages,
+        _flutterRoot = flutterRoot,
         _processUtils =
             ProcessUtils(logger: logger, processManager: processManager),
         _logger = logger;
@@ -61,6 +67,7 @@
     XcodeProjectInterpreter? xcodeProjectInterpreter,
     Platform? platform,
     FileSystem? fileSystem,
+    String? flutterRoot,
     Logger? logger,
   }) {
     platform ??= FakePlatform(
@@ -72,6 +79,8 @@
       platform: platform,
       processManager: processManager,
       fileSystem: fileSystem ?? MemoryFileSystem.test(),
+      userMessages: UserMessages(),
+      flutterRoot: flutterRoot,
       logger: logger,
       xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
     );
@@ -81,6 +90,8 @@
   final ProcessUtils _processUtils;
   final FileSystem _fileSystem;
   final XcodeProjectInterpreter _xcodeProjectInterpreter;
+  final UserMessages _userMessage;
+  final String? _flutterRoot;
   final Logger _logger;
 
   bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
@@ -101,6 +112,38 @@
     return _xcodeSelectPath;
   }
 
+  String get xcodeAppPath {
+    // If the Xcode Select Path is /Applications/Xcode.app/Contents/Developer,
+    // the path to Xcode App is /Applications/Xcode.app
+
+    final String? pathToXcode = xcodeSelectPath;
+    if (pathToXcode == null || pathToXcode.isEmpty) {
+      throwToolExit(_userMessage.xcodeMissing);
+    }
+    final int index = pathToXcode.indexOf('.app');
+    if (index == -1) {
+      throwToolExit(_userMessage.xcodeMissing);
+    }
+    return pathToXcode.substring(0, index + 4);
+  }
+
+  /// Path to script to automate debugging through Xcode. Used in xcode_debug.dart.
+  /// Located in this file to make it easily overrideable in google3.
+  String get xcodeAutomationScriptPath {
+    final String flutterRoot = _flutterRoot ?? Cache.flutterRoot!;
+    final String flutterToolsAbsolutePath = _fileSystem.path.join(
+      flutterRoot,
+      'packages',
+      'flutter_tools',
+    );
+
+    final String filePath = '$flutterToolsAbsolutePath/bin/xcode_debug.js';
+    if (!_fileSystem.file(filePath).existsSync()) {
+      throwToolExit('Unable to find Xcode automation script at $filePath');
+    }
+    return filePath;
+  }
+
   bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
 
   Version? get currentVersion => _xcodeProjectInterpreter.version;
@@ -150,6 +193,28 @@
     return _isSimctlInstalled ?? false;
   }
 
+  bool? _isDevicectlInstalled;
+
+  /// Verifies that `devicectl` is installed by checking Xcode version and trying
+  /// to run it. `devicectl` is made available in Xcode 15.
+  bool get isDevicectlInstalled {
+    if (_isDevicectlInstalled == null) {
+      try {
+        if (currentVersion == null || currentVersion!.major < 15) {
+          _isDevicectlInstalled = false;
+          return _isDevicectlInstalled!;
+        }
+        final RunResult result = _processUtils.runSync(
+          <String>[...xcrunCommand(), 'devicectl', '--version'],
+        );
+        _isDevicectlInstalled = result.exitCode == 0;
+      } on ProcessException {
+        _isDevicectlInstalled = false;
+      }
+    }
+    return _isDevicectlInstalled ?? false;
+  }
+
   bool get isRequiredVersionSatisfactory {
     final Version? version = currentVersion;
     if (version == null) {
diff --git a/packages/flutter_tools/lib/src/mdns_discovery.dart b/packages/flutter_tools/lib/src/mdns_discovery.dart
index 8219611..da65275 100644
--- a/packages/flutter_tools/lib/src/mdns_discovery.dart
+++ b/packages/flutter_tools/lib/src/mdns_discovery.dart
@@ -130,9 +130,9 @@
   /// The [deviceVmservicePort] parameter must be set to specify which port
   /// to find.
   ///
-  /// [applicationId] and [deviceVmservicePort] are required for launch so that
-  /// if multiple flutter apps are running on different devices, it will
-  /// only match with the device running the desired app.
+  /// [applicationId] and either [deviceVmservicePort] or [deviceName] are
+  /// required for launch so that if multiple flutter apps are running on
+  /// different devices, it will only match with the device running the desired app.
   ///
   /// The [useDeviceIPAsHost] parameter flags whether to get the device IP
   /// and the [ipv6] parameter flags whether to get an iPv6 address
@@ -141,21 +141,27 @@
   /// The [timeout] parameter determines how long to continue to wait for
   /// services to become active.
   ///
-  /// If a Dart VM Service matching the [applicationId] and [deviceVmservicePort]
-  /// cannot be found after the [timeout], it will call [throwToolExit].
+  /// If a Dart VM Service matching the [applicationId] and
+  /// [deviceVmservicePort]/[deviceName] cannot be found before the [timeout]
+  /// is reached, it will call [throwToolExit].
   @visibleForTesting
   Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({
     required String applicationId,
-    required int deviceVmservicePort,
+    int? deviceVmservicePort,
+    String? deviceName,
     bool ipv6 = false,
     bool useDeviceIPAsHost = false,
     Duration timeout = const Duration(minutes: 10),
   }) async {
-    // Query for a specific application and device port.
+    // Either the device port or the device name must be provided.
+    assert(deviceVmservicePort != null || deviceName != null);
+
+    // Query for a specific application matching on either device port or device name.
     return firstMatchingVmService(
       _client,
       applicationId: applicationId,
       deviceVmservicePort: deviceVmservicePort,
+      deviceName: deviceName,
       ipv6: ipv6,
       useDeviceIPAsHost: useDeviceIPAsHost,
       timeout: timeout,
@@ -170,6 +176,7 @@
     MDnsClient client, {
     String? applicationId,
     int? deviceVmservicePort,
+    String? deviceName,
     bool ipv6 = false,
     bool useDeviceIPAsHost = false,
     Duration timeout = const Duration(minutes: 10),
@@ -178,6 +185,7 @@
       client,
       applicationId: applicationId,
       deviceVmservicePort: deviceVmservicePort,
+      deviceName: deviceName,
       ipv6: ipv6,
       useDeviceIPAsHost: useDeviceIPAsHost,
       timeout: timeout,
@@ -193,6 +201,7 @@
     MDnsClient client, {
     String? applicationId,
     int? deviceVmservicePort,
+    String? deviceName,
     bool ipv6 = false,
     bool useDeviceIPAsHost = false,
     required Duration timeout,
@@ -263,6 +272,11 @@
           continue;
         }
 
+        // If deviceName is set, only use records that match it
+        if (deviceName != null && !deviceNameMatchesTargetName(deviceName, srvRecord.target)) {
+          continue;
+        }
+
         // Get the IP address of the device if using the IP as the host.
         InternetAddress? ipAddress;
         if (useDeviceIPAsHost) {
@@ -332,6 +346,15 @@
     }
   }
 
+  @visibleForTesting
+  bool deviceNameMatchesTargetName(String deviceName, String targetName) {
+    // Remove `.local` from the name along with any non-word, non-digit characters.
+    final RegExp cleanedNameRegex = RegExp(r'\.local|\W');
+    final String cleanedDeviceName = deviceName.trim().toLowerCase().replaceAll(cleanedNameRegex, '');
+    final String cleanedTargetName = targetName.toLowerCase().replaceAll(cleanedNameRegex, '');
+    return cleanedDeviceName == cleanedTargetName;
+  }
+
   String _getAuthCode(String txtRecord) {
     const String authCodePrefix = 'authCode=';
     final Iterable<String> matchingRecords =
@@ -354,7 +377,7 @@
   /// When [useDeviceIPAsHost] is true, it will use the device's IP as the
   /// host and will not forward the port.
   ///
-  /// Differs from `getVMServiceUriForLaunch` because it can search for any available Dart VM Service.
+  /// Differs from [getVMServiceUriForLaunch] because it can search for any available Dart VM Service.
   /// Since [applicationId] and [deviceVmservicePort] are optional, it can either look for any service
   /// or a specific service matching [applicationId]/[deviceVmservicePort].
   /// It may find more than one service, which will throw an error listing the found services.
@@ -391,20 +414,22 @@
   /// When [useDeviceIPAsHost] is true, it will use the device's IP as the
   /// host and will not forward the port.
   ///
-  /// Differs from `getVMServiceUriForAttach` because it only searches for a specific service.
-  /// This is enforced by [applicationId] and [deviceVmservicePort] being required.
+  /// Differs from [getVMServiceUriForAttach] because it only searches for a specific service.
+  /// This is enforced by [applicationId] being required and using either the
+  /// [deviceVmservicePort] or the [device]'s name to query.
   Future<Uri?> getVMServiceUriForLaunch(
     String applicationId,
     Device device, {
     bool usesIpv6 = false,
     int? hostVmservicePort,
-    required int deviceVmservicePort,
+    int? deviceVmservicePort,
     bool useDeviceIPAsHost = false,
     Duration timeout = const Duration(minutes: 10),
   }) async {
     final MDnsVmServiceDiscoveryResult? result = await queryForLaunch(
       applicationId: applicationId,
       deviceVmservicePort: deviceVmservicePort,
+      deviceName: deviceVmservicePort == null ? device.name : null,
       ipv6: usesIpv6,
       useDeviceIPAsHost: useDeviceIPAsHost,
       timeout: timeout,
diff --git a/packages/flutter_tools/templates/template_manifest.json b/packages/flutter_tools/templates/template_manifest.json
index d0902d5..3729d89 100644
--- a/packages/flutter_tools/templates/template_manifest.json
+++ b/packages/flutter_tools/templates/template_manifest.json
@@ -339,6 +339,15 @@
         "templates/skeleton/README.md.tmpl",
         "templates/skeleton/test/implementation_test.dart.test.tmpl",
         "templates/skeleton/test/unit_test.dart.tmpl",
-        "templates/skeleton/test/widget_test.dart.tmpl"
+        "templates/skeleton/test/widget_test.dart.tmpl",
+
+        "templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.pbxproj",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/contents.xcworkspacedata",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
+        "templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/xcshareddata/xcschemes/Runner.xcscheme.tmpl"
     ]
 }
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/README.md b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/README.md
new file mode 100644
index 0000000..2b2b697
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/README.md
@@ -0,0 +1,5 @@
+# Template Xcode project with a custom application bundle
+
+This template is an empty Xcode project with a settable application bundle path
+within the `xcscheme`. It is used when debugging a project on a physical iOS 17+
+device via Xcode 15+ when `--use-application-binary` is used.
\ No newline at end of file
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.pbxproj b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.pbxproj
new file mode 100644
index 0000000..8f544ef
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.pbxproj
@@ -0,0 +1,297 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 54;
+	objects = {
+
+/* Begin PBXFileReference section */
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				97C146EF1CF9000F007C117D /* Products */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = Runner;
+			productName = Runner;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		97C146E61CF9000F007C117D /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = YES;
+				LastUpgradeCheck = 1430;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					97C146ED1CF9000F007C117D = {
+						CreatedOnToolsVersion = 7.3.1;
+						LastSwiftMigration = 1100;
+					};
+				};
+			};
+			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+			compatibilityVersion = "Xcode 9.3";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 97C146E51CF9000F007C117D;
+			productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				97C146ED1CF9000F007C117D /* Runner */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		331C807F294A63A400263BE5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		97C147041CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+				CLANG_CXX_LIBRARY = "libc++";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu99;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147031CF9000F007C117D /* Debug */,
+				97C147041CF9000F007C117D /* Release */,
+				249021D3217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				97C147061CF9000F007C117D /* Debug */,
+				97C147071CF9000F007C117D /* Release */,
+				249021D4217E4FDB00AE95B9 /* Profile */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/contents.xcworkspacedata b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/xcshareddata/xcschemes/Runner.xcscheme.tmpl b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/xcshareddata/xcschemes/Runner.xcscheme.tmpl
new file mode 100644
index 0000000..bcca935
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/xcshareddata/xcschemes/Runner.xcscheme.tmpl
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1430"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+               BuildableName = "Runner.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <PathRunnable
+         runnableDebuggingMode = "0"
+         FilePath = "{{applicationBundlePath}}">
+      </PathRunnable>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <PathRunnable
+         runnableDebuggingMode = "0"
+         FilePath = "{{applicationBundlePath}}">
+      </PathRunnable>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata
new file mode 100644
index 0000000..1d526a1
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "group:Runner.xcodeproj">
+   </FileRef>
+</Workspace>
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IDEDidComputeMac32BitWarning</key>
+	<true/>
+</dict>
+</plist>
diff --git a/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/packages/flutter_tools/templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>PreviewsEnabled</key>
+	<false/>
+</dict>
+</plist>
diff --git a/packages/flutter_tools/test/general.shard/device_test.dart b/packages/flutter_tools/test/general.shard/device_test.dart
index 2774536..860a58f 100644
--- a/packages/flutter_tools/test/general.shard/device_test.dart
+++ b/packages/flutter_tools/test/general.shard/device_test.dart
@@ -885,6 +885,26 @@
       );
     });
 
+    testWithoutContext('Get launch arguments for physical CoreDevice with debugging enabled with no launch arguments', () {
+      final DebuggingOptions original = DebuggingOptions.enabled(
+        BuildInfo.debug,
+      );
+
+      final List<String> launchArguments = original.getIOSLaunchArguments(
+        EnvironmentType.physical,
+        null,
+        <String, Object?>{},
+        isCoreDevice: true,
+      );
+
+      expect(
+        launchArguments.join(' '),
+        <String>[
+          '--enable-dart-profiling',
+        ].join(' '),
+      );
+    });
+
     testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () {
       final DebuggingOptions original = DebuggingOptions.enabled(
         BuildInfo.debug,
diff --git a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart
new file mode 100644
index 0000000..d820e1a
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart
@@ -0,0 +1,1949 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file/memory.dart';
+import 'package:file_testing/file_testing.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/version.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+
+import '../../src/common.dart';
+import '../../src/fake_process_manager.dart';
+
+void main() {
+  late MemoryFileSystem fileSystem;
+
+  setUp(() {
+    fileSystem = MemoryFileSystem.test();
+  });
+
+  group('Xcode prior to Core Device Control/Xcode 15', () {
+    late BufferLogger logger;
+    late FakeProcessManager fakeProcessManager;
+    late Xcode xcode;
+    late IOSCoreDeviceControl deviceControl;
+
+    setUp(() {
+      logger = BufferLogger.test();
+      fakeProcessManager = FakeProcessManager.empty();
+      final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter.test(
+        processManager: fakeProcessManager,
+        version: Version(14, 0, 0),
+      );
+      xcode = Xcode.test(
+        processManager: FakeProcessManager.any(),
+        xcodeProjectInterpreter: xcodeProjectInterpreter,
+      );
+      deviceControl = IOSCoreDeviceControl(
+        logger: logger,
+        processManager: fakeProcessManager,
+        xcode: xcode,
+        fileSystem: fileSystem,
+      );
+    });
+
+    group('devicectl is not installed', () {
+      testWithoutContext('fails to get device list', () async {
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl is not installed.'));
+        expect(devices.isEmpty, isTrue);
+      });
+
+      testWithoutContext('fails to install app', () async {
+        final bool status = await deviceControl.installApp(deviceId: 'device-id', bundlePath: '/path/to/bundle');
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl is not installed.'));
+        expect(status, isFalse);
+      });
+
+      testWithoutContext('fails to launch app', () async {
+        final bool status = await deviceControl.launchApp(deviceId: 'device-id', bundleId: 'com.example.flutterApp');
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl is not installed.'));
+        expect(status, isFalse);
+      });
+
+      testWithoutContext('fails to check if app is installed', () async {
+        final bool status = await deviceControl.isAppInstalled(deviceId: 'device-id', bundleId: 'com.example.flutterApp');
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl is not installed.'));
+        expect(status, isFalse);
+      });
+    });
+  });
+
+  group('Core Device Control', () {
+    late BufferLogger logger;
+    late FakeProcessManager fakeProcessManager;
+    late Xcode xcode;
+    late IOSCoreDeviceControl deviceControl;
+
+    setUp(() {
+      logger = BufferLogger.test();
+      fakeProcessManager = FakeProcessManager.empty();
+      xcode = Xcode.test(processManager: FakeProcessManager.any());
+      deviceControl = IOSCoreDeviceControl(
+        logger: logger,
+        processManager: fakeProcessManager,
+        xcode: xcode,
+        fileSystem: fileSystem,
+      );
+    });
+
+    group('install app', () {
+      const String deviceId = 'device-id';
+      const String bundlePath = '/path/to/com.example.flutterApp';
+
+      testWithoutContext('Successful install', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "install",
+      "app",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "build/ios/iphoneos/Runner.app",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json"
+    ],
+    "commandType" : "devicectl.device.install.app",
+    "environment" : {
+
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "installedApplications" : [
+      {
+        "bundleID" : "com.example.bundle",
+        "databaseSequenceNumber" : 1230,
+        "databaseUUID" : "1234A567-D890-1B23-BCF4-D5D67A8D901E",
+        "installationURL" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
+        "launchServicesIdentifier" : "unknown",
+        "options" : {
+
+        }
+      }
+    ]
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('install_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'install',
+            'app',
+            '--device',
+            deviceId,
+            bundlePath,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.installApp(
+          deviceId: deviceId,
+          bundlePath: bundlePath,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(status, true);
+      });
+
+      testWithoutContext('devicectl fails install', () async {
+        const String deviceControlOutput = '''
+{
+  "error" : {
+    "code" : 1005,
+    "domain" : "com.apple.dt.CoreDeviceError",
+    "userInfo" : {
+      "NSLocalizedDescription" : {
+        "string" : "Could not obtain access to one or more requested file system resources because CoreDevice was unable to create bookmark data."
+      },
+      "NSUnderlyingError" : {
+        "error" : {
+          "code" : 260,
+          "domain" : "NSCocoaErrorDomain",
+          "userInfo" : {
+
+          }
+        }
+      }
+    }
+  },
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "install",
+      "app",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "/path/to/app",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json"
+    ],
+    "commandType" : "devicectl.device.install.app",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "failed",
+    "version" : "341"
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('install_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'install',
+            'app',
+            '--device',
+            deviceId,
+            bundlePath,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+          exitCode: 1,
+          stderr: '''
+ERROR: Could not obtain access to one or more requested file system resources because CoreDevice was unable to create bookmark data. (com.apple.dt.CoreDeviceError error 1005.)
+         NSURL = file:///path/to/app
+--------------------------------------------------------------------------------
+ERROR: The file couldn’t be opened because it doesn’t exist. (NSCocoaErrorDomain error 260.)
+'''
+        ));
+
+        final bool status = await deviceControl.installApp(
+          deviceId: deviceId,
+          bundlePath: bundlePath,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('ERROR: Could not obtain access to one or more requested file system'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails install because of unexpected JSON', () async {
+        const String deviceControlOutput = '''
+{
+  "valid_unexpected_json": true
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('install_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'install',
+            'app',
+            '--device',
+            deviceId,
+            bundlePath,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.installApp(
+          deviceId: deviceId,
+          bundlePath: bundlePath,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned unexpected JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails install because of invalid JSON', () async {
+        const String deviceControlOutput = '''
+invalid JSON
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('install_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'install',
+            'app',
+            '--device',
+            deviceId,
+            bundlePath,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.installApp(
+          deviceId: deviceId,
+          bundlePath: bundlePath,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned non-JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+    });
+
+    group('uninstall app', () {
+      const String deviceId = 'device-id';
+      const String bundleId = 'com.example.flutterApp';
+
+      testWithoutContext('Successful uninstall', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "uninstall",
+      "app",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "build/ios/iphoneos/Runner.app",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/uninstall_results.json"
+    ],
+    "commandType" : "devicectl.device.uninstall.app",
+    "environment" : {
+
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "uninstalledApplications" : [
+      {
+        "bundleID" : "com.example.bundle"
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('uninstall_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'uninstall',
+            'app',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.uninstallApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(status, true);
+      });
+
+      testWithoutContext('devicectl fails uninstall', () async {
+        const String deviceControlOutput = '''
+{
+  "error" : {
+    "code" : 1005,
+    "domain" : "com.apple.dt.CoreDeviceError",
+    "userInfo" : {
+      "NSLocalizedDescription" : {
+        "string" : "Could not obtain access to one or more requested file system resources because CoreDevice was unable to create bookmark data."
+      },
+      "NSUnderlyingError" : {
+        "error" : {
+          "code" : 260,
+          "domain" : "NSCocoaErrorDomain",
+          "userInfo" : {
+
+          }
+        }
+      }
+    }
+  },
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "uninstall",
+      "app",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "com.example.flutterApp",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/uninstall_results.json"
+    ],
+    "commandType" : "devicectl.device.uninstall.app",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "failed",
+    "version" : "341"
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('uninstall_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'uninstall',
+            'app',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+          exitCode: 1,
+          stderr: '''
+ERROR: Could not obtain access to one or more requested file system resources because CoreDevice was unable to create bookmark data. (com.apple.dt.CoreDeviceError error 1005.)
+         NSURL = file:///path/to/app
+--------------------------------------------------------------------------------
+ERROR: The file couldn’t be opened because it doesn’t exist. (NSCocoaErrorDomain error 260.)
+'''
+        ));
+
+        final bool status = await deviceControl.uninstallApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('ERROR: Could not obtain access to one or more requested file system'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails uninstall because of unexpected JSON', () async {
+        const String deviceControlOutput = '''
+{
+  "valid_unexpected_json": true
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('uninstall_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'uninstall',
+            'app',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.uninstallApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned unexpected JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails uninstall because of invalid JSON', () async {
+        const String deviceControlOutput = '''
+invalid JSON
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('uninstall_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'uninstall',
+            'app',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.uninstallApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned non-JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+    });
+
+    group('launch app', () {
+      const String deviceId = 'device-id';
+      const String bundleId = 'com.example.flutterApp';
+
+      testWithoutContext('Successful launch without launch args', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "process",
+      "launch",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "com.example.flutterApp",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json"
+    ],
+    "commandType" : "devicectl.device.process.launch",
+    "environment" : {
+
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "launchOptions" : {
+      "activatedWhenStarted" : true,
+      "arguments" : [
+
+      ],
+      "environmentVariables" : {
+        "TERM" : "vt100"
+      },
+      "platformSpecificOptions" : {
+
+      },
+      "startStopped" : false,
+      "terminateExistingInstances" : false,
+      "user" : {
+        "active" : true
+      }
+    },
+    "process" : {
+      "auditToken" : [
+        12345,
+        678
+      ],
+      "executable" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/Runner",
+      "processIdentifier" : 1234
+    }
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('launch_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'process',
+            'launch',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.launchApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(status, true);
+      });
+
+      testWithoutContext('Successful launch with launch args', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "process",
+      "launch",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "com.example.flutterApp",
+      "--arg1",
+      "--arg2",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json"
+    ],
+    "commandType" : "devicectl.device.process.launch",
+    "environment" : {
+
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "launchOptions" : {
+      "activatedWhenStarted" : true,
+      "arguments" : [
+
+      ],
+      "environmentVariables" : {
+        "TERM" : "vt100"
+      },
+      "platformSpecificOptions" : {
+
+      },
+      "startStopped" : false,
+      "terminateExistingInstances" : false,
+      "user" : {
+        "active" : true
+      }
+    },
+    "process" : {
+      "auditToken" : [
+        12345,
+        678
+      ],
+      "executable" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/Runner",
+      "processIdentifier" : 1234
+    }
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('launch_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'process',
+            'launch',
+            '--device',
+            deviceId,
+            bundleId,
+            '--arg1',
+            '--arg2',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.launchApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+          launchArguments: <String>['--arg1', '--arg2'],
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(status, true);
+      });
+
+      testWithoutContext('devicectl fails install', () async {
+        const String deviceControlOutput = '''
+{
+  "error" : {
+    "code" : -10814,
+    "domain" : "NSOSStatusErrorDomain",
+    "userInfo" : {
+      "_LSFunction" : {
+        "string" : "runEvaluator"
+      },
+      "_LSLine" : {
+        "int" : 1608
+      }
+    }
+  },
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "process",
+      "launch",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "com.example.flutterApp",
+      "--json-output",
+      "/var/folders/wq/randompath/T/flutter_tools.rand0/core_devices.rand0/install_results.json"
+    ],
+    "commandType" : "devicectl.device.process.launch",
+    "environment" : {
+
+    },
+    "outcome" : "failed",
+    "version" : "341"
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('launch_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'process',
+            'launch',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+          exitCode: 1,
+          stderr: '''
+ERROR: The operation couldn?t be completed. (OSStatus error -10814.) (NSOSStatusErrorDomain error -10814.)
+    _LSFunction = runEvaluator
+    _LSLine = 1608
+'''
+        ));
+
+        final bool status = await deviceControl.launchApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('ERROR: The operation couldn?t be completed.'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails launch because of unexpected JSON', () async {
+        const String deviceControlOutput = '''
+{
+  "valid_unexpected_json": true
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('launch_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'process',
+            'launch',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+
+        final bool status = await deviceControl.launchApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned unexpected JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails launch because of invalid JSON', () async {
+        const String deviceControlOutput = '''
+invalid JSON
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('launch_results.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'process',
+            'launch',
+            '--device',
+            deviceId,
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.launchApp(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned non-JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+    });
+
+    group('list apps', () {
+      const String deviceId = 'device-id';
+      const String bundleId = 'com.example.flutterApp';
+
+      testWithoutContext('Successfully parses apps', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "info",
+      "apps",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "--bundle-id",
+      "com.example.flutterApp",
+      "--json-output",
+      "apps.txt"
+    ],
+    "commandType" : "devicectl.device.info.apps",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "apps" : [
+      {
+        "appClip" : false,
+        "builtByDeveloper" : true,
+        "bundleIdentifier" : "com.example.flutterApp",
+        "bundleVersion" : "1",
+        "defaultApp" : false,
+        "hidden" : false,
+        "internalApp" : false,
+        "name" : "Bundle",
+        "removable" : true,
+        "url" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
+        "version" : "1.0.0"
+      },
+      {
+        "appClip" : true,
+        "builtByDeveloper" : false,
+        "bundleIdentifier" : "com.example.flutterApp2",
+        "bundleVersion" : "2",
+        "defaultApp" : true,
+        "hidden" : true,
+        "internalApp" : true,
+        "name" : "Bundle 2",
+        "removable" : false,
+        "url" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
+        "version" : "1.0.0"
+      }
+    ],
+    "defaultAppsIncluded" : false,
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "hiddenAppsIncluded" : false,
+    "internalAppsIncluded" : false,
+    "matchingBundleIdentifier" : "com.example.flutterApp",
+    "removableAppsIncluded" : true
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_app_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'info',
+            'apps',
+            '--device',
+            deviceId,
+            '--bundle-id',
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDeviceInstalledApp> apps = await deviceControl.getInstalledApps(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(apps.length, 2);
+
+        expect(apps[0].appClip, isFalse);
+        expect(apps[0].builtByDeveloper, isTrue);
+        expect(apps[0].bundleIdentifier, 'com.example.flutterApp');
+        expect(apps[0].bundleVersion, '1');
+        expect(apps[0].defaultApp, isFalse);
+        expect(apps[0].hidden, isFalse);
+        expect(apps[0].internalApp, isFalse);
+        expect(apps[0].name, 'Bundle');
+        expect(apps[0].removable, isTrue);
+        expect(apps[0].url, 'file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/');
+        expect(apps[0].version, '1.0.0');
+
+        expect(apps[1].appClip, isTrue);
+        expect(apps[1].builtByDeveloper, isFalse);
+        expect(apps[1].bundleIdentifier, 'com.example.flutterApp2');
+        expect(apps[1].bundleVersion, '2');
+        expect(apps[1].defaultApp, isTrue);
+        expect(apps[1].hidden, isTrue);
+        expect(apps[1].internalApp, isTrue);
+        expect(apps[1].name, 'Bundle 2');
+        expect(apps[1].removable, isFalse);
+        expect(apps[1].url, 'file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/');
+        expect(apps[1].version, '1.0.0');
+      });
+
+
+      testWithoutContext('Successfully find installed app', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "info",
+      "apps",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "--bundle-id",
+      "com.example.flutterApp",
+      "--json-output",
+      "apps.txt"
+    ],
+    "commandType" : "devicectl.device.info.apps",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "apps" : [
+      {
+        "appClip" : false,
+        "builtByDeveloper" : true,
+        "bundleIdentifier" : "com.example.flutterApp",
+        "bundleVersion" : "1",
+        "defaultApp" : false,
+        "hidden" : false,
+        "internalApp" : false,
+        "name" : "Bundle",
+        "removable" : true,
+        "url" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
+        "version" : "1.0.0"
+      }
+    ],
+    "defaultAppsIncluded" : false,
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "hiddenAppsIncluded" : false,
+    "internalAppsIncluded" : false,
+    "matchingBundleIdentifier" : "com.example.flutterApp",
+    "removableAppsIncluded" : true
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_app_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'info',
+            'apps',
+            '--device',
+            deviceId,
+            '--bundle-id',
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.isAppInstalled(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(status, true);
+      });
+
+      testWithoutContext('Succeeds but does not find app', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "info",
+      "apps",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "--bundle-id",
+      "com.example.flutterApp",
+      "--json-output",
+      "apps.txt"
+    ],
+    "commandType" : "devicectl.device.info.apps",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "341"
+  },
+  "result" : {
+    "apps" : [
+    ],
+    "defaultAppsIncluded" : false,
+    "deviceIdentifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+    "hiddenAppsIncluded" : false,
+    "internalAppsIncluded" : false,
+    "matchingBundleIdentifier" : "com.example.flutterApp",
+    "removableAppsIncluded" : true
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_app_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'info',
+            'apps',
+            '--device',
+            deviceId,
+            '--bundle-id',
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.isAppInstalled(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, isEmpty);
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('devicectl fails to get apps', () async {
+        const String deviceControlOutput = '''
+{
+  "error" : {
+    "code" : 1000,
+    "domain" : "com.apple.dt.CoreDeviceError",
+    "userInfo" : {
+      "NSLocalizedDescription" : {
+        "string" : "The specified device was not found."
+      }
+    }
+  },
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "device",
+      "info",
+      "apps",
+      "--device",
+      "00001234-0001234A3C03401E",
+      "--bundle-id",
+      "com.example.flutterApp",
+      "--json-output",
+      "apps.txt"
+    ],
+    "commandType" : "devicectl.device.info.apps",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "failed",
+    "version" : "341"
+  }
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_app_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'info',
+            'apps',
+            '--device',
+            deviceId,
+            '--bundle-id',
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+          exitCode: 1,
+          stderr: '''
+ERROR: The specified device was not found. (com.apple.dt.CoreDeviceError error 1000.)
+'''
+        ));
+
+        final bool status = await deviceControl.isAppInstalled(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('ERROR: The specified device was not found.'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails launch because of unexpected JSON', () async {
+        const String deviceControlOutput = '''
+{
+  "valid_unexpected_json": true
+}
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_app_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'info',
+            'apps',
+            '--device',
+            deviceId,
+            '--bundle-id',
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+
+        final bool status = await deviceControl.isAppInstalled(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned unexpected JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+
+      testWithoutContext('fails launch because of invalid JSON', () async {
+        const String deviceControlOutput = '''
+invalid JSON
+''';
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_app_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'device',
+            'info',
+            'apps',
+            '--device',
+            deviceId,
+            '--bundle-id',
+            bundleId,
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final bool status = await deviceControl.isAppInstalled(
+          deviceId: deviceId,
+          bundleId: bundleId,
+        );
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(logger.errorText, contains('devicectl returned non-JSON response'));
+        expect(tempFile, isNot(exists));
+        expect(status, false);
+      });
+    });
+
+    group('list devices', () {
+      testWithoutContext('No devices', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "list",
+      "devices",
+      "--json-output",
+      "core_device_list.json"
+    ],
+    "commandType" : "devicectl.list.devices",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "325.3"
+  },
+  "result" : {
+    "devices" : [
+
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(devices.isEmpty, isTrue);
+      });
+
+      testWithoutContext('All sections parsed', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "list",
+      "devices",
+      "--json-output",
+      "core_device_list.json"
+    ],
+    "commandType" : "devicectl.list.devices",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "325.3"
+  },
+  "result" : {
+    "devices" : [
+      {
+        "capabilities" : [
+        ],
+        "connectionProperties" : {
+        },
+        "deviceProperties" : {
+        },
+        "hardwareProperties" : {
+        },
+        "identifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+        "visibilityClass" : "default"
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(devices.length, 1);
+
+        expect(devices[0].capabilities, isNotNull);
+        expect(devices[0].connectionProperties, isNotNull);
+        expect(devices[0].deviceProperties, isNotNull);
+        expect(devices[0].hardwareProperties, isNotNull);
+        expect(devices[0].coreDeviceIdentifer, '123456BB5-AEDE-7A22-B890-1234567890DD');
+        expect(devices[0].visibilityClass, 'default');
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(tempFile, isNot(exists));
+      });
+
+      testWithoutContext('All sections parsed, device missing sections', () async {
+        const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "list",
+      "devices",
+      "--json-output",
+      "core_device_list.json"
+    ],
+    "commandType" : "devicectl.list.devices",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "325.3"
+  },
+  "result" : {
+    "devices" : [
+      {
+        "identifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+        "visibilityClass" : "default"
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(devices.length, 1);
+
+        expect(devices[0].capabilities, isEmpty);
+        expect(devices[0].connectionProperties, isNull);
+        expect(devices[0].deviceProperties, isNull);
+        expect(devices[0].hardwareProperties, isNull);
+        expect(devices[0].coreDeviceIdentifer, '123456BB5-AEDE-7A22-B890-1234567890DD');
+        expect(devices[0].visibilityClass, 'default');
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(tempFile, isNot(exists));
+      });
+
+      testWithoutContext('capabilities parsed', () async {
+        const String deviceControlOutput = '''
+{
+  "result" : {
+    "devices" : [
+      {
+        "capabilities" : [
+          {
+            "featureIdentifier" : "com.apple.coredevice.feature.spawnexecutable",
+            "name" : "Spawn Executable"
+          },
+          {
+            "featureIdentifier" : "com.apple.coredevice.feature.launchapplication",
+            "name" : "Launch Application"
+          }
+        ]
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(devices.length, 1);
+
+        expect(devices[0].capabilities.length, 2);
+        expect(devices[0].capabilities[0].featureIdentifier, 'com.apple.coredevice.feature.spawnexecutable');
+        expect(devices[0].capabilities[0].name, 'Spawn Executable');
+        expect(devices[0].capabilities[1].featureIdentifier, 'com.apple.coredevice.feature.launchapplication');
+        expect(devices[0].capabilities[1].name, 'Launch Application');
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(tempFile, isNot(exists));
+      });
+
+      testWithoutContext('connectionProperties parsed', () async {
+        const String deviceControlOutput = '''
+{
+  "result" : {
+    "devices" : [
+      {
+        "connectionProperties" : {
+          "authenticationType" : "manualPairing",
+          "isMobileDeviceOnly" : false,
+          "lastConnectionDate" : "2023-06-15T15:29:00.082Z",
+          "localHostnames" : [
+            "Victorias-iPad.coredevice.local",
+            "00001234-0001234A3C03401E.coredevice.local",
+            "123456BB5-AEDE-7A22-B890-1234567890DD.coredevice.local"
+          ],
+          "pairingState" : "paired",
+          "potentialHostnames" : [
+            "00001234-0001234A3C03401E.coredevice.local",
+            "123456BB5-AEDE-7A22-B890-1234567890DD.coredevice.local"
+          ],
+          "transportType" : "wired",
+          "tunnelIPAddress" : "fdf1:23c4:cd56::1",
+          "tunnelState" : "connected",
+          "tunnelTransportProtocol" : "tcp"
+        }
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(devices.length, 1);
+
+        expect(devices[0].connectionProperties?.authenticationType, 'manualPairing');
+        expect(devices[0].connectionProperties?.isMobileDeviceOnly, false);
+        expect(devices[0].connectionProperties?.lastConnectionDate, '2023-06-15T15:29:00.082Z');
+        expect(
+          devices[0].connectionProperties?.localHostnames,
+          <String>[
+            'Victorias-iPad.coredevice.local',
+            '00001234-0001234A3C03401E.coredevice.local',
+            '123456BB5-AEDE-7A22-B890-1234567890DD.coredevice.local',
+          ],
+        );
+        expect(devices[0].connectionProperties?.pairingState, 'paired');
+        expect(devices[0].connectionProperties?.potentialHostnames, <String>[
+          '00001234-0001234A3C03401E.coredevice.local',
+          '123456BB5-AEDE-7A22-B890-1234567890DD.coredevice.local',
+        ]);
+        expect(devices[0].connectionProperties?.transportType, 'wired');
+        expect(devices[0].connectionProperties?.tunnelIPAddress, 'fdf1:23c4:cd56::1');
+        expect(devices[0].connectionProperties?.tunnelState, 'connected');
+        expect(devices[0].connectionProperties?.tunnelTransportProtocol, 'tcp');
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(tempFile, isNot(exists));
+      });
+
+      testWithoutContext('deviceProperties parsed', () async {
+        const String deviceControlOutput = '''
+{
+  "result" : {
+    "devices" : [
+      {
+        "deviceProperties" : {
+          "bootedFromSnapshot" : true,
+          "bootedSnapshotName" : "com.apple.os.update-123456",
+          "bootState" : "booted",
+          "ddiServicesAvailable" : true,
+          "developerModeStatus" : "enabled",
+          "hasInternalOSBuild" : false,
+          "name" : "iPadName",
+          "osBuildUpdate" : "21A5248v",
+          "osVersionNumber" : "17.0",
+          "rootFileSystemIsWritable" : false,
+          "screenViewingURL" : "coredevice-devices:/viewDeviceByUUID?uuid=123456BB5-AEDE-7A22-B890-1234567890DD"
+        }
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(devices.length, 1);
+
+        expect(devices[0].deviceProperties?.bootedFromSnapshot, true);
+        expect(devices[0].deviceProperties?.bootedSnapshotName, 'com.apple.os.update-123456');
+        expect(devices[0].deviceProperties?.bootState, 'booted');
+        expect(devices[0].deviceProperties?.ddiServicesAvailable, true);
+        expect(devices[0].deviceProperties?.developerModeStatus, 'enabled');
+        expect(devices[0].deviceProperties?.hasInternalOSBuild, false);
+        expect(devices[0].deviceProperties?.name, 'iPadName');
+        expect(devices[0].deviceProperties?.osBuildUpdate, '21A5248v');
+        expect(devices[0].deviceProperties?.osVersionNumber, '17.0');
+        expect(devices[0].deviceProperties?.rootFileSystemIsWritable, false);
+        expect(devices[0].deviceProperties?.screenViewingURL, 'coredevice-devices:/viewDeviceByUUID?uuid=123456BB5-AEDE-7A22-B890-1234567890DD');
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(tempFile, isNot(exists));
+      });
+
+      testWithoutContext('hardwareProperties parsed', () async {
+        const String deviceControlOutput = r'''
+{
+  "result" : {
+    "devices" : [
+      {
+        "hardwareProperties" : {
+          "cpuType" : {
+            "name" : "arm64e",
+            "subType" : 2,
+            "type" : 16777228
+          },
+          "deviceType" : "iPad",
+          "ecid" : 12345678903408542,
+          "hardwareModel" : "J617AP",
+          "internalStorageCapacity" : 128000000000,
+          "marketingName" : "iPad Pro (11-inch) (4th generation)\"",
+          "platform" : "iOS",
+          "productType" : "iPad14,3",
+          "serialNumber" : "HC123DHCQV",
+          "supportedCPUTypes" : [
+            {
+              "name" : "arm64e",
+              "subType" : 2,
+              "type" : 16777228
+            },
+            {
+              "name" : "arm64",
+              "subType" : 0,
+              "type" : 16777228
+            }
+          ],
+          "supportedDeviceFamilies" : [
+            1,
+            2
+          ],
+          "thinningProductType" : "iPad14,3-A",
+          "udid" : "00001234-0001234A3C03401E"
+        }
+      }
+    ]
+  }
+}
+''';
+
+        final File tempFile = fileSystem.systemTempDirectory
+            .childDirectory('core_devices.rand0')
+            .childFile('core_device_list.json');
+        fakeProcessManager.addCommand(FakeCommand(
+          command: <String>[
+            'xcrun',
+            'devicectl',
+            'list',
+            'devices',
+            '--timeout',
+            '5',
+            '--json-output',
+            tempFile.path,
+          ],
+          onRun: () {
+            expect(tempFile, exists);
+            tempFile.writeAsStringSync(deviceControlOutput);
+          },
+        ));
+
+        final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+        expect(devices.length, 1);
+
+        expect(devices[0].hardwareProperties?.cpuType, isNotNull);
+        expect(devices[0].hardwareProperties?.cpuType?.name, 'arm64e');
+        expect(devices[0].hardwareProperties?.cpuType?.subType, 2);
+        expect(devices[0].hardwareProperties?.cpuType?.cpuType, 16777228);
+        expect(devices[0].hardwareProperties?.deviceType, 'iPad');
+        expect(devices[0].hardwareProperties?.ecid, 12345678903408542);
+        expect(devices[0].hardwareProperties?.hardwareModel, 'J617AP');
+        expect(devices[0].hardwareProperties?.internalStorageCapacity, 128000000000);
+        expect(devices[0].hardwareProperties?.marketingName, 'iPad Pro (11-inch) (4th generation)"');
+        expect(devices[0].hardwareProperties?.platform, 'iOS');
+        expect(devices[0].hardwareProperties?.productType, 'iPad14,3');
+        expect(devices[0].hardwareProperties?.serialNumber, 'HC123DHCQV');
+        expect(devices[0].hardwareProperties?.supportedCPUTypes, isNotNull);
+        expect(devices[0].hardwareProperties?.supportedCPUTypes?[0].name, 'arm64e');
+        expect(devices[0].hardwareProperties?.supportedCPUTypes?[0].subType, 2);
+        expect(devices[0].hardwareProperties?.supportedCPUTypes?[0].cpuType, 16777228);
+        expect(devices[0].hardwareProperties?.supportedCPUTypes?[1].name, 'arm64');
+        expect(devices[0].hardwareProperties?.supportedCPUTypes?[1].subType, 0);
+        expect(devices[0].hardwareProperties?.supportedCPUTypes?[1].cpuType, 16777228);
+        expect(devices[0].hardwareProperties?.supportedDeviceFamilies, <int>[1, 2]);
+        expect(devices[0].hardwareProperties?.thinningProductType, 'iPad14,3-A');
+
+        expect(devices[0].hardwareProperties?.udid, '00001234-0001234A3C03401E');
+
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(tempFile, isNot(exists));
+      });
+
+      group('Handles errors', () {
+        testWithoutContext('invalid json', () async {
+          const String deviceControlOutput = '''Invalid JSON''';
+
+          final File tempFile = fileSystem.systemTempDirectory
+              .childDirectory('core_devices.rand0')
+              .childFile('core_device_list.json');
+          fakeProcessManager.addCommand(FakeCommand(
+            command: <String>[
+              'xcrun',
+              'devicectl',
+              'list',
+              'devices',
+              '--timeout',
+              '5',
+              '--json-output',
+              tempFile.path,
+            ],
+            onRun: () {
+              expect(tempFile, exists);
+              tempFile.writeAsStringSync(deviceControlOutput);
+            },
+          ));
+
+          final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+          expect(devices.isEmpty, isTrue);
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+          expect(logger.errorText, contains('devicectl returned non-JSON response: Invalid JSON'));
+        });
+
+        testWithoutContext('unexpected json', () async {
+          const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "list",
+      "devices",
+      "--json-output",
+      "device_list.json"
+    ],
+    "commandType" : "devicectl.list.devices",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "325.3"
+  },
+  "result" : [
+
+  ]
+}
+''';
+
+          final File tempFile = fileSystem.systemTempDirectory
+              .childDirectory('core_devices.rand0')
+              .childFile('core_device_list.json');
+          fakeProcessManager.addCommand(FakeCommand(
+            command: <String>[
+              'xcrun',
+              'devicectl',
+              'list',
+              'devices',
+              '--timeout',
+              '5',
+              '--json-output',
+              tempFile.path,
+            ],
+            onRun: () {
+              expect(tempFile, exists);
+              tempFile.writeAsStringSync(deviceControlOutput);
+            },
+          ));
+
+          final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices();
+          expect(devices.isEmpty, isTrue);
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+          expect(logger.errorText, contains('devicectl returned unexpected JSON response:'));
+        });
+
+        testWithoutContext('When timeout is below minimum, default to minimum', () async {
+          const String deviceControlOutput = '''
+{
+  "info" : {
+    "arguments" : [
+      "devicectl",
+      "list",
+      "devices",
+      "--json-output",
+      "core_device_list.json"
+    ],
+    "commandType" : "devicectl.list.devices",
+    "environment" : {
+      "TERM" : "xterm-256color"
+    },
+    "outcome" : "success",
+    "version" : "325.3"
+  },
+  "result" : {
+    "devices" : [
+      {
+        "identifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
+        "visibilityClass" : "default"
+      }
+    ]
+  }
+}
+''';
+
+          final File tempFile = fileSystem.systemTempDirectory
+              .childDirectory('core_devices.rand0')
+              .childFile('core_device_list.json');
+          fakeProcessManager.addCommand(FakeCommand(
+            command: <String>[
+              'xcrun',
+              'devicectl',
+              'list',
+              'devices',
+              '--timeout',
+              '5',
+              '--json-output',
+              tempFile.path,
+            ],
+            onRun: () {
+              expect(tempFile, exists);
+              tempFile.writeAsStringSync(deviceControlOutput);
+            },
+          ));
+
+          final List<IOSCoreDevice> devices = await deviceControl.getCoreDevices(
+            timeout: const Duration(seconds: 2),
+          );
+          expect(devices.isNotEmpty, isTrue);
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+          expect(
+            logger.errorText,
+            contains('Timeout of 2 seconds is below the minimum timeout value '
+                'for devicectl. Changing the timeout to the minimum value of 5.'),
+          );
+        });
+      });
+    });
+
+
+  });
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/devices_test.dart b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
index cb222d9..146e05b 100644
--- a/packages/flutter_tools/test/general.shard/ios/devices_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/devices_test.dart
@@ -19,11 +19,13 @@
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/device_port_forwarder.dart';
 import 'package:flutter_tools/src/ios/application_package.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/ios_workflow.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
 import 'package:flutter_tools/src/macos/xcdevice.dart';
 import 'package:test/fake.dart';
 
@@ -42,6 +44,8 @@
     late IOSDeploy iosDeploy;
     late IMobileDevice iMobileDevice;
     late FileSystem fileSystem;
+    late IOSCoreDeviceControl coreDeviceControl;
+    late XcodeDebug xcodeDebug;
 
     setUp(() {
       final Artifacts artifacts = Artifacts.test();
@@ -61,6 +65,8 @@
         logger: logger,
         processManager: FakeProcessManager.any(),
       );
+      coreDeviceControl = FakeIOSCoreDeviceControl();
+      xcodeDebug = FakeXcodeDebug();
     });
 
     testWithoutContext('successfully instantiates on Mac OS', () {
@@ -72,12 +78,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         sdkVersion: '13.3',
         cpuArchitecture: DarwinArch.arm64,
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
       expect(device.isSupported(), isTrue);
     });
@@ -91,11 +100,14 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.armv7,
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
       expect(device.isSupported(), isFalse);
     });
@@ -109,12 +121,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '1.0.0',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).majorSdkVersion, 1);
       expect(IOSDevice(
         'device-123',
@@ -124,12 +139,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '13.1.1',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).majorSdkVersion, 13);
       expect(IOSDevice(
         'device-123',
@@ -139,12 +157,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '10',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).majorSdkVersion, 10);
       expect(IOSDevice(
         'device-123',
@@ -154,12 +175,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '0',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).majorSdkVersion, 0);
       expect(IOSDevice(
         'device-123',
@@ -169,12 +193,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: 'bogus',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).majorSdkVersion, 0);
     });
 
@@ -187,12 +214,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '13.3.1',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).sdkVersion;
       Version expectedVersion = Version(13, 3, 1, text: '13.3.1');
       expect(sdkVersion, isNotNull);
@@ -207,12 +237,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '13.3.1 (20ADBC)',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).sdkVersion;
       expectedVersion = Version(13, 3, 1, text: '13.3.1 (20ADBC)');
       expect(sdkVersion, isNotNull);
@@ -227,12 +260,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '16.4.1(a) (20ADBC)',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).sdkVersion;
       expectedVersion = Version(16, 4, 1, text: '16.4.1(a) (20ADBC)');
       expect(sdkVersion, isNotNull);
@@ -247,12 +283,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: '0',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).sdkVersion;
       expectedVersion = Version(0, 0, 0, text: '0');
       expect(sdkVersion, isNotNull);
@@ -267,11 +306,14 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).sdkVersion;
       expect(sdkVersion, isNull);
 
@@ -283,12 +325,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         cpuArchitecture: DarwinArch.arm64,
         sdkVersion: 'bogus',
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       ).sdkVersion;
       expect(sdkVersion, isNull);
     });
@@ -302,12 +347,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         sdkVersion: '13.3 17C54',
         cpuArchitecture: DarwinArch.arm64,
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
 
       expect(await device.sdkNameAndVersion,'iOS 13.3 17C54');
@@ -322,12 +370,15 @@
         platform: macPlatform,
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         name: 'iPhone 1',
         sdkVersion: '13.3',
         cpuArchitecture: DarwinArch.arm64,
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
 
       expect(device.supportsRuntimeMode(BuildMode.debug), true);
@@ -348,12 +399,15 @@
               platform: platform,
               iosDeploy: iosDeploy,
               iMobileDevice: iMobileDevice,
+              coreDeviceControl: coreDeviceControl,
+              xcodeDebug: xcodeDebug,
               name: 'iPhone 1',
               sdkVersion: '13.3',
               cpuArchitecture: DarwinArch.arm64,
               connectionInterface: DeviceConnectionInterface.attached,
               isConnected: true,
               devModeEnabled: true,
+              isCoreDevice: false,
             );
           },
           throwsAssertionError,
@@ -440,12 +494,15 @@
           platform: macPlatform,
           iosDeploy: iosDeploy,
           iMobileDevice: iMobileDevice,
+          coreDeviceControl: coreDeviceControl,
+          xcodeDebug: xcodeDebug,
           name: 'iPhone 1',
           sdkVersion: '13.3',
           cpuArchitecture: DarwinArch.arm64,
           connectionInterface: DeviceConnectionInterface.attached,
           isConnected: true,
           devModeEnabled: true,
+          isCoreDevice: false,
         );
         logReader1 = createLogReader(device, appPackage1, process1);
         logReader2 = createLogReader(device, appPackage2, process2);
@@ -471,6 +528,8 @@
     late IOSDeploy iosDeploy;
     late IMobileDevice iMobileDevice;
     late IOSWorkflow iosWorkflow;
+    late IOSCoreDeviceControl coreDeviceControl;
+    late XcodeDebug xcodeDebug;
     late IOSDevice device1;
     late IOSDevice device2;
 
@@ -494,6 +553,8 @@
         processManager: fakeProcessManager,
         logger: logger,
       );
+      coreDeviceControl = FakeIOSCoreDeviceControl();
+      xcodeDebug = FakeXcodeDebug();
 
       device1 = IOSDevice(
         'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
@@ -503,12 +564,15 @@
         iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         logger: logger,
         platform: macPlatform,
         fileSystem: MemoryFileSystem.test(),
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
 
       device2 = IOSDevice(
@@ -519,12 +583,15 @@
         iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         logger: logger,
         platform: macPlatform,
         fileSystem: MemoryFileSystem.test(),
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: true,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
     });
 
@@ -781,6 +848,8 @@
     late IOSDeploy iosDeploy;
     late IMobileDevice iMobileDevice;
     late IOSWorkflow iosWorkflow;
+    late IOSCoreDeviceControl coreDeviceControl;
+    late XcodeDebug xcodeDebug;
     late IOSDevice notConnected1;
 
     setUp(() {
@@ -803,6 +872,8 @@
         processManager: fakeProcessManager,
         logger: logger,
       );
+      coreDeviceControl = FakeIOSCoreDeviceControl();
+      xcodeDebug = FakeXcodeDebug();
       notConnected1 = IOSDevice(
         '00000001-0000000000000000',
         name: 'iPad',
@@ -811,12 +882,15 @@
         iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
         iosDeploy: iosDeploy,
         iMobileDevice: iMobileDevice,
+        coreDeviceControl: coreDeviceControl,
+        xcodeDebug: xcodeDebug,
         logger: logger,
         platform: macPlatform,
         fileSystem: MemoryFileSystem.test(),
         connectionInterface: DeviceConnectionInterface.attached,
         isConnected: false,
         devModeEnabled: true,
+        isCoreDevice: false,
       );
     });
 
@@ -965,3 +1039,10 @@
     return true;
   }
 }
+
+class FakeXcodeDebug extends Fake implements XcodeDebug {
+  @override
+  bool get debugStarted => false;
+}
+
+class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart
index 8c93d8f..6a5f522 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_install_test.dart
@@ -12,10 +12,13 @@
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/ios/application_package.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
+import 'package:test/fake.dart';
 
 import '../../src/common.dart';
 import '../../src/fake_process_manager.dart';
@@ -105,6 +108,28 @@
     expect(processManager, hasNoRemainingExpectations);
   });
 
+  testWithoutContext('IOSDevice.installApp uses devicectl for CoreDevices', () async {
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      uncompressedBundle: fileSystem.currentDirectory,
+      applicationPackage: bundleDirectory,
+    );
+
+    final FakeProcessManager processManager = FakeProcessManager.empty();
+
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+      interfaceType: DeviceConnectionInterface.attached,
+      artifacts: artifacts,
+      isCoreDevice: true,
+    );
+    final bool wasInstalled = await device.installApp(iosApp);
+
+    expect(wasInstalled, true);
+    expect(processManager, hasNoRemainingExpectations);
+  });
+
   testWithoutContext('IOSDevice.uninstallApp calls ios-deploy correctly', () async {
     final IOSApp iosApp = PrebuiltIOSApp(
       projectBundleId: 'app',
@@ -134,6 +159,28 @@
     expect(processManager, hasNoRemainingExpectations);
   });
 
+  testWithoutContext('IOSDevice.uninstallApp uses devicectl for CoreDevices', () async {
+    final IOSApp iosApp = PrebuiltIOSApp(
+      projectBundleId: 'app',
+      uncompressedBundle: fileSystem.currentDirectory,
+      applicationPackage: bundleDirectory,
+    );
+
+    final FakeProcessManager processManager = FakeProcessManager.empty();
+
+    final IOSDevice device = setUpIOSDevice(
+      processManager: processManager,
+      fileSystem: fileSystem,
+      interfaceType: DeviceConnectionInterface.attached,
+      artifacts: artifacts,
+      isCoreDevice: true,
+    );
+    final bool wasUninstalled = await device.uninstallApp(iosApp);
+
+    expect(wasUninstalled, true);
+    expect(processManager, hasNoRemainingExpectations);
+  });
+
   group('isAppInstalled', () {
     testWithoutContext('catches ProcessException from ios-deploy', () async {
       final IOSApp iosApp = PrebuiltIOSApp(
@@ -263,6 +310,28 @@
       expect(processManager, hasNoRemainingExpectations);
       expect(logger.traceText, contains(stderr));
     });
+
+    testWithoutContext('uses devicectl for CoreDevices', () async {
+      final IOSApp iosApp = PrebuiltIOSApp(
+        projectBundleId: 'app',
+        uncompressedBundle: fileSystem.currentDirectory,
+        applicationPackage: bundleDirectory,
+      );
+
+      final FakeProcessManager processManager = FakeProcessManager.empty();
+
+      final IOSDevice device = setUpIOSDevice(
+        processManager: processManager,
+        fileSystem: fileSystem,
+        interfaceType: DeviceConnectionInterface.attached,
+        artifacts: artifacts,
+        isCoreDevice: true,
+      );
+      final bool wasInstalled = await device.isAppInstalled(iosApp);
+
+      expect(wasInstalled, true);
+      expect(processManager, hasNoRemainingExpectations);
+    });
   });
 
   testWithoutContext('IOSDevice.installApp catches ProcessException from ios-deploy', () async {
@@ -314,6 +383,8 @@
 
     expect(wasAppUninstalled, false);
   });
+
+
 }
 
 IOSDevice setUpIOSDevice({
@@ -322,6 +393,7 @@
   Logger? logger,
   DeviceConnectionInterface? interfaceType,
   Artifacts? artifacts,
+  bool isCoreDevice = false,
 }) {
   logger ??= BufferLogger.test();
   final FakePlatform platform = FakePlatform(
@@ -357,9 +429,42 @@
       artifacts: artifacts,
       cache: cache,
     ),
+    coreDeviceControl: FakeIOSCoreDeviceControl(),
+    xcodeDebug: FakeXcodeDebug(),
     iProxy: IProxy.test(logger: logger, processManager: processManager),
     connectionInterface: interfaceType ?? DeviceConnectionInterface.attached,
     isConnected: true,
     devModeEnabled: true,
+    isCoreDevice: isCoreDevice,
   );
 }
+
+class FakeXcodeDebug extends Fake implements XcodeDebug {}
+
+class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
+  @override
+  Future<bool> installApp({
+    required String deviceId,
+    required String bundlePath,
+  }) async {
+
+    return true;
+  }
+
+  @override
+  Future<bool> uninstallApp({
+    required String deviceId,
+    required String bundleId,
+  }) async {
+
+    return true;
+  }
+
+  @override
+  Future<bool> isAppInstalled({
+    required String deviceId,
+    required String bundleId,
+  }) async {
+    return true;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart
index 9f65a67..01506cd 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_logger_test.dart
@@ -190,7 +190,7 @@
       ]));
     });
 
-    testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to debugger', () async {
+    testWithoutContext('IOSDeviceLogReader ignores VM Service logs when attached to and received flutter logs from debugger', () async {
       final Event stdoutEvent = Event(
         kind: 'Stdout',
         timestamp: 0,
@@ -229,14 +229,14 @@
       iosDeployDebugger.debuggerAttached = true;
 
       final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
-        'Message from debugger',
+        'flutter: Message from debugger',
       ]);
       iosDeployDebugger.logLines = debuggingLogs;
       logReader.debuggerStream = iosDeployDebugger;
 
       // Wait for stream listeners to fire.
       await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
-        equals('Message from debugger'),
+        equals('flutter: Message from debugger'),
       ]));
     });
   });
@@ -349,9 +349,8 @@
     });
   });
 
-  group('both syslog and debugger stream', () {
-
-    testWithoutContext('useBothLogDeviceReaders is true when CI option is true and sdk is at least 16', () {
+  group('Determine which loggers to use', () {
+    testWithoutContext('for physically attached CoreDevice', () {
       final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
         iMobileDevice: IMobileDevice(
           artifacts: artifacts,
@@ -359,14 +358,18 @@
           cache: fakeCache,
           logger: logger,
         ),
-        usingCISystem: true,
-        majorSdkVersion: 16,
+        majorSdkVersion: 17,
+        isCoreDevice: true,
       );
 
-      expect(logReader.useBothLogDeviceReaders, isTrue);
+      expect(logReader.useSyslogLogging, isTrue);
+      expect(logReader.useUnifiedLogging, isTrue);
+      expect(logReader.useIOSDeployLogging, isFalse);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog);
+      expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
     });
 
-    testWithoutContext('useBothLogDeviceReaders is false when sdk is less than 16', () {
+    testWithoutContext('for wirelessly attached CoreDevice', () {
       final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
         iMobileDevice: IMobileDevice(
           artifacts: artifacts,
@@ -374,14 +377,19 @@
           cache: fakeCache,
           logger: logger,
         ),
-        usingCISystem: true,
-        majorSdkVersion: 15,
+        majorSdkVersion: 17,
+        isCoreDevice: true,
+        isWirelesslyConnected: true,
       );
 
-      expect(logReader.useBothLogDeviceReaders, isFalse);
+      expect(logReader.useSyslogLogging, isFalse);
+      expect(logReader.useUnifiedLogging, isTrue);
+      expect(logReader.useIOSDeployLogging, isFalse);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging);
+      expect(logReader.logSources.fallbackSource, isNull);
     });
 
-    testWithoutContext('useBothLogDeviceReaders is false when CI option is false', () {
+    testWithoutContext('for iOS 12 or less device', () {
       final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
         iMobileDevice: IMobileDevice(
           artifacts: artifacts,
@@ -389,27 +397,17 @@
           cache: fakeCache,
           logger: logger,
         ),
-        majorSdkVersion: 16,
+        majorSdkVersion: 12,
       );
 
-      expect(logReader.useBothLogDeviceReaders, isFalse);
+      expect(logReader.useSyslogLogging, isTrue);
+      expect(logReader.useUnifiedLogging, isFalse);
+      expect(logReader.useIOSDeployLogging, isFalse);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog);
+      expect(logReader.logSources.fallbackSource, isNull);
     });
 
-    testWithoutContext('syslog only sends flutter messages to stream when useBothLogDeviceReaders is true', () async {
-      processManager.addCommand(
-        FakeCommand(
-            command: <String>[
-              ideviceSyslogPath, '-u', '1234',
-            ],
-            stdout: '''
-Runner(Flutter)[297] <Notice>: A is for ari
-Runner(Flutter)[297] <Notice>: I is for ichigo
-May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
-May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: This is a test
-May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
-'''
-        ),
-      );
+    testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger not attached', () {
       final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
         iMobileDevice: IMobileDevice(
           artifacts: artifacts,
@@ -417,38 +415,17 @@
           cache: fakeCache,
           logger: logger,
         ),
-        usingCISystem: true,
-        majorSdkVersion: 16,
+        majorSdkVersion: 13,
       );
-      final List<String> lines = await logReader.logLines.toList();
 
-      expect(logReader.useBothLogDeviceReaders, isTrue);
-      expect(processManager, hasNoRemainingExpectations);
-      expect(lines, <String>[
-        'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
-        'flutter: This is a test'
-      ]);
+      expect(logReader.useSyslogLogging, isFalse);
+      expect(logReader.useUnifiedLogging, isTrue);
+      expect(logReader.useIOSDeployLogging, isTrue);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+      expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
     });
 
-    testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger', () async {
-      processManager.addCommand(
-        FakeCommand(
-            command: <String>[
-              ideviceSyslogPath, '-u', '1234',
-            ],
-            stdout: '''
-May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
-May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: Check for duplicate
-May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
-'''
-        ),
-      );
-
-      final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
-        '2023-06-01 12:49:01.445093-0500 Runner[2225:533240] flutter: Check for duplicate',
-        '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
-      ]);
-
+    testWithoutContext('for iOS 13 or greater non-CoreDevice, _iosDeployDebugger not attached, and VM is connected', () {
       final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
         iMobileDevice: IMobileDevice(
           artifacts: artifacts,
@@ -456,32 +433,67 @@
           cache: fakeCache,
           logger: logger,
         ),
-        usingCISystem: true,
-        majorSdkVersion: 16,
+        majorSdkVersion: 13,
       );
+
+      final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
+        const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+          'streamId': 'Debug',
+        }),
+        const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+          'streamId': 'Stdout',
+        }),
+        const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+          'streamId': 'Stderr',
+        }),
+      ]).vmService;
+
+      logReader.connectedVMService = vmService;
+
+      expect(logReader.useSyslogLogging, isFalse);
+      expect(logReader.useUnifiedLogging, isTrue);
+      expect(logReader.useIOSDeployLogging, isTrue);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.unifiedLogging);
+      expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.iosDeploy);
+    });
+
+    testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger is attached', () {
+      final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+        iMobileDevice: IMobileDevice(
+          artifacts: artifacts,
+          processManager: processManager,
+          cache: fakeCache,
+          logger: logger,
+        ),
+        majorSdkVersion: 13,
+      );
+
       final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
-      iosDeployDebugger.logLines = debuggingLogs;
+      iosDeployDebugger.debuggerAttached = true;
       logReader.debuggerStream = iosDeployDebugger;
-      final Future<List<String>> logLines = logReader.logLines.toList();
-      final List<String> lines = await logLines;
 
-      expect(logReader.useBothLogDeviceReaders, isTrue);
-      expect(processManager, hasNoRemainingExpectations);
-      expect(lines.length, 3);
-      expect(lines, containsAll(<String>[
-        '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
-        'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
-        'flutter: Check for duplicate',
-      ]));
+      final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
+        const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+          'streamId': 'Debug',
+        }),
+        const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+          'streamId': 'Stdout',
+        }),
+        const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+          'streamId': 'Stderr',
+        }),
+      ]).vmService;
 
+      logReader.connectedVMService = vmService;
+
+      expect(logReader.useSyslogLogging, isFalse);
+      expect(logReader.useUnifiedLogging, isTrue);
+      expect(logReader.useIOSDeployLogging, isTrue);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+      expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
     });
 
-    testWithoutContext('IOSDeviceLogReader only uses ios-deploy debugger when useBothLogDeviceReaders is false', () async {
-      final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
-        '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
-        '',
-      ]);
-
+    testWithoutContext('for iOS 16 or greater non-CoreDevice', () {
       final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
         iMobileDevice: IMobileDevice(
           artifacts: artifacts,
@@ -491,20 +503,490 @@
         ),
         majorSdkVersion: 16,
       );
-      final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
-      iosDeployDebugger.logLines = debuggingLogs;
-      logReader.debuggerStream = iosDeployDebugger;
-      final Future<List<String>> logLines = logReader.logLines.toList();
-      final List<String> lines = await logLines;
 
-      expect(logReader.useBothLogDeviceReaders, isFalse);
-      expect(processManager, hasNoRemainingExpectations);
-      expect(
-        lines.contains(
-          '(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
+      final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
+      iosDeployDebugger.debuggerAttached = true;
+      logReader.debuggerStream = iosDeployDebugger;
+
+      expect(logReader.useSyslogLogging, isFalse);
+      expect(logReader.useUnifiedLogging, isTrue);
+      expect(logReader.useIOSDeployLogging, isTrue);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+      expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.unifiedLogging);
+    });
+
+    testWithoutContext('for iOS 16 or greater non-CoreDevice in CI', () {
+      final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+        iMobileDevice: IMobileDevice(
+          artifacts: artifacts,
+          processManager: processManager,
+          cache: fakeCache,
+          logger: logger,
         ),
-        isTrue,
+        usingCISystem: true,
+        majorSdkVersion: 16,
       );
+
+      expect(logReader.useSyslogLogging, isTrue);
+      expect(logReader.useUnifiedLogging, isFalse);
+      expect(logReader.useIOSDeployLogging, isTrue);
+      expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+      expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
+    });
+
+    group('when useSyslogLogging', () {
+
+      testWithoutContext('is true syslog sends flutter messages to stream', () async {
+        processManager.addCommand(
+          FakeCommand(
+              command: <String>[
+                ideviceSyslogPath, '-u', '1234',
+              ],
+              stdout: '''
+  Runner(Flutter)[297] <Notice>: A is for ari
+  Runner(Flutter)[297] <Notice>: I is for ichigo
+  May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/
+  May 30 13:56:28 Runner(Flutter)[2037] <Notice>: flutter: This is a test
+  May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.
+  '''
+          ),
+        );
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: processManager,
+            cache: fakeCache,
+            logger: logger,
+          ),
+          usingCISystem: true,
+          majorSdkVersion: 16,
+        );
+        final List<String> lines = await logReader.logLines.toList();
+
+        expect(logReader.useSyslogLogging, isTrue);
+        expect(processManager, hasNoRemainingExpectations);
+        expect(lines, <String>[
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          'flutter: This is a test'
+        ]);
+      });
+
+      testWithoutContext('is false syslog does not send flutter messages to stream', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: processManager,
+            cache: fakeCache,
+            logger: logger,
+          ),
+          majorSdkVersion: 16,
+        );
+
+        final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
+        iosDeployDebugger.logLines =  Stream<String>.fromIterable(<String>[]);
+        logReader.debuggerStream = iosDeployDebugger;
+
+        final List<String> lines = await logReader.logLines.toList();
+
+        expect(logReader.useSyslogLogging, isFalse);
+        expect(processManager, hasNoRemainingExpectations);
+        expect(lines, isEmpty);
+      });
+    });
+
+    group('when useIOSDeployLogging', () {
+
+      testWithoutContext('is true ios-deploy sends flutter messages to stream', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: processManager,
+            cache: fakeCache,
+            logger: logger,
+          ),
+          majorSdkVersion: 16,
+        );
+
+        final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
+        final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
+          'flutter: Message from debugger',
+        ]);
+        iosDeployDebugger.logLines = debuggingLogs;
+        logReader.debuggerStream = iosDeployDebugger;
+
+        final List<String> lines = await logReader.logLines.toList();
+
+        expect(logReader.useIOSDeployLogging, isTrue);
+        expect(processManager, hasNoRemainingExpectations);
+        expect(lines, <String>[
+          'flutter: Message from debugger',
+        ]);
+      });
+
+      testWithoutContext('is false ios-deploy does not send flutter messages to stream', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          majorSdkVersion: 12,
+        );
+
+        final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
+        final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
+          'flutter: Message from debugger',
+        ]);
+        iosDeployDebugger.logLines = debuggingLogs;
+        logReader.debuggerStream = iosDeployDebugger;
+
+        final List<String> lines = await logReader.logLines.toList();
+
+        expect(logReader.useIOSDeployLogging, isFalse);
+        expect(processManager, hasNoRemainingExpectations);
+        expect(lines, isEmpty);
+      });
+    });
+
+    group('when useUnifiedLogging', () {
+
+
+      testWithoutContext('is true Dart VM sends flutter messages to stream', () async {
+        final Event stdoutEvent = Event(
+          kind: 'Stdout',
+          timestamp: 0,
+          bytes: base64.encode(utf8.encode('flutter: A flutter message')),
+        );
+        final Event stderrEvent = Event(
+          kind: 'Stderr',
+          timestamp: 0,
+          bytes: base64.encode(utf8.encode('flutter: A second flutter message')),
+        );
+        final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
+          const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+            'streamId': 'Debug',
+          }),
+          const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+            'streamId': 'Stdout',
+          }),
+          const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+            'streamId': 'Stderr',
+          }),
+          FakeVmServiceStreamResponse(event: stdoutEvent, streamId: 'Stdout'),
+          FakeVmServiceStreamResponse(event: stderrEvent, streamId: 'Stderr'),
+        ]).vmService;
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          useSyslog: false,
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: processManager,
+            cache: fakeCache,
+            logger: logger,
+          ),
+        );
+        logReader.connectedVMService = vmService;
+
+        // Wait for stream listeners to fire.
+        expect(logReader.useUnifiedLogging, isTrue);
+        expect(processManager, hasNoRemainingExpectations);
+        await expectLater(logReader.logLines, emitsInAnyOrder(<Matcher>[
+          equals('flutter: A flutter message'),
+          equals('flutter: A second flutter message'),
+        ]));
+      });
+
+      testWithoutContext('is false Dart VM does not send flutter messages to stream', () async {
+        final Event stdoutEvent = Event(
+          kind: 'Stdout',
+          timestamp: 0,
+          bytes: base64.encode(utf8.encode('flutter: A flutter message')),
+        );
+        final Event stderrEvent = Event(
+          kind: 'Stderr',
+          timestamp: 0,
+          bytes: base64.encode(utf8.encode('flutter: A second flutter message')),
+        );
+        final FlutterVmService vmService = FakeVmServiceHost(requests: <VmServiceExpectation>[
+          const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+            'streamId': 'Debug',
+          }),
+          const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+            'streamId': 'Stdout',
+          }),
+          const FakeVmServiceRequest(method: 'streamListen', args: <String, Object>{
+            'streamId': 'Stderr',
+          }),
+          FakeVmServiceStreamResponse(event: stdoutEvent, streamId: 'Stdout'),
+          FakeVmServiceStreamResponse(event: stderrEvent, streamId: 'Stderr'),
+        ]).vmService;
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          majorSdkVersion: 12,
+        );
+        logReader.connectedVMService = vmService;
+
+        final List<String> lines = await logReader.logLines.toList();
+
+        // Wait for stream listeners to fire.
+        expect(logReader.useUnifiedLogging, isFalse);
+        expect(processManager, hasNoRemainingExpectations);
+        expect(lines, isEmpty);
+      });
+    });
+
+    group('and when to exclude logs:', () {
+
+      testWithoutContext('all primary messages are included except if fallback sent flutter message first', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          usingCISystem: true,
+          majorSdkVersion: 16,
+        );
+
+        expect(logReader.useSyslogLogging, isTrue);
+        expect(logReader.useIOSDeployLogging, isTrue);
+        expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+        expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
+
+        final Future<List<String>> logLines = logReader.logLines.toList();
+
+        logReader.addToLinesController(
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        // Will be excluded because was already added by fallback.
+        logReader.addToLinesController(
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        logReader.addToLinesController(
+          'A second non-flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        logReader.addToLinesController(
+          'flutter: Another flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        final List<String> lines = await logLines;
+
+        expect(lines, containsAllInOrder(<String>[
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', // from idevicesyslog
+          'A second non-flutter message', // from iosDeploy
+          'flutter: Another flutter message', // from iosDeploy
+        ]));
+      });
+
+      testWithoutContext('all primary messages are included when there is no fallback', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          majorSdkVersion: 12,
+        );
+
+        expect(logReader.useSyslogLogging, isTrue);
+        expect(logReader.logSources.primarySource, IOSDeviceLogSource.idevicesyslog);
+        expect(logReader.logSources.fallbackSource, isNull);
+
+        final Future<List<String>> logLines = logReader.logLines.toList();
+
+        logReader.addToLinesController(
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'A non-flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'A non-flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        final List<String> lines = await logLines;
+
+        expect(lines, containsAllInOrder(<String>[
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          'A non-flutter message',
+          'A non-flutter message',
+          'flutter: A flutter message',
+          'flutter: A flutter message',
+        ]));
+      });
+
+      testWithoutContext('primary messages are not added if fallback already added them, otherwise duplicates are allowed', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          usingCISystem: true,
+          majorSdkVersion: 16,
+        );
+
+        expect(logReader.useSyslogLogging, isTrue);
+        expect(logReader.useIOSDeployLogging, isTrue);
+        expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+        expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
+
+        final Future<List<String>> logLines = logReader.logLines.toList();
+
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'A non-flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        logReader.addToLinesController(
+          'A non-flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        // Will be excluded because was already added by fallback.
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        // Will be excluded because was already added by fallback.
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        // Will be included because, although the message is the same, the
+        // fallback only added it twice so this third one is considered new.
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+
+        final List<String> lines = await logLines;
+
+        expect(lines, containsAllInOrder(<String>[
+          'flutter: A flutter message', // from idevicesyslog
+          'flutter: A flutter message', // from idevicesyslog
+          'A non-flutter message', // from iosDeploy
+          'A non-flutter message', // from iosDeploy
+          'flutter: A flutter message', // from iosDeploy
+        ]));
+      });
+
+      testWithoutContext('flutter fallback messages are included until a primary flutter message is received', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          usingCISystem: true,
+          majorSdkVersion: 16,
+        );
+
+        expect(logReader.useSyslogLogging, isTrue);
+        expect(logReader.useIOSDeployLogging, isTrue);
+        expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+        expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
+
+        final Future<List<String>> logLines = logReader.logLines.toList();
+
+        logReader.addToLinesController(
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        logReader.addToLinesController(
+          'A second non-flutter message',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        // Will be included because the first log from primary source wasn't a
+        // flutter log.
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        // Will be excluded because was already added by fallback, however, it
+        // will be used to determine a flutter log was received by the primary source.
+        logReader.addToLinesController(
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
+          IOSDeviceLogSource.iosDeploy,
+        );
+        // Will be excluded because flutter log from primary was received.
+        logReader.addToLinesController(
+          'flutter: A third flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+
+        final List<String> lines = await logLines;
+
+        expect(lines, containsAllInOrder(<String>[
+          'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/', // from idevicesyslog
+          'A second non-flutter message', // from iosDeploy
+          'flutter: A flutter message', // from idevicesyslog
+        ]));
+      });
+
+      testWithoutContext('non-flutter fallback messages are not included', () async {
+        final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
+          iMobileDevice: IMobileDevice(
+            artifacts: artifacts,
+            processManager: FakeProcessManager.any(),
+            cache: fakeCache,
+            logger: logger,
+          ),
+          usingCISystem: true,
+          majorSdkVersion: 16,
+        );
+
+        expect(logReader.useSyslogLogging, isTrue);
+        expect(logReader.useIOSDeployLogging, isTrue);
+        expect(logReader.logSources.primarySource, IOSDeviceLogSource.iosDeploy);
+        expect(logReader.logSources.fallbackSource, IOSDeviceLogSource.idevicesyslog);
+
+        final Future<List<String>> logLines = logReader.logLines.toList();
+
+        logReader.addToLinesController(
+          'flutter: A flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+        // Will be excluded because it's from fallback and not a flutter message.
+        logReader.addToLinesController(
+          'A non-flutter message',
+          IOSDeviceLogSource.idevicesyslog,
+        );
+
+        final List<String> lines = await logLines;
+
+        expect(lines, containsAllInOrder(<String>[
+          'flutter: A flutter message',
+        ]));
+      });
     });
   });
 }
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart
index 082d70b..abe1e85 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_project_test.dart
@@ -10,11 +10,14 @@
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
 import 'package:flutter_tools/src/project.dart';
+import 'package:test/fake.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
@@ -94,6 +97,8 @@
       cache: Cache.test(processManager: processManager),
     ),
     iMobileDevice: IMobileDevice.test(processManager: processManager),
+    coreDeviceControl: FakeIOSCoreDeviceControl(),
+    xcodeDebug: FakeXcodeDebug(),
     platform: platform,
     name: 'iPhone 1',
     sdkVersion: '13.3',
@@ -102,5 +107,10 @@
     connectionInterface: DeviceConnectionInterface.attached,
     isConnected: true,
     devModeEnabled: true,
+    isCoreDevice: false,
   );
 }
+
+class FakeXcodeDebug extends Fake implements XcodeDebug {}
+
+class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
index be0d51a..d7f354c 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:fake_async/fake_async.dart';
 import 'package:file/memory.dart';
 import 'package:file_testing/file_testing.dart';
@@ -13,11 +15,14 @@
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/device_port_forwarder.dart';
 import 'package:flutter_tools/src/ios/application_package.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
 import 'package:flutter_tools/src/project.dart';
@@ -25,6 +30,7 @@
 
 import '../../src/common.dart';
 import '../../src/context.dart' hide FakeXcodeProjectInterpreter;
+import '../../src/fake_devices.dart';
 import '../../src/fake_process_manager.dart';
 import '../../src/fakes.dart';
 
@@ -287,13 +293,363 @@
       Xcode: () => xcode,
     }, skip: true); // TODO(zanderso): clean up with https://github.com/flutter/flutter/issues/60675
   });
+
+  group('IOSDevice.startApp for CoreDevice', () {
+    late FileSystem fileSystem;
+    late FakeProcessManager processManager;
+    late BufferLogger logger;
+    late Xcode xcode;
+    late FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter;
+    late XcodeProjectInfo projectInfo;
+
+    setUp(() {
+      logger = BufferLogger.test();
+      fileSystem = MemoryFileSystem.test();
+      processManager = FakeProcessManager.empty();
+      projectInfo = XcodeProjectInfo(
+        <String>['Runner'],
+        <String>['Debug', 'Release'],
+        <String>['Runner'],
+        logger,
+      );
+      fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
+      xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
+      fileSystem.file('foo/.packages')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('\n');
+    });
+
+    group('in release mode', () {
+      testUsingContext('suceeds when install and launch succeed', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+        );
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+          platformArgs: <String, Object>{},
+        );
+
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, true);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
+      testUsingContext('fails when install fails', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(
+            installSuccess: false,
+          ),
+        );
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+          platformArgs: <String, Object>{},
+        );
+
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, false);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
+      testUsingContext('fails when launch fails', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(
+            launchSuccess: false,
+          ),
+        );
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+          platformArgs: <String, Object>{},
+        );
+
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, false);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
+      testUsingContext('ensure arguments passed to launch', () async {
+        final FakeIOSCoreDeviceControl coreDeviceControl = FakeIOSCoreDeviceControl();
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: coreDeviceControl,
+        );
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
+          platformArgs: <String, Object>{},
+        );
+
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, true);
+        expect(processManager, hasNoRemainingExpectations);
+        expect(coreDeviceControl.argumentsUsedForLaunch, isNotNull);
+        expect(coreDeviceControl.argumentsUsedForLaunch, contains('--enable-dart-profiling'));
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
+    });
+
+    group('in debug mode', () {
+
+      testUsingContext('succeeds', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: FakeXcodeDebug(
+            expectedProject: XcodeDebugProject(
+              scheme: 'Runner',
+              xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
+              xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
+            ),
+            expectedDeviceId: '123',
+            expectedLaunchArguments: <String>['--enable-dart-profiling'],
+          ),
+        );
+
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        iosDevice.portForwarder = const NoOpDevicePortForwarder();
+        iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
+
+        // Start writing messages to the log reader.
+        Timer.run(() {
+          deviceLogReader.addLine('Foo');
+          deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+        });
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
+            BuildMode.debug,
+            null,
+            buildName: '1.2.3',
+            buildNumber: '4',
+            treeShakeIcons: false,
+          )),
+          platformArgs: <String, Object>{},
+        );
+
+        expect(logger.errorText, isEmpty);
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, true);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
+      testUsingContext('fails when Xcode project is not found', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl()
+        );
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
+            BuildMode.debug,
+            null,
+            buildName: '1.2.3',
+            buildNumber: '4',
+            treeShakeIcons: false,
+          )),
+          platformArgs: <String, Object>{},
+        );
+        expect(logger.errorText, contains('Xcode project not found'));
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, false);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
+        Xcode: () => xcode,
+      });
+
+      testUsingContext('fails when Xcode workspace is not found', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl()
+        );
+        setUpIOSProject(fileSystem, createWorkspace: false);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final LaunchResult launchResult = await iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
+            BuildMode.debug,
+            null,
+            buildName: '1.2.3',
+            buildNumber: '4',
+            treeShakeIcons: false,
+          )),
+          platformArgs: <String, Object>{},
+        );
+        expect(logger.errorText, contains('Unable to get Xcode workspace'));
+        expect(fileSystem.directory('build/ios/iphoneos'), exists);
+        expect(launchResult.started, false);
+        expect(processManager, hasNoRemainingExpectations);
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+
+      testUsingContext('fails when scheme is not found', () async {
+        final IOSDevice iosDevice = setUpIOSDevice(
+          fileSystem: fileSystem,
+          processManager: FakeProcessManager.any(),
+          logger: logger,
+          artifacts: artifacts,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl()
+        );
+        setUpIOSProject(fileSystem);
+        final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
+        final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
+        fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
+
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        iosDevice.portForwarder = const NoOpDevicePortForwarder();
+        iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
+
+        // Start writing messages to the log reader.
+        Timer.run(() {
+          deviceLogReader.addLine('Foo');
+          deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+        });
+
+        expect(() async => iosDevice.startApp(
+          buildableIOSApp,
+          debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
+            BuildMode.debug,
+            'Flavor',
+            buildName: '1.2.3',
+            buildNumber: '4',
+            treeShakeIcons: false,
+          )),
+          platformArgs: <String, Object>{},
+        ), throwsToolExit());
+      }, overrides: <Type, Generator>{
+        ProcessManager: () => FakeProcessManager.any(),
+        FileSystem: () => fileSystem,
+        Logger: () => logger,
+        Platform: () => macPlatform,
+        XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
+        Xcode: () => xcode,
+      });
+    });
+  });
 }
 
-void setUpIOSProject(FileSystem fileSystem) {
+void setUpIOSProject(FileSystem fileSystem, {bool createWorkspace = true}) {
   fileSystem.file('pubspec.yaml').createSync();
   fileSystem.file('.packages').writeAsStringSync('\n');
   fileSystem.directory('ios').createSync();
-  fileSystem.directory('ios/Runner.xcworkspace').createSync();
+  if (createWorkspace) {
+    fileSystem.directory('ios/Runner.xcworkspace').createSync();
+  }
   fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync(recursive: true);
   // This is the expected output directory.
   fileSystem.directory('build/ios/iphoneos/My Super Awesome App.app').createSync(recursive: true);
@@ -305,6 +661,9 @@
   Logger? logger,
   ProcessManager? processManager,
   Artifacts? artifacts,
+  bool isCoreDevice = false,
+  IOSCoreDeviceControl? coreDeviceControl,
+  FakeXcodeDebug? xcodeDebug,
 }) {
   artifacts ??= Artifacts.test();
   final Cache cache = Cache.test(
@@ -336,10 +695,13 @@
       artifacts: artifacts,
       cache: cache,
     ),
+    coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
+    xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
     cpuArchitecture: DarwinArch.arm64,
     connectionInterface: DeviceConnectionInterface.attached,
     isConnected: true,
     devModeEnabled: true,
+    isCoreDevice: isCoreDevice,
   );
 }
 
@@ -381,3 +743,70 @@
     Duration timeout = const Duration(minutes: 1),
   }) async => buildSettings;
 }
+
+class FakeXcodeDebug extends Fake implements XcodeDebug {
+  FakeXcodeDebug({
+    this.debugSuccess = true,
+    this.expectedProject,
+    this.expectedDeviceId,
+    this.expectedLaunchArguments,
+  });
+
+  final bool debugSuccess;
+
+  final XcodeDebugProject? expectedProject;
+  final String? expectedDeviceId;
+  final List<String>? expectedLaunchArguments;
+
+  @override
+  Future<bool> debugApp({
+    required XcodeDebugProject project,
+    required String deviceId,
+    required List<String> launchArguments,
+  }) async {
+    if (expectedProject != null) {
+      expect(project.scheme, expectedProject!.scheme);
+      expect(project.xcodeWorkspace.path, expectedProject!.xcodeWorkspace.path);
+      expect(project.xcodeProject.path, expectedProject!.xcodeProject.path);
+      expect(project.isTemporaryProject, expectedProject!.isTemporaryProject);
+    }
+    if (expectedDeviceId != null) {
+      expect(deviceId, expectedDeviceId);
+    }
+    if (expectedLaunchArguments != null) {
+      expect(expectedLaunchArguments, launchArguments);
+    }
+    return debugSuccess;
+  }
+}
+
+class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
+  FakeIOSCoreDeviceControl({
+    this.installSuccess = true,
+    this.launchSuccess = true
+  });
+
+  final bool installSuccess;
+  final bool launchSuccess;
+  List<String>? _launchArguments;
+
+  List<String>? get argumentsUsedForLaunch => _launchArguments;
+
+  @override
+  Future<bool> installApp({
+    required String deviceId,
+    required String bundlePath,
+  }) async {
+    return installSuccess;
+  }
+
+  @override
+  Future<bool> launchApp({
+    required String deviceId,
+    required String bundleId,
+    List<String> launchArguments = const <String>[],
+  }) async {
+    _launchArguments = launchArguments;
+    return launchSuccess;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
index 5d01888..8e8e223 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -5,20 +5,25 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:fake_async/fake_async.dart';
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/process.dart';
+import 'package:flutter_tools/src/base/template.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/device_port_forwarder.dart';
 import 'package:flutter_tools/src/ios/application_package.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/ios_deploy.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
 import 'package:flutter_tools/src/ios/mac.dart';
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
 import 'package:flutter_tools/src/mdns_discovery.dart';
 import 'package:test/fake.dart';
 
@@ -601,6 +606,212 @@
     expect(await device.stopApp(iosApp), false);
     expect(processManager, hasNoRemainingExpectations);
   });
+
+  group('IOSDevice.startApp for CoreDevice', () {
+    group('in debug mode', () {
+      testUsingContext('succeeds', () async {
+        final FileSystem fileSystem = MemoryFileSystem.test();
+        final FakeProcessManager processManager = FakeProcessManager.empty();
+
+        final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
+        final Directory bundleLocation = fileSystem.currentDirectory;
+        final IOSDevice device = setUpIOSDevice(
+          processManager: processManager,
+          fileSystem: fileSystem,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: FakeXcodeDebug(
+            expectedProject: XcodeDebugProject(
+              scheme: 'Runner',
+              xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
+              xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+            ),
+            expectedDeviceId: '123',
+            expectedLaunchArguments: <String>['--enable-dart-profiling'],
+            expectedBundlePath: bundleLocation.path,
+          )
+        );
+        final IOSApp iosApp = PrebuiltIOSApp(
+          projectBundleId: 'app',
+          bundleName: 'Runner',
+          uncompressedBundle: bundleLocation,
+          applicationPackage: bundleLocation,
+        );
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        device.portForwarder = const NoOpDevicePortForwarder();
+        device.setLogReader(iosApp, deviceLogReader);
+
+        // Start writing messages to the log reader.
+        Timer.run(() {
+          deviceLogReader.addLine('Foo');
+          deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+        });
+
+        final LaunchResult launchResult = await device.startApp(iosApp,
+          prebuiltApplication: true,
+          debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+          platformArgs: <String, dynamic>{},
+        );
+
+        expect(launchResult.started, true);
+      });
+
+      testUsingContext('prints warning message if it takes too long to start debugging', () async {
+        final FileSystem fileSystem = MemoryFileSystem.test();
+        final FakeProcessManager processManager = FakeProcessManager.empty();
+        final BufferLogger logger = BufferLogger.test();
+        final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
+        final Directory bundleLocation = fileSystem.currentDirectory;
+        final Completer<void> completer = Completer<void>();
+        final FakeXcodeDebug xcodeDebug = FakeXcodeDebug(
+            expectedProject: XcodeDebugProject(
+              scheme: 'Runner',
+              xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
+              xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+            ),
+            expectedDeviceId: '123',
+            expectedLaunchArguments: <String>['--enable-dart-profiling'],
+            expectedBundlePath: bundleLocation.path,
+            completer: completer,
+          );
+        final IOSDevice device = setUpIOSDevice(
+          processManager: processManager,
+          fileSystem: fileSystem,
+          logger: logger,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: xcodeDebug,
+        );
+        final IOSApp iosApp = PrebuiltIOSApp(
+          projectBundleId: 'app',
+          bundleName: 'Runner',
+          uncompressedBundle: bundleLocation,
+          applicationPackage: bundleLocation,
+        );
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        device.portForwarder = const NoOpDevicePortForwarder();
+        device.setLogReader(iosApp, deviceLogReader);
+
+        // Start writing messages to the log reader.
+        Timer.run(() {
+          deviceLogReader.addLine('Foo');
+          deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+        });
+
+        FakeAsync().run((FakeAsync fakeAsync) {
+          device.startApp(iosApp,
+            prebuiltApplication: true,
+            debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+            platformArgs: <String, dynamic>{},
+          );
+
+          fakeAsync.flushTimers();
+          expect(logger.errorText, contains('Xcode is taking longer than expected to start debugging the app. Ensure the project is opened in Xcode.'));
+          completer.complete();
+        });
+      });
+
+      testUsingContext('succeeds with shutdown hook added when running from CI', () async {
+        final FileSystem fileSystem = MemoryFileSystem.test();
+        final FakeProcessManager processManager = FakeProcessManager.empty();
+
+        final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
+        final Directory bundleLocation = fileSystem.currentDirectory;
+        final IOSDevice device = setUpIOSDevice(
+          processManager: processManager,
+          fileSystem: fileSystem,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: FakeXcodeDebug(
+            expectedProject: XcodeDebugProject(
+              scheme: 'Runner',
+              xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
+              xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+            ),
+            expectedDeviceId: '123',
+            expectedLaunchArguments: <String>['--enable-dart-profiling'],
+            expectedBundlePath: bundleLocation.path,
+          )
+        );
+        final IOSApp iosApp = PrebuiltIOSApp(
+          projectBundleId: 'app',
+          bundleName: 'Runner',
+          uncompressedBundle: bundleLocation,
+          applicationPackage: bundleLocation,
+        );
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        device.portForwarder = const NoOpDevicePortForwarder();
+        device.setLogReader(iosApp, deviceLogReader);
+
+        // Start writing messages to the log reader.
+        Timer.run(() {
+          deviceLogReader.addLine('Foo');
+          deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
+        });
+
+        final FakeShutDownHooks shutDownHooks = FakeShutDownHooks();
+
+        final LaunchResult launchResult = await device.startApp(iosApp,
+          prebuiltApplication: true,
+          debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, usingCISystem: true),
+          platformArgs: <String, dynamic>{},
+          shutdownHooks: shutDownHooks,
+        );
+
+        expect(launchResult.started, true);
+        expect(shutDownHooks.hooks.length, 1);
+      });
+
+      testUsingContext('IOSDevice.startApp attaches in debug mode via mDNS when device logging fails', () async {
+        final FileSystem fileSystem = MemoryFileSystem.test();
+        final FakeProcessManager processManager = FakeProcessManager.empty();
+
+        final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
+        final Directory bundleLocation = fileSystem.currentDirectory;
+        final IOSDevice device = setUpIOSDevice(
+          processManager: processManager,
+          fileSystem: fileSystem,
+          isCoreDevice: true,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: FakeXcodeDebug(
+            expectedProject: XcodeDebugProject(
+              scheme: 'Runner',
+              xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
+              xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+            ),
+            expectedDeviceId: '123',
+            expectedLaunchArguments: <String>['--enable-dart-profiling'],
+            expectedBundlePath: bundleLocation.path,
+          )
+        );
+        final IOSApp iosApp = PrebuiltIOSApp(
+          projectBundleId: 'app',
+          bundleName: 'Runner',
+          uncompressedBundle: bundleLocation,
+          applicationPackage: bundleLocation,
+        );
+        final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
+
+        device.portForwarder = const NoOpDevicePortForwarder();
+        device.setLogReader(iosApp, deviceLogReader);
+
+        final LaunchResult launchResult = await device.startApp(iosApp,
+          prebuiltApplication: true,
+          debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+          platformArgs: <String, dynamic>{},
+        );
+
+        expect(launchResult.started, true);
+        expect(launchResult.hasVmService, true);
+        expect(await device.stopApp(iosApp), true);
+      }, overrides: <Type, Generator>{
+        MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(),
+      });
+    });
+  });
 }
 
 IOSDevice setUpIOSDevice({
@@ -610,6 +821,9 @@
   ProcessManager? processManager,
   IOSDeploy? iosDeploy,
   DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
+  bool isCoreDevice = false,
+  IOSCoreDeviceControl? coreDeviceControl,
+  FakeXcodeDebug? xcodeDebug,
 }) {
   final Artifacts artifacts = Artifacts.test();
   final FakePlatform macPlatform = FakePlatform(
@@ -646,10 +860,13 @@
       artifacts: artifacts,
       cache: cache,
     ),
+    coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
+    xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
     cpuArchitecture: DarwinArch.arm64,
     connectionInterface: interfaceType,
     isConnected: true,
     devModeEnabled: true,
+    isCoreDevice: isCoreDevice,
   );
 }
 
@@ -669,10 +886,88 @@
     Device device, {
     bool usesIpv6 = false,
     int? hostVmservicePort,
-    required int deviceVmservicePort,
+    int? deviceVmservicePort,
     bool useDeviceIPAsHost = false,
     Duration timeout = Duration.zero,
   }) async {
     return Uri.tryParse('http://0.0.0.0:1234');
   }
 }
+
+class FakeXcodeDebug extends Fake implements XcodeDebug {
+  FakeXcodeDebug({
+    this.debugSuccess = true,
+    this.expectedProject,
+    this.expectedDeviceId,
+    this.expectedLaunchArguments,
+    this.expectedBundlePath,
+    this.completer,
+  });
+
+  final bool debugSuccess;
+  final XcodeDebugProject? expectedProject;
+  final String? expectedDeviceId;
+  final List<String>? expectedLaunchArguments;
+  final String? expectedBundlePath;
+  final Completer<void>? completer;
+
+  @override
+  bool debugStarted = false;
+
+  @override
+  Future<XcodeDebugProject> createXcodeProjectWithCustomBundle(
+    String deviceBundlePath, {
+    required TemplateRenderer templateRenderer,
+    Directory? projectDestination,
+    bool verboseLogging = false,
+  }) async {
+    if (expectedBundlePath != null) {
+      expect(expectedBundlePath, deviceBundlePath);
+    }
+    return expectedProject!;
+  }
+
+  @override
+  Future<bool> debugApp({
+    required XcodeDebugProject project,
+    required String deviceId,
+    required List<String> launchArguments,
+  }) async {
+    if (expectedProject != null) {
+      expect(project.scheme, expectedProject!.scheme);
+      expect(project.xcodeWorkspace.path, expectedProject!.xcodeWorkspace.path);
+      expect(project.xcodeProject.path, expectedProject!.xcodeProject.path);
+      expect(project.isTemporaryProject, expectedProject!.isTemporaryProject);
+    }
+    if (expectedDeviceId != null) {
+      expect(deviceId, expectedDeviceId);
+    }
+    if (expectedLaunchArguments != null) {
+      expect(expectedLaunchArguments, launchArguments);
+    }
+    debugStarted = debugSuccess;
+
+    if (completer != null) {
+      await completer!.future;
+    }
+    return debugSuccess;
+  }
+
+  @override
+  Future<bool> exit({
+    bool force = false,
+    bool skipDelay = false,
+  }) async {
+    return true;
+  }
+}
+
+class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {}
+
+class FakeShutDownHooks extends Fake implements ShutdownHooks {
+  List<ShutdownHook> hooks = <ShutdownHook>[];
+  @override
+  void addShutdownHook(ShutdownHook shutdownHook) {
+    hooks.add(shutdownHook);
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
new file mode 100644
index 0000000..cbd2416
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
@@ -0,0 +1,1136 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' as io;
+
+import 'package:file/memory.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/version.dart';
+import 'package:flutter_tools/src/globals.dart' as globals;
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
+import 'package:flutter_tools/src/ios/xcodeproj.dart';
+import 'package:flutter_tools/src/macos/xcode.dart';
+import 'package:test/fake.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/fake_process_manager.dart';
+
+void main() {
+  group('Debug project through Xcode', () {
+    late MemoryFileSystem fileSystem;
+    late BufferLogger logger;
+    late FakeProcessManager fakeProcessManager;
+
+    const String flutterRoot = '/path/to/flutter';
+    const String pathToXcodeAutomationScript = '$flutterRoot/packages/flutter_tools/bin/xcode_debug.js';
+
+    setUp(() {
+      fileSystem = MemoryFileSystem.test();
+      logger = BufferLogger.test();
+      fakeProcessManager = FakeProcessManager.empty();
+    });
+
+    group('debugApp', () {
+      const String pathToXcodeApp = '/Applications/Xcode.app';
+      const String deviceId = '0000001234';
+
+      late Xcode xcode;
+      late Directory xcodeproj;
+      late Directory xcworkspace;
+      late XcodeDebugProject project;
+
+      setUp(() {
+        xcode = setupXcode(
+          fakeProcessManager: fakeProcessManager,
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+
+        xcodeproj = fileSystem.directory('Runner.xcodeproj');
+        xcworkspace = fileSystem.directory('Runner.xcworkspace');
+        project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+        );
+      });
+
+      testWithoutContext('succeeds in opening and debugging with launch options and verbose logging', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--verbose',
+            ],
+            stdout: '''
+  {"status":false,"errorMessage":"Xcode is not running","debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'open',
+              '-a',
+              pathToXcodeApp,
+              '-g',
+              '-j',
+              xcworkspace.path
+            ],
+          ),
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'debug',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--device-id',
+              deviceId,
+              '--scheme',
+              project.scheme,
+              '--skip-building',
+              '--launch-args',
+              r'["--enable-dart-profiling","--trace-allowlist=\"foo,bar\""]',
+              '--verbose',
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":{"completed":false,"status":"running","errorMessage":null}}
+  ''',
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+          verboseLogging: true,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[
+            '--enable-dart-profiling',
+            '--trace-allowlist="foo,bar"'
+          ],
+        );
+
+        expect(logger.errorText, isEmpty);
+        expect(logger.traceText, contains('Error checking if project opened in Xcode'));
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(xcodeDebug.startDebugActionProcess, isNull);
+        expect(status, true);
+      });
+
+      testWithoutContext('succeeds in opening and debugging without launch options and verbose logging', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":false,"errorMessage":"Xcode is not running","debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'open',
+              '-a',
+              pathToXcodeApp,
+              '-g',
+              '-j',
+              xcworkspace.path
+            ],
+          ),
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'debug',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--device-id',
+              deviceId,
+              '--scheme',
+              project.scheme,
+              '--skip-building',
+              '--launch-args',
+              '[]'
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":{"completed":false,"status":"running","errorMessage":null}}
+  ''',
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[],
+        );
+
+        expect(logger.errorText, isEmpty);
+        expect(logger.traceText, contains('Error checking if project opened in Xcode'));
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(xcodeDebug.startDebugActionProcess, isNull);
+        expect(status, true);
+      });
+
+      testWithoutContext('fails if project fails to open', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":false,"errorMessage":"Xcode is not running","debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'open',
+              '-a',
+              pathToXcodeApp,
+              '-g',
+              '-j',
+              xcworkspace.path
+            ],
+            exception: ProcessException(
+              'open',
+              <String>[
+                '-a',
+                '/non_existant_path',
+                '-g',
+                '-j',
+                xcworkspace.path,
+              ],
+              'The application /non_existant_path cannot be opened for an unexpected reason',
+            ),
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[
+            '--enable-dart-profiling',
+            '--trace-allowlist="foo,bar"',
+          ],
+        );
+
+        expect(
+          logger.errorText,
+          contains('The application /non_existant_path cannot be opened for an unexpected reason'),
+        );
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, false);
+      });
+
+      testWithoutContext('fails if osascript errors', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":"","debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'debug',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--device-id',
+              deviceId,
+              '--scheme',
+              project.scheme,
+              '--skip-building',
+              '--launch-args',
+              r'["--enable-dart-profiling","--trace-allowlist=\"foo,bar\""]'
+            ],
+            exitCode: 1,
+            stderr: "/flutter/packages/flutter_tools/bin/xcode_debug.js: execution error: Error: ReferenceError: Can't find variable: y (-2700)",
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[
+            '--enable-dart-profiling',
+            '--trace-allowlist="foo,bar"',
+          ],
+        );
+
+        expect(logger.errorText, contains('Error executing osascript'));
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, false);
+      });
+
+      testWithoutContext('fails if osascript output returns false status', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'debug',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--device-id',
+              deviceId,
+              '--scheme',
+              project.scheme,
+              '--skip-building',
+              '--launch-args',
+              r'["--enable-dart-profiling","--trace-allowlist=\"foo,bar\""]'
+            ],
+            stdout: '''
+  {"status":false,"errorMessage":"Unable to find target device.","debugResult":null}
+  ''',
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[
+            '--enable-dart-profiling',
+            '--trace-allowlist="foo,bar"',
+          ],
+        );
+
+        expect(
+          logger.errorText,
+          contains('Error starting debug session in Xcode'),
+        );
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, false);
+      });
+
+      testWithoutContext('fails if missing debug results', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'debug',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--device-id',
+              deviceId,
+              '--scheme',
+              project.scheme,
+              '--skip-building',
+              '--launch-args',
+              r'["--enable-dart-profiling","--trace-allowlist=\"foo,bar\""]'
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[
+            '--enable-dart-profiling',
+            '--trace-allowlist="foo,bar"'
+          ],
+        );
+
+        expect(
+          logger.errorText,
+          contains('Unable to get debug results from response'),
+        );
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, false);
+      });
+
+      testWithoutContext('fails if debug results status is not running', () async {
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'check-workspace-opened',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'debug',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--device-id',
+              deviceId,
+              '--scheme',
+              project.scheme,
+              '--skip-building',
+              '--launch-args',
+              r'["--enable-dart-profiling","--trace-allowlist=\"foo,bar\""]'
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":{"completed":false,"status":"not yet started","errorMessage":null}}
+  ''',
+          ),
+        ]);
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final bool status = await xcodeDebug.debugApp(
+          project: project,
+          deviceId: deviceId,
+          launchArguments: <String>[
+            '--enable-dart-profiling',
+            '--trace-allowlist="foo,bar"',
+          ],
+        );
+
+        expect(logger.errorText, contains('Unexpected debug results'));
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, false);
+      });
+    });
+
+    group('parse script response', () {
+      testWithoutContext('fails if osascript output returns non-json output', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: FakeProcessManager.any(),
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final XcodeAutomationScriptResponse? response = xcodeDebug.parseScriptResponse('not json');
+
+        expect(
+          logger.errorText,
+          contains('osascript returned non-JSON response'),
+        );
+        expect(response, isNull);
+      });
+
+      testWithoutContext('fails if osascript output returns unexpected json', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: FakeProcessManager.any(),
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final XcodeAutomationScriptResponse? response = xcodeDebug.parseScriptResponse('[]');
+
+        expect(
+          logger.errorText,
+          contains('osascript returned unexpected JSON response'),
+        );
+        expect(response, isNull);
+      });
+
+      testWithoutContext('fails if osascript output is missing status field', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: FakeProcessManager.any(),
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        final XcodeAutomationScriptResponse? response = xcodeDebug.parseScriptResponse('{}');
+
+        expect(
+          logger.errorText,
+          contains('osascript returned unexpected JSON response'),
+        );
+        expect(response, isNull);
+      });
+    });
+
+    group('exit', () {
+      const String pathToXcodeApp = '/Applications/Xcode.app';
+
+      late Directory projectDirectory;
+      late Directory xcodeproj;
+      late Directory xcworkspace;
+
+      setUp(() {
+        projectDirectory = fileSystem.directory('FlutterApp');
+        xcodeproj = projectDirectory.childDirectory('Runner.xcodeproj');
+        xcworkspace = projectDirectory.childDirectory('Runner.xcworkspace');
+      });
+
+      testWithoutContext('exits when waiting for debug session to start', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: fakeProcessManager,
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebugProject project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'stop',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+        ]);
+
+        xcodeDebug.startDebugActionProcess = FakeProcess();
+        xcodeDebug.currentDebuggingProject = project;
+
+        expect(xcodeDebug.startDebugActionProcess, isNotNull);
+        expect(xcodeDebug.currentDebuggingProject, isNotNull);
+
+        final bool exitStatus = await xcodeDebug.exit();
+
+        expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
+        expect(xcodeDebug.currentDebuggingProject, isNull);
+        expect(logger.errorText, isEmpty);
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(exitStatus, isTrue);
+      });
+
+      testWithoutContext('exits and deletes temporary directory', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: fakeProcessManager,
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        xcodeproj.createSync(recursive: true);
+        xcworkspace.createSync(recursive: true);
+
+        final XcodeDebugProject project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+          isTemporaryProject: true,
+        );
+
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'stop',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--close-window'
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+        ]);
+
+        xcodeDebug.startDebugActionProcess = FakeProcess();
+        xcodeDebug.currentDebuggingProject = project;
+
+        expect(xcodeDebug.startDebugActionProcess, isNotNull);
+        expect(xcodeDebug.currentDebuggingProject, isNotNull);
+        expect(projectDirectory.existsSync(), isTrue);
+        expect(xcodeproj.existsSync(), isTrue);
+        expect(xcworkspace.existsSync(), isTrue);
+
+        final bool status = await xcodeDebug.exit(skipDelay: true);
+
+        expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
+        expect(xcodeDebug.currentDebuggingProject, isNull);
+        expect(projectDirectory.existsSync(), isFalse);
+        expect(xcodeproj.existsSync(), isFalse);
+        expect(xcworkspace.existsSync(), isFalse);
+        expect(logger.errorText, isEmpty);
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, isTrue);
+      });
+
+      testWithoutContext('prints error message when deleting temporary directory that is nonexistant', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: fakeProcessManager,
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebugProject project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+          isTemporaryProject: true,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'stop',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--close-window'
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+        ]);
+
+        xcodeDebug.startDebugActionProcess = FakeProcess();
+        xcodeDebug.currentDebuggingProject = project;
+
+        expect(xcodeDebug.startDebugActionProcess, isNotNull);
+        expect(xcodeDebug.currentDebuggingProject, isNotNull);
+        expect(projectDirectory.existsSync(), isFalse);
+        expect(xcodeproj.existsSync(), isFalse);
+        expect(xcworkspace.existsSync(), isFalse);
+
+        final bool status = await xcodeDebug.exit(skipDelay: true);
+
+        expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
+        expect(xcodeDebug.currentDebuggingProject, isNull);
+        expect(projectDirectory.existsSync(), isFalse);
+        expect(xcodeproj.existsSync(), isFalse);
+        expect(xcworkspace.existsSync(), isFalse);
+        expect(logger.errorText, contains('Failed to delete temporary Xcode project'));
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, isTrue);
+      });
+
+      testWithoutContext('kill Xcode when force exit', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: FakeProcessManager.any(),
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebugProject project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+        fakeProcessManager.addCommands(<FakeCommand>[
+          const FakeCommand(
+            command: <String>[
+              'killall',
+              '-9',
+              'Xcode',
+            ],
+          ),
+        ]);
+
+        xcodeDebug.startDebugActionProcess = FakeProcess();
+        xcodeDebug.currentDebuggingProject = project;
+
+        expect(xcodeDebug.startDebugActionProcess, isNotNull);
+        expect(xcodeDebug.currentDebuggingProject, isNotNull);
+
+        final bool exitStatus = await xcodeDebug.exit(force: true);
+
+        expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
+        expect(xcodeDebug.currentDebuggingProject, isNull);
+        expect(logger.errorText, isEmpty);
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(exitStatus, isTrue);
+      });
+
+      testWithoutContext('does not crash when deleting temporary directory that is nonexistant when force exiting', () async {
+        final Xcode xcode = setupXcode(
+          fakeProcessManager: FakeProcessManager.any(),
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        final XcodeDebugProject project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+          isTemporaryProject: true,
+        );
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager:FakeProcessManager.any(),
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+
+        xcodeDebug.startDebugActionProcess = FakeProcess();
+        xcodeDebug.currentDebuggingProject = project;
+
+        expect(xcodeDebug.startDebugActionProcess, isNotNull);
+        expect(xcodeDebug.currentDebuggingProject, isNotNull);
+        expect(projectDirectory.existsSync(), isFalse);
+        expect(xcodeproj.existsSync(), isFalse);
+        expect(xcworkspace.existsSync(), isFalse);
+
+        final bool status = await xcodeDebug.exit(force: true);
+
+        expect((xcodeDebug.startDebugActionProcess! as FakeProcess).killed, isTrue);
+        expect(xcodeDebug.currentDebuggingProject, isNull);
+        expect(projectDirectory.existsSync(), isFalse);
+        expect(xcodeproj.existsSync(), isFalse);
+        expect(xcworkspace.existsSync(), isFalse);
+        expect(logger.errorText, isEmpty);
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, isTrue);
+      });
+    });
+
+    group('stop app', () {
+      const String pathToXcodeApp = '/Applications/Xcode.app';
+
+      late Xcode xcode;
+      late Directory xcodeproj;
+      late Directory xcworkspace;
+      late XcodeDebugProject project;
+
+      setUp(() {
+        xcode = setupXcode(
+          fakeProcessManager: fakeProcessManager,
+          fileSystem: fileSystem,
+          flutterRoot: flutterRoot,
+        );
+        xcodeproj = fileSystem.directory('Runner.xcodeproj');
+        xcworkspace = fileSystem.directory('Runner.xcworkspace');
+        project = XcodeDebugProject(
+          scheme: 'Runner',
+          xcodeProject: xcodeproj,
+          xcodeWorkspace: xcworkspace,
+        );
+      });
+
+      testWithoutContext('succeeds with all optional flags', () async {
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'stop',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--close-window',
+              '--prompt-to-save'
+            ],
+            stdout: '''
+  {"status":true,"errorMessage":null,"debugResult":null}
+  ''',
+          ),
+        ]);
+
+        final bool status = await xcodeDebug.stopDebuggingApp(
+          project: project,
+          closeXcode: true,
+          promptToSaveOnClose: true,
+        );
+
+        expect(logger.errorText, isEmpty);
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, isTrue);
+      });
+
+      testWithoutContext('fails if osascript output returns false status', () async {
+        final XcodeDebug xcodeDebug = XcodeDebug(
+          logger: logger,
+          processManager: fakeProcessManager,
+          xcode: xcode,
+          fileSystem: fileSystem,
+        );
+        fakeProcessManager.addCommands(<FakeCommand>[
+          FakeCommand(
+            command: <String>[
+              'xcrun',
+              'osascript',
+              '-l',
+              'JavaScript',
+              pathToXcodeAutomationScript,
+              'stop',
+              '--xcode-path',
+              pathToXcodeApp,
+              '--project-path',
+              project.xcodeProject.path,
+              '--workspace-path',
+              project.xcodeWorkspace.path,
+              '--close-window',
+              '--prompt-to-save'
+            ],
+            stdout: '''
+  {"status":false,"errorMessage":"Failed to stop app","debugResult":null}
+  ''',
+          ),
+        ]);
+
+        final bool status = await xcodeDebug.stopDebuggingApp(
+          project: project,
+          closeXcode: true,
+          promptToSaveOnClose: true,
+        );
+
+        expect(logger.errorText, contains('Error stopping app in Xcode'));
+        expect(fakeProcessManager, hasNoRemainingExpectations);
+        expect(status, isFalse);
+      });
+    });
+  });
+
+  group('Debug project through Xcode with app bundle', () {
+    late BufferLogger logger;
+    late FakeProcessManager fakeProcessManager;
+    late MemoryFileSystem fileSystem;
+
+    const String flutterRoot = '/path/to/flutter';
+
+    setUp(() {
+      logger = BufferLogger.test();
+      fakeProcessManager = FakeProcessManager.empty();
+      fileSystem = MemoryFileSystem.test();
+    });
+
+    testUsingContext('creates temporary xcode project', () async {
+      final Xcode xcode = setupXcode(
+        fakeProcessManager: fakeProcessManager,
+        fileSystem: fileSystem,
+        flutterRoot: flutterRoot,
+      );
+
+      final XcodeDebug xcodeDebug = XcodeDebug(
+        logger: logger,
+        processManager: fakeProcessManager,
+        xcode: xcode,
+        fileSystem: globals.fs,
+      );
+
+      final Directory projectDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_empty_xcode.');
+
+      try {
+        final XcodeDebugProject project = await xcodeDebug.createXcodeProjectWithCustomBundle(
+          '/path/to/bundle',
+          templateRenderer: globals.templateRenderer,
+          projectDestination: projectDirectory,
+        );
+
+        final File schemeFile = projectDirectory
+            .childDirectory('Runner.xcodeproj')
+            .childDirectory('xcshareddata')
+            .childDirectory('xcschemes')
+            .childFile('Runner.xcscheme');
+
+        expect(project.scheme, 'Runner');
+        expect(project.xcodeProject.existsSync(), isTrue);
+        expect(project.xcodeWorkspace.existsSync(), isTrue);
+        expect(project.isTemporaryProject, isTrue);
+        expect(projectDirectory.childDirectory('Runner.xcodeproj').existsSync(), isTrue);
+        expect(projectDirectory.childDirectory('Runner.xcworkspace').existsSync(), isTrue);
+        expect(schemeFile.existsSync(), isTrue);
+        expect(schemeFile.readAsStringSync(), contains('FilePath = "/path/to/bundle"'));
+
+      } catch (err) { // ignore: avoid_catches_without_on_clauses
+        fail(err.toString());
+      } finally {
+        projectDirectory.deleteSync(recursive: true);
+      }
+    });
+  });
+}
+
+Xcode setupXcode({
+  required FakeProcessManager fakeProcessManager,
+  required FileSystem fileSystem,
+  required String flutterRoot,
+  bool xcodeSelect = true,
+}) {
+  fakeProcessManager.addCommand(const FakeCommand(
+    command: <String>['/usr/bin/xcode-select', '--print-path'],
+    stdout: '/Applications/Xcode.app/Contents/Developer',
+  ));
+
+  fileSystem.file('$flutterRoot/packages/flutter_tools/bin/xcode_debug.js').createSync(recursive: true);
+
+  final XcodeProjectInterpreter xcodeProjectInterpreter = XcodeProjectInterpreter.test(
+    processManager: FakeProcessManager.any(),
+    version: Version(14, 0, 0),
+  );
+
+  return Xcode.test(
+    processManager: fakeProcessManager,
+    xcodeProjectInterpreter: xcodeProjectInterpreter,
+    fileSystem: fileSystem,
+    flutterRoot: flutterRoot,
+  );
+}
+
+class FakeProcess extends Fake implements Process {
+  bool killed = false;
+
+  @override
+  bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
+    killed = true;
+    return true;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
index 90210c3..3dfe915 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcodeproj_test.dart
@@ -1306,5 +1306,75 @@
         expectedBuildNumber: '1',
       );
     });
+
+    group('CoreDevice', () {
+      testUsingContext('sets BUILD_DIR for core devices in debug mode', () async {
+        const BuildInfo buildInfo = BuildInfo.debug;
+        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+        await updateGeneratedXcodeProperties(
+          project: project,
+          buildInfo: buildInfo,
+          useMacOSConfig: true,
+          usingCoreDevice: true,
+        );
+
+        final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
+        expect(config.existsSync(), isTrue);
+
+        final String contents = config.readAsStringSync();
+        expect(contents, contains('\nBUILD_DIR=/build/ios\n'));
+      }, overrides: <Type, Generator>{
+        Artifacts: () => localIosArtifacts,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
+      });
+
+      testUsingContext('does not set BUILD_DIR for core devices in release mode', () async {
+        const BuildInfo buildInfo = BuildInfo.release;
+        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+        await updateGeneratedXcodeProperties(
+          project: project,
+          buildInfo: buildInfo,
+          useMacOSConfig: true,
+          usingCoreDevice: true,
+        );
+
+        final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
+        expect(config.existsSync(), isTrue);
+
+        final String contents = config.readAsStringSync();
+        expect(contents.contains('\nBUILD_DIR'), isFalse);
+      }, overrides: <Type, Generator>{
+        Artifacts: () => localIosArtifacts,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
+      });
+
+      testUsingContext('does not set BUILD_DIR for non core devices', () async {
+        const BuildInfo buildInfo = BuildInfo.debug;
+        final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
+        await updateGeneratedXcodeProperties(
+          project: project,
+          buildInfo: buildInfo,
+          useMacOSConfig: true,
+        );
+
+        final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
+        expect(config.existsSync(), isTrue);
+
+        final String contents = config.readAsStringSync();
+        expect(contents.contains('\nBUILD_DIR'), isFalse);
+      }, overrides: <Type, Generator>{
+        Artifacts: () => localIosArtifacts,
+        Platform: () => macOS,
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+        XcodeProjectInterpreter: () => xcodeProjectInterpreter,
+      });
+    });
   });
 }
diff --git a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
index 887712a..6df6b82 100644
--- a/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
+++ b/packages/flutter_tools/test/general.shard/macos/xcode_test.dart
@@ -4,16 +4,20 @@
 
 import 'dart:async';
 
+import 'package:file/memory.dart';
 import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/io.dart' show ProcessException;
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/user_messages.dart';
 import 'package:flutter_tools/src/base/version.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/ios/core_devices.dart';
 import 'package:flutter_tools/src/ios/devices.dart';
 import 'package:flutter_tools/src/ios/iproxy.dart';
+import 'package:flutter_tools/src/ios/xcode_debug.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/macos/xcdevice.dart';
 import 'package:flutter_tools/src/macos/xcode.dart';
@@ -75,7 +79,7 @@
         expect(fakeProcessManager, hasNoRemainingExpectations);
       });
 
-      testWithoutContext('isSimctlInstalled is true when simctl list fails', () {
+      testWithoutContext('isSimctlInstalled is false when simctl list fails', () {
         fakeProcessManager.addCommand(
           const FakeCommand(
             command: <String>[
@@ -97,6 +101,156 @@
         expect(fakeProcessManager, hasNoRemainingExpectations);
       });
 
+      group('isDevicectlInstalled', () {
+        testWithoutContext('is true when Xcode is 15+ and devicectl succeeds', () {
+          fakeProcessManager.addCommand(
+            const FakeCommand(
+              command: <String>[
+                'xcrun',
+                'devicectl',
+                '--version',
+              ],
+            ),
+          );
+          xcodeProjectInterpreter.version = Version(15, 0, 0);
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+          );
+
+          expect(xcode.isDevicectlInstalled, isTrue);
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+        });
+
+        testWithoutContext('is false when devicectl fails', () {
+          fakeProcessManager.addCommand(
+            const FakeCommand(
+              command: <String>[
+                'xcrun',
+                'devicectl',
+                '--version',
+              ],
+              exitCode: 1,
+            ),
+          );
+          xcodeProjectInterpreter.version = Version(15, 0, 0);
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+          );
+
+          expect(xcode.isDevicectlInstalled, isFalse);
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+        });
+
+        testWithoutContext('is false when Xcode is less than 15', () {
+          xcodeProjectInterpreter.version = Version(14, 0, 0);
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+          );
+
+          expect(xcode.isDevicectlInstalled, isFalse);
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+        });
+      });
+
+      group('pathToXcodeApp', () {
+        late UserMessages userMessages;
+
+        setUp(() {
+          userMessages = UserMessages();
+        });
+
+        testWithoutContext('parses correctly', () {
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+          );
+
+          fakeProcessManager.addCommand(const FakeCommand(
+            command: <String>['/usr/bin/xcode-select', '--print-path'],
+            stdout: '/Applications/Xcode.app/Contents/Developer',
+          ));
+
+          expect(xcode.xcodeAppPath, '/Applications/Xcode.app');
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+        });
+
+        testWithoutContext('throws error if not found', () {
+          final Xcode xcode = Xcode.test(
+            processManager: FakeProcessManager.any(),
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+          );
+
+          expect(
+            () => xcode.xcodeAppPath,
+            throwsToolExit(message: userMessages.xcodeMissing),
+          );
+        });
+
+        testWithoutContext('throws error with unexpected outcome', () {
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+          );
+
+          fakeProcessManager.addCommand(const FakeCommand(
+            command: <String>[
+              '/usr/bin/xcode-select',
+              '--print-path',
+            ],
+            stdout: '/Library/Developer/CommandLineTools',
+          ));
+
+          expect(
+            () => xcode.xcodeAppPath,
+            throwsToolExit(message: userMessages.xcodeMissing),
+          );
+          expect(fakeProcessManager, hasNoRemainingExpectations);
+        });
+      });
+
+      group('pathToXcodeAutomationScript', () {
+        const String flutterRoot = '/path/to/flutter';
+
+        late MemoryFileSystem fileSystem;
+
+        setUp(() {
+          fileSystem = MemoryFileSystem.test();
+        });
+
+        testWithoutContext('returns path when file is found', () {
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+            fileSystem: fileSystem,
+            flutterRoot: flutterRoot,
+          );
+
+          fileSystem.file('$flutterRoot/packages/flutter_tools/bin/xcode_debug.js').createSync(recursive: true);
+
+          expect(
+            xcode.xcodeAutomationScriptPath,
+            '$flutterRoot/packages/flutter_tools/bin/xcode_debug.js',
+          );
+        });
+
+        testWithoutContext('throws error when not found', () {
+          final Xcode xcode = Xcode.test(
+            processManager: fakeProcessManager,
+            xcodeProjectInterpreter: xcodeProjectInterpreter,
+            fileSystem: fileSystem,
+            flutterRoot: flutterRoot,
+          );
+
+          expect(() =>
+            xcode.xcodeAutomationScriptPath,
+            throwsToolExit()
+          );
+        });
+      });
+
       group('macOS', () {
         late Xcode xcode;
         late BufferLogger logger;
@@ -339,6 +493,7 @@
     group('xcdevice not installed', () {
       late XCDevice xcdevice;
       late Xcode xcode;
+      late MemoryFileSystem fileSystem;
 
       setUp(() {
         xcode = Xcode.test(
@@ -348,6 +503,7 @@
             version: null, // Not installed.
           ),
         );
+        fileSystem = MemoryFileSystem.test();
         xcdevice = XCDevice(
           processManager: fakeProcessManager,
           logger: logger,
@@ -356,6 +512,9 @@
           artifacts: Artifacts.test(),
           cache: Cache.test(processManager: FakeProcessManager.any()),
           iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
+          fileSystem: fileSystem,
+          coreDeviceControl: FakeIOSCoreDeviceControl(),
+          xcodeDebug: FakeXcodeDebug(),
         );
       });
 
@@ -373,9 +532,13 @@
     group('xcdevice', () {
       late XCDevice xcdevice;
       late Xcode xcode;
+      late MemoryFileSystem fileSystem;
+      late FakeIOSCoreDeviceControl coreDeviceControl;
 
       setUp(() {
         xcode = Xcode.test(processManager: FakeProcessManager.any());
+        fileSystem = MemoryFileSystem.test();
+        coreDeviceControl = FakeIOSCoreDeviceControl();
         xcdevice = XCDevice(
           processManager: fakeProcessManager,
           logger: logger,
@@ -384,6 +547,9 @@
           artifacts: Artifacts.test(),
           cache: Cache.test(processManager: FakeProcessManager.any()),
           iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
+          fileSystem: fileSystem,
+          coreDeviceControl: coreDeviceControl,
+          xcodeDebug: FakeXcodeDebug(),
         );
       });
 
@@ -1117,6 +1283,176 @@
         }, overrides: <Type, Generator>{
           Platform: () => macPlatform,
         });
+
+        group('with CoreDevices', () {
+          testUsingContext('returns devices with corresponding CoreDevices', () async {
+            const String devicesOutput = '''
+[
+  {
+    "simulator" : true,
+    "operatingSystemVersion" : "13.3 (17K446)",
+    "available" : true,
+    "platform" : "com.apple.platform.appletvsimulator",
+    "modelCode" : "AppleTV5,3",
+    "identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6",
+    "architecture" : "x86_64",
+    "modelName" : "Apple TV",
+    "name" : "Apple TV"
+  },
+  {
+    "simulator" : false,
+    "operatingSystemVersion" : "13.3 (17C54)",
+    "interface" : "usb",
+    "available" : true,
+    "platform" : "com.apple.platform.iphoneos",
+    "modelCode" : "iPhone8,1",
+    "identifier" : "00008027-00192736010F802E",
+    "architecture" : "arm64",
+    "modelName" : "iPhone 6s",
+    "name" : "An iPhone (Space Gray)"
+  },
+  {
+    "simulator" : false,
+    "operatingSystemVersion" : "10.1 (14C54)",
+    "interface" : "usb",
+    "available" : true,
+    "platform" : "com.apple.platform.iphoneos",
+    "modelCode" : "iPad11,4",
+    "identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
+    "architecture" : "armv7",
+    "modelName" : "iPad Air 3rd Gen",
+    "name" : "iPad 1"
+  },
+  {
+    "simulator" : false,
+    "operatingSystemVersion" : "10.1 (14C54)",
+    "interface" : "network",
+    "available" : true,
+    "platform" : "com.apple.platform.iphoneos",
+    "modelCode" : "iPad11,4",
+    "identifier" : "234234234234234234345445687594e089dede3c44",
+    "architecture" : "arm64",
+    "modelName" : "iPad Air 3rd Gen",
+    "name" : "A networked iPad"
+  },
+  {
+    "simulator" : false,
+    "operatingSystemVersion" : "10.1 (14C54)",
+    "interface" : "usb",
+    "available" : true,
+    "platform" : "com.apple.platform.iphoneos",
+    "modelCode" : "iPad11,4",
+    "identifier" : "f577a7903cc54959be2e34bc4f7f80b7009efcf4",
+    "architecture" : "BOGUS",
+    "modelName" : "iPad Air 3rd Gen",
+    "name" : "iPad 2"
+  },
+  {
+    "simulator" : true,
+    "operatingSystemVersion" : "6.1.1 (17S445)",
+    "available" : true,
+    "platform" : "com.apple.platform.watchsimulator",
+    "modelCode" : "Watch5,4",
+    "identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A",
+    "architecture" : "i386",
+    "modelName" : "Apple Watch Series 5 - 44mm",
+    "name" : "Apple Watch Series 5 - 44mm"
+  },
+  {
+    "simulator" : false,
+    "operatingSystemVersion" : "13.3 (17C54)",
+    "interface" : "usb",
+    "available" : false,
+    "platform" : "com.apple.platform.iphoneos",
+    "modelCode" : "iPhone8,1",
+    "identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
+    "architecture" : "arm64",
+    "modelName" : "iPhone 6s",
+    "name" : "iPhone",
+    "error" : {
+      "code" : -9,
+      "failureReason" : "",
+      "description" : "iPhone is not paired with your computer.",
+      "domain" : "com.apple.platform.iphoneos"
+    }
+  }
+]
+''';
+            coreDeviceControl.devices.addAll(<FakeIOSCoreDevice>[
+              FakeIOSCoreDevice(
+                udid: '00008027-00192736010F802E',
+                connectionInterface: DeviceConnectionInterface.wireless,
+                developerModeStatus: 'enabled',
+              ),
+              FakeIOSCoreDevice(
+                connectionInterface: DeviceConnectionInterface.wireless,
+                developerModeStatus: 'enabled',
+              ),
+              FakeIOSCoreDevice(
+                udid: '234234234234234234345445687594e089dede3c44',
+                connectionInterface: DeviceConnectionInterface.attached,
+              ),
+              FakeIOSCoreDevice(
+                udid: 'f577a7903cc54959be2e34bc4f7f80b7009efcf4',
+                connectionInterface: DeviceConnectionInterface.attached,
+                developerModeStatus: 'disabled',
+              ),
+            ]);
+
+            fakeProcessManager.addCommand(const FakeCommand(
+              command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
+              stdout: devicesOutput,
+            ));
+
+            final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
+            expect(devices, hasLength(5));
+            expect(devices[0].id, '00008027-00192736010F802E');
+            expect(devices[0].name, 'An iPhone (Space Gray)');
+            expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
+            expect(devices[0].cpuArchitecture, DarwinArch.arm64);
+            expect(devices[0].connectionInterface, DeviceConnectionInterface.wireless);
+            expect(devices[0].isConnected, true);
+            expect(devices[0].devModeEnabled, true);
+
+            expect(devices[1].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
+            expect(devices[1].name, 'iPad 1');
+            expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
+            expect(devices[1].cpuArchitecture, DarwinArch.armv7);
+            expect(devices[1].connectionInterface, DeviceConnectionInterface.attached);
+            expect(devices[1].isConnected, true);
+            expect(devices[1].devModeEnabled, true);
+
+            expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
+            expect(devices[2].name, 'A networked iPad');
+            expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
+            expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
+            expect(devices[2].connectionInterface, DeviceConnectionInterface.attached);
+            expect(devices[2].isConnected, true);
+            expect(devices[2].devModeEnabled, false);
+
+            expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
+            expect(devices[3].name, 'iPad 2');
+            expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
+            expect(devices[3].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
+            expect(devices[3].connectionInterface, DeviceConnectionInterface.attached);
+            expect(devices[3].isConnected, true);
+            expect(devices[3].devModeEnabled, false);
+
+            expect(devices[4].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
+            expect(devices[4].name, 'iPhone');
+            expect(await devices[4].sdkNameAndVersion, 'iOS 13.3 17C54');
+            expect(devices[4].cpuArchitecture, DarwinArch.arm64);
+            expect(devices[4].connectionInterface, DeviceConnectionInterface.attached);
+            expect(devices[4].isConnected, false);
+            expect(devices[4].devModeEnabled, true);
+
+            expect(fakeProcessManager, hasNoRemainingExpectations);
+          }, overrides: <Type, Generator>{
+            Platform: () => macPlatform,
+            Artifacts: () => Artifacts.test(),
+          });
+
+        });
       });
 
       group('diagnostics', () {
@@ -1312,3 +1648,41 @@
   @override
   List<String> xcrunCommand() => <String>['xcrun'];
 }
+
+class FakeXcodeDebug extends Fake implements XcodeDebug {}
+
+class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
+
+  List<FakeIOSCoreDevice> devices = <FakeIOSCoreDevice>[];
+
+  @override
+  Future<List<IOSCoreDevice>> getCoreDevices({Duration timeout = Duration.zero}) async {
+    return devices;
+  }
+}
+
+class FakeIOSCoreDevice extends Fake implements IOSCoreDevice {
+  FakeIOSCoreDevice({
+    this.udid,
+    this.connectionInterface,
+    this.developerModeStatus,
+  });
+
+  final String? developerModeStatus;
+
+  @override
+  final String? udid;
+
+  @override
+  final DeviceConnectionInterface? connectionInterface;
+
+  @override
+  IOSCoreDeviceProperties? get deviceProperties => FakeIOSCoreDeviceProperties(developerModeStatus: developerModeStatus);
+}
+
+class FakeIOSCoreDeviceProperties extends Fake implements IOSCoreDeviceProperties {
+  FakeIOSCoreDeviceProperties({required this.developerModeStatus});
+
+  @override
+  final String? developerModeStatus;
+}
diff --git a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
index b0f6085..8c68685 100644
--- a/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
+++ b/packages/flutter_tools/test/general.shard/mdns_discovery_test.dart
@@ -478,6 +478,18 @@
     });
 
     group('for launch', () {
+      testWithoutContext('Ensure either port or device name are provided', () async {
+        final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
+
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        expect(() async => portDiscovery.queryForLaunch(applicationId: 'app-id'), throwsAssertionError);
+      });
+
       testWithoutContext('No ports available', () async {
         final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
 
@@ -666,6 +678,93 @@
               message:'Did not find a Dart VM Service advertised for srv-bar on port 321.'),
         );
       });
+
+      testWithoutContext('Matches on application id and device name', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'srv-bar': <SrvResourceRecord>[
+              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'My-Phone.local'),
+            ],
+          },
+        );
+        final FakeIOSDevice device = FakeIOSDevice(
+          name: 'My Phone',
+        );
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
+          'srv-bar',
+          device,
+        );
+        expect(uri.toString(), 'http://127.0.0.1:123/');
+      });
+
+      testWithoutContext('Throw error if unable to find VM Service with app id and device name', () async {
+        final MDnsClient client = FakeMDnsClient(
+          <PtrResourceRecord>[
+            PtrResourceRecord('foo', future, domainName: 'srv-foo'),
+            PtrResourceRecord('bar', future, domainName: 'srv-bar'),
+            PtrResourceRecord('baz', future, domainName: 'srv-boo'),
+          ],
+          <String, List<SrvResourceRecord>>{
+            'srv-foo': <SrvResourceRecord>[
+              SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
+            ],
+          },
+        );
+        final FakeIOSDevice device = FakeIOSDevice(
+          name: 'My Phone',
+        );
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: client,
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(
+          portDiscovery.getVMServiceUriForLaunch(
+            'srv-bar',
+            device,
+          ),
+          throwsToolExit(
+              message:'Did not find a Dart VM Service advertised for srv-bar'),
+        );
+      });
+    });
+
+    group('deviceNameMatchesTargetName', () {
+      testWithoutContext('compares case insensitive and without spaces, hypthens, .local', () {
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: FakeMDnsClient(
+            <PtrResourceRecord>[],
+            <String, List<SrvResourceRecord>>{},
+          ),
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+
+        expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone.local'), isTrue);
+      });
+
+      testWithoutContext('includes numbers in comparison', () {
+        final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
+          mdnsClient: FakeMDnsClient(
+            <PtrResourceRecord>[],
+            <String, List<SrvResourceRecord>>{},
+          ),
+          logger: BufferLogger.test(),
+          flutterUsage: TestUsage(),
+        );
+        expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone-2.local'), isFalse);
+      });
     });
 
     testWithoutContext('Find firstMatchingVmService with many available and no application id', () async {
@@ -895,6 +994,11 @@
 // Until we fix that, we have to also ignore related lints here.
 // ignore: avoid_implementing_value_types
 class FakeIOSDevice extends Fake implements IOSDevice {
+  FakeIOSDevice({this.name = 'iPhone'});
+
+  @override
+  final String name;
+
   @override
   Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;