| 1 | import CallsBroadcastKit |
| 2 | import CallsKit |
| 3 | import ReplayKit |
| 4 | import SwiftUI |
| 5 | |
| 6 | /// Listens for the broadcast extension for as long as a call is on screen, |
| 7 | /// and turns each broadcast into the call's screen share. |
| 8 | /// |
| 9 | /// The order is the reverse of the Mac's. There, the button asks and the |
| 10 | /// capture answers; here the person starts a broadcast at the system's |
| 11 | /// sheet, the extension connects, and only then does the call learn there |
| 12 | /// is a screen to send. A broadcast that arrives when the call cannot take |
| 13 | /// it — not yet admitted, or already sharing — is closed at once, and the |
| 14 | /// extension ends it with a sentence of its own. |
| 15 | @MainActor final class PhoneScreenShare: ObservableObject { |
| 16 | private var inbox: BroadcastInbox? |
| 17 | |
| 18 | func listen(for call: Call, leave: @escaping () -> Void) { |
| 19 | guard inbox == nil, let group = BroadcastPath.appGroup, |
| 20 | let path = BroadcastPath.socket(appGroup: group) |
| 21 | else { return } |
| 22 | let inbox = BroadcastInbox(path: path) |
| 23 | inbox.onConnection = { [weak call] connection in |
| 24 | MainActor.assumeIsolated { |
| 25 | guard let call, call.state?.phase == "admitted", !call.sharingScreen else { |
| 26 | connection.close() |
| 27 | return |
| 28 | } |
| 29 | let capture = BroadcastScreenCapture(connection) |
| 30 | // A broadcast stopped from Control Center while the app is in the |
| 31 | // background ends the only reason the call was still up there. |
| 32 | capture.afterEnded = { |
| 33 | if UIApplication.shared.applicationState == .background { leave() } |
| 34 | } |
| 35 | Task { await call.shareScreen(capture) } |
| 36 | } |
| 37 | } |
| 38 | do { |
| 39 | try inbox.start() |
| 40 | self.inbox = inbox |
| 41 | } catch { |
| 42 | // No App Group on this build (the simulator's unsigned one, say): |
| 43 | // the button still shows, and the extension says it could not |
| 44 | // reach a call. Nothing else in the call depends on this. |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | func stop() { |
| 49 | inbox?.stop() |
| 50 | inbox = nil |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | /// The system's broadcast button, dressed as the call's other controls. |
| 55 | /// |
| 56 | /// RPSystemBroadcastPickerView is the only way an app may start a |
| 57 | /// broadcast, and it only responds to a tap on its own button. So the |
| 58 | /// picker is the button: its image is replaced with the call's symbol and |
| 59 | /// tint, and it is sized like a CallActionStyle control beside the others. |
| 60 | /// `preferredExtension` makes the sheet offer Calls' own extension rather |
| 61 | /// than every broadcast service on the phone. |
| 62 | struct BroadcastButton: UIViewRepresentable { |
| 63 | static let extensionIdentifier = Bundle.main.bundleIdentifier.map { $0 + ".broadcast" } |
| 64 | |
| 65 | func makeUIView(context: Context) -> RPSystemBroadcastPickerView { |
| 66 | let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) |
| 67 | picker.preferredExtension = Self.extensionIdentifier |
| 68 | picker.showsMicrophoneButton = false |
| 69 | picker.accessibilityLabel = "Share your screen" |
| 70 | style(picker) |
| 71 | return picker |
| 72 | } |
| 73 | func updateUIView(_ picker: RPSystemBroadcastPickerView, context: Context) { style(picker) } |
| 74 | |
| 75 | private func style(_ picker: RPSystemBroadcastPickerView) { |
| 76 | guard let button = picker.subviews.compactMap({ $0 as? UIButton }).first else { return } |
| 77 | let symbol = UIImage( |
| 78 | systemName: "rectangle.on.rectangle", |
| 79 | withConfiguration: UIImage.SymbolConfiguration(textStyle: .title3)) |
| 80 | button.setImage(symbol, for: .normal) |
| 81 | button.tintColor = .systemOrange |
| 82 | button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.12) |
| 83 | button.layer.cornerRadius = 14 |
| 84 | button.accessibilityLabel = "Share your screen" |
| 85 | button.frame = picker.bounds |
| 86 | button.autoresizingMask = [.flexibleWidth, .flexibleHeight] |
| 87 | } |
| 88 | } |
| 89 | |