# `MobDev.IosLayoutPlist`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/ios_layout_plist.ex#L1)

The Info.plist keys that decide where an iOS app runs and how it may lay
out: `UIDeviceFamily` (iPhone `1`, iPad `2`), `UISupportedInterfaceOrientations`
(iPhone), `UISupportedInterfaceOrientations~ipad`, and `UIRequiresFullScreen`.

## `mob.exs` overrides

Three `config :mob_dev` keys override the project's `ios/Info.plist` in the
built app bundle (the dev simulator and device builds and `mix mob.release`).
The project file is never rewritten, so the setting reaches new and existing
apps alike:

  * `ios_target_devices` — `[:iphone, :ipad]` (universal) or `[:iphone]`.
    Sets `UIDeviceFamily`. Without iPad, an iPad runs the app letterboxed
    in iPhone compatibility mode, and it can't join Split View. An App Store
    app that has shipped iPad support can't drop it in an update.
  * `ios_orientations` — `:all`, `:portrait` or `:landscape`. Sets the
    iPhone `UISupportedInterfaceOrientations` (and removes any
    `UISupportedInterfaceOrientations~iphone`). iPad always declares all four
    (`UISupportedInterfaceOrientations~ipad`): iPadOS 26 rotates resizable
    apps freely, and iPad multitasking requires all four.
  * `multi_window` — `true` or `false` (MOB-245). `true` sets
    `UIApplicationSceneManifest` → `UIApplicationSupportsMultipleScenes`, so
    iPad users can open several windows of the app, each with its own
    navigation (`Mob.Scene` in mob). It needs the plist's existing
    `UISceneConfigurations` → `UIWindowSceneSessionRoleApplication` entry
    (the generated `SceneDelegate`); the build refuses a plist without one
    rather than add a manifest that would switch the app to a scene
    lifecycle with no delegate. `false` removes the
    key, which iOS reads as `false`. Needs a mob release with `Mob.Scene`.

Unset keys leave `ios/Info.plist` as written. `UIRequiresFullScreen` has no
override: the only supported value is `false` (or no key), and
`mix mob.doctor` warns when a plist sets it.

See `decisions/2026-10-01-ios-layout-plist-keys.md`.

# `check`

```elixir
@type check() :: {:ok | :warn | :fail, String.t(), String.t(), String.t() | nil}
```

One `mix mob.doctor` row.

# `keys`

```elixir
@type keys() :: %{
  device_family: [integer()] | nil,
  orientations: [String.t()] | nil,
  iphone_orientations: [String.t()] | nil,
  ipad_orientations: [String.t()] | nil,
  requires_full_screen: boolean() | nil,
  scene_configurations?: boolean()
}
```

The layout keys of a parsed Info.plist; `nil` when the key is absent.

# `apply!`

```elixir
@spec apply!(
  Path.t(),
  keyword()
) :: :ok
```

Applies `plist_commands/1` to the Info.plist at `path` with PlistBuddy
(macOS). Raises when a command other than `Delete` fails.

# `audit`

```elixir
@spec audit(
  String.t(),
  keyword()
) :: [check()]
```

The `mix mob.doctor` rows for an app's Info.plist text and `mob.exs`
config: one `:ok` row when the app can run full-screen on iPad, rotate and
join Split View, otherwise a `:warn` per problem with the exact fix.
Problems a `mob.exs` setting chose on purpose (`ios_target_devices:
[:iphone]`, `ios_orientations: :portrait`) aren't warned about.

# `plist_commands`

```elixir
@spec plist_commands(keyword()) :: [String.t()]
```

The PlistBuddy commands that stamp the `mob.exs` overrides into a bundle's
Info.plist, in order. A `Delete` of a key the plist lacks fails harmlessly;
every other command must succeed, including the `Print` that checks a
`multi_window: true` plist has an application scene configuration. Raises
`Mix.Error` on an invalid setting.

# `read`

```elixir
@spec read(String.t()) :: {:ok, keys()} | {:error, String.t()}
```

Reads the layout keys from an XML Info.plist. `{:error, reason}` when the
text isn't an XML property list (a binary plist, say).

# `settings`

```elixir
@spec settings(keyword()) ::
  {:ok,
   %{
     device_family: [integer()] | nil,
     orientations: [String.t()] | nil,
     multi_window: boolean() | nil
   }}
  | {:error, String.t()}
```

Validates the `mob.exs` overrides. Returns the resolved settings, `nil` for
each key that is unset.

# `top_level_keys`

```elixir
@spec top_level_keys(String.t()) :: {:ok, [String.t()]} | {:error, String.t()}
```

The keys of an XML property list's top-level `<dict>`, parsed structurally
(comments, CDATA and nested dictionaries contribute nothing).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
