| 1 | # Calls for iPhone |
| 2 | |
| 3 | A SwiftUI iOS 17+ app using the same Go authentication/signaling core and native |
| 4 | WebRTC engine as Calls for Mac. No embedded webview is used. The Xcode project is |
| 5 | generated from `project.yml`; edit that specification and the Swift sources. |
| 6 | |
| 7 | ## Build |
| 8 | |
| 9 | Requires Xcode, Go, and XcodeGen (`brew install xcodegen`). Dependencies are pinned |
| 10 | in the shared Swift package and the isolated native Go module. |
| 11 | |
| 12 | ```sh |
| 13 | make iphone |
| 14 | # For a signed physical-device build: |
| 15 | CALLS_DEVELOPMENT_TEAM=YOUR_TEAM_ID make iphone-device |
| 16 | ``` |
| 17 | |
| 18 | Build products are `native/build/ios-simulator/Build/Products/Debug-iphonesimulator/CallsPhone.app` |
| 19 | and `native/build/ios-device/Build/Products/Debug-iphoneos/CallsPhone.app`. |
| 20 | The device build uses automatic development signing with your selected team; it |
| 21 | does not upload an app to App Store Connect or TestFlight. |
| 22 | |
| 23 | Generate/open the Xcode project for normal device development: |
| 24 | |
| 25 | ```sh |
| 26 | xcodegen generate --spec native/ios/project.yml |
| 27 | open native/ios/CallsPhone.xcodeproj |
| 28 | ``` |
| 29 | |
| 30 | Select your development team and connected iPhone, then Run. Alternatively, |
| 31 | install a signed app using `xcrun devicectl device install app --device DEVICE_ID` |
| 32 | followed by the device build's `.app` path. The iPhone must be connected and |
| 33 | available to Xcode, with Developer Mode enabled. |
| 34 | |
| 35 | ## First phone milestone |
| 36 | |
| 37 | - **Sign in** to the Calls server (calls.oficina.works unless changed with the |
| 38 | home screen's gear), or **Join a call as a guest** with a pasted invite link; |
| 39 | sign in in Safari/the default browser. |
| 40 | - Open the email link normally, compare the browser's code with Calls's code, |
| 41 | approve, then return to Calls. A member's session is kept in the Keychain |
| 42 | (this device only) and checked with the server at the next launch; see |
| 43 | [../README.md](../README.md). **Sign out** forgets it. |
| 44 | - Join using the front camera and microphone; the local video display is mirrored. |
| 45 | Sent video and remote video retain their original orientation. |
| 46 | - Native remote video, microphone/camera controls, guest admission, chat, and leave. |
| 47 | - Chat is always available beside the call controls. Its button shows new-message |
| 48 | counts; opening it shows the latest messages and a composer without scrolling |
| 49 | past the video tiles. Opening chat keeps the call active. |
| 50 | - Members have a Share button beside Chat, with Copy link and the system share menu. |
| 51 | - Camera/microphone permission refusals appear in the app. Retry after changing |
| 52 | permission in Settings. The simulator has no physical camera. |
| 53 | |
| 54 | Calls are foreground-only in this milestone. Switching away ends the call, |
| 55 | except while you are sharing your screen; sign-in survives switching to the |
| 56 | browser. Camera switching, CallKit/background calling, and distribution are |
| 57 | follow-up work. Physical camera, audible audio, Bluetooth routing, rotation while |
| 58 | capturing, and interruption recovery need a real-device acceptance pass. |
| 59 | |
| 60 | ## Sharing the screen |
| 61 | |
| 62 | **Share your screen** in the call is the system's broadcast button |
| 63 | (`RPSystemBroadcastPickerView`, offering only Calls' own extension). The whole |
| 64 | screen is shared, which is the only thing iOS lets any app share. The frames |
| 65 | reach a **broadcast upload extension** (`Broadcast/SampleHandler.swift`, target |
| 66 | `CallsBroadcast`), a separate process with a memory ceiling of about fifty |
| 67 | megabytes. It links only `CallsBroadcastKit` and no WebRTC, which keeps it near |
| 68 | 600 KB. It shrinks each frame to 1280 on the long side, at up to fifteen a second, |
| 69 | and passes it to the app over a Unix socket in the **App Group** container |
| 70 | (`calls.sock`). The app is already in the call and sends the frames on the same |
| 71 | `sendonly` screen transceiver the Mac and the browser use. Only one frame is in |
| 72 | flight at a time; any frame that arrives while one is being written is dropped, |
| 73 | never queued. |
| 74 | |
| 75 | Stopping works from either end. **Stop sharing** in the app closes the socket; |
| 76 | the extension's next write fails and it ends the broadcast with "You stopped |
| 77 | sharing your screen in Calls." Stopping from Control Center or the red status |
| 78 | bar closes the socket from the other side, and the app stops the share. A |
| 79 | broadcast started with no call to take it ends with "Join a call in Calls, then |
| 80 | share your screen from there." |
| 81 | |
| 82 | A call that is sharing stays up when you leave the app, because showing other |
| 83 | apps is the point. That needs the `audio` background mode. Every other call |
| 84 | still ends when the app goes to the background. If the broadcast is stopped |
| 85 | from outside while Calls is in the background, the call ends too. |
| 86 | |
| 87 | The App Group is one build setting, `CALLS_APP_GROUP` in `project.yml` |
| 88 | (`group.net.amadan.oficina.calls`). It feeds both targets' entitlements and the |
| 89 | `CallsAppGroup` key in both Info.plists. The extension's bundle identifier is the |
| 90 | app's plus `.broadcast`. The app looks for its extension by that rule, so the |
| 91 | two must stay in step. |
| 92 | |
| 93 | **Signing.** The simulator build is unsigned, so it has no App Group and cannot |
| 94 | broadcast; ReplayKit broadcasts need a device anyway. Before the first signed |
| 95 | device build, in the Apple Developer portal (Certificates, Identifiers & |
| 96 | Profiles): |
| 97 | |
| 98 | 1. Identifiers → App Groups → **+** → `group.net.amadan.oficina.calls`. |
| 99 | 2. Identifiers → `net.amadan.oficina.calls.ios` → enable **App Groups** → |
| 100 | choose that group. (Push stays as it is.) |
| 101 | 3. Identifiers → **+** → App IDs → App → `net.amadan.oficina.calls.ios.broadcast`, |
| 102 | description "Calls broadcast", with **App Groups** enabled and the same group. |
| 103 | 4. `CALLS_DEVELOPMENT_TEAM=<team> make iphone-device`. Automatic signing makes |
| 104 | both development profiles once the identifiers exist, and |
| 105 | `-allowProvisioningUpdates` can also create steps 1–3 for you if you would |
| 106 | rather let Xcode do it. |
| 107 | |
| 108 | A TestFlight or App Store build needs distribution profiles for **both** bundle |
| 109 | identifiers, and the extension goes up inside the app. |
| 110 | |
| 111 | **The TestFlight release branch differs.** `release/calls-testflight` ships |
| 112 | under the bundle identifier `works.oficina.meet`, not |
| 113 | `net.amadan.oficina.calls.ios`. When screen sharing reaches that branch it |
| 114 | needs, in its `project.yml`: |
| 115 | |
| 116 | - `CALLS_APP_GROUP: group.works.oficina.meet` |
| 117 | - the `CallsBroadcast` target's `PRODUCT_BUNDLE_IDENTIFIER: |
| 118 | works.oficina.meet.broadcast` (the app finds its extension as its own |
| 119 | identifier plus `.broadcast`, so this one is not a free choice) |
| 120 | |
| 121 | and, in the Apple Developer portal, the App Group `group.works.oficina.meet` |
| 122 | enabled on both `works.oficina.meet` and `works.oficina.meet.broadcast`, with |
| 123 | distribution profiles for both. The development values above stay as they are |
| 124 | on main. |
| 125 | |
| 126 | ## Verification |
| 127 | |
| 128 | ```sh |
| 129 | xcrun simctl list devices available |
| 130 | # Choose an iPhone simulator UUID: |
| 131 | CALLS_IOS_SIMULATOR=YOUR_SIMULATOR_UUID make iphone-browser |
| 132 | ``` |
| 133 | |
| 134 | The Go harness creates isolated databases, meetings, and captured magic-link |
| 135 | emails. iPhone UI tests use the real Safari handoff for members and guests and |
| 136 | compare the authorization codes before returning to the app. No production |
| 137 | credentials are used and no real email is sent. The UI tests launch with |
| 138 | `-CallsForgetSignIn YES`, which signs the app out before anything else: the |
| 139 | simulator's Keychain outlives reinstalls, and a session kept by an earlier run |
| 140 | against a harness server long gone would otherwise decide what the next run sees. |
| 141 | |
| 142 | The native media test creates two iOS WebRTC peers with a synthetic camera, |
| 143 | checks decoded video in both directions, continuity through three-second stream |
| 144 | renewals, and removal of the departed peer. Hardware audio is disabled in this |
| 145 | simulator-only test: it neither records the host microphone nor claims to prove |
| 146 | physical audio quality. Synthetic capture is compiled only into test targets. |
| 147 | Xcode logs and result bundles are retained under `native/build/`. |
| 148 | |