lib/live_view_native_swift_ui/platform.ex

defmodule LiveViewNativeSwiftUi.Platform do
  import LiveViewNativePlatform.Utils,
    only: [check_dependency!: 1, check_platform!: 2, run_command!: 3]

  defstruct [
    :app_name,
    :bundle_name,
    :project_path,
    default_simulator_device: "iPhone 13",
    default_simulator_os: "iOS",
    default_simulator_os_version: "16-0",
    xcode_path: "/Applications/Xcode.app"
  ]

  defimpl LiveViewNativePlatform do
    require Logger

    def context(_struct) do
      %LiveViewNativePlatform.Context{
        modifiers: %LiveViewNativeSwiftUi.Modifiers{},
        platform_id: :ios,
        template_engine: LiveViewNative.Engine,
        template_namespace: SwiftUi
      }
    end

    def start_simulator(struct, opts \\ []) do
      %{
        app_name: app_name,
        bundle_name: bundle_name,
        default_simulator_device: default_simulator_device,
        default_simulator_os: default_simulator_os,
        default_simulator_os_version: default_simulator_os_version,
        project_path: project_path,
        xcode_path: xcode_path
      } = struct

      # Raises if either `xcrun` or `xcodebuild` are missing from the current system PATH.
      # These are requirements to run the Simulator
      check_dependency!("xcrun")
      check_dependency!("xcodebuild")

      # Raises if `LiveViewNativeSwiftUi.Platform` isn't configured to support Simulator.
      check_platform!(struct, %{
        app_name: [:must_be_string],
        bundle_name: [:must_be_string],
        project_path: [:must_be_string, :must_point_to_directory]
      })

      # Get Simulator options from arguments, falling back to the `LiveViewNativeSwiftUi.Platform`
      # configuration or defaults.
      simulator_device = Keyword.get(opts, :device, default_simulator_device)
      simulator_os = Keyword.get(opts, :os, default_simulator_os)
      simulator_os_version = Keyword.get(opts, :os_version, default_simulator_os_version)

      # Get the Simulator device information using the result of `xcrun simctl list --json devices available`
      Logger.info("Checking for available devices.")

      %{"devices" => devices} =
        run_command!("xcrun", ["simctl", "list", "--json", "devices", "available"], format: :json)

      devices_for_os =
        Map.get(
          devices,
          "com.apple.CoreSimulator.SimRuntime.#{simulator_os}-#{simulator_os_version}",
          []
        )

      device_info = Enum.find(devices_for_os, &(&1["name"] == simulator_device))

      # Run the app in the specified Simulator if it exists.
      case device_info do
        %{"udid" => udid} ->
          # Setup a build path
          live_view_native_priv_dir = :code.priv_dir(:live_view_native)
          platform_builds_path = Path.join(live_view_native_priv_dir, "platform_builds")
          File.mkdir(platform_builds_path)
          build_path = Path.join(platform_builds_path, "#{app_name}.#{udid}")
          File.mkdir(build_path)

          # Build a project bundle for the Simulator
          Logger.info(
            "Building #{app_name}.app for #{simulator_device} (#{simulator_os} #{simulator_os_version})..."
          )

          run_command!(
            "xcrun",
            [
              "xcodebuild",
              "-scheme",
              app_name,
              "-project",
              Path.expand(project_path),
              "-configuration",
              "Debug",
              "-destination",
              "id=#{udid}",
              "-derivedDataPath",
              build_path
            ],
            []
          )

          # Run the Simulator
          Logger.info(
            "Running simulator for #{simulator_device} (#{simulator_os} #{simulator_os_version})..."
          )

          simulator_path =
            Path.join(
              xcode_path,
              "Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator"
            )

          run_command!(simulator_path, ["-CurrentDeviceUDID", udid], [])

          # Install the project bundle in the Simulator and launch it
          Logger.info("Running #{app_name}.app in simulator...")

          run_command!(
            "xcrun",
            [
              "simctl",
              "install",
              udid,
              "#{build_path}/Build/Products/Debug-iphonesimulator/#{app_name}.app"
            ],
            []
          )

          run_command!("xcrun", ["simctl", "launch", udid, bundle_name], [])

        device_info ->
          raise "Missing or invalid Simulator device: #{device_info}"
      end
    end
  end
end