import CallsBroadcastKit import CallsKit import ReplayKit import SwiftUI /// Listens for the broadcast extension for as long as a call is on screen, /// and turns each broadcast into the call's screen share. /// /// The order is the reverse of the Mac's. There, the button asks and the /// capture answers; here the person starts a broadcast at the system's /// sheet, the extension connects, and only then does the call learn there /// is a screen to send. A broadcast that arrives when the call cannot take /// it — not yet admitted, or already sharing — is closed at once, and the /// extension ends it with a sentence of its own. @MainActor final class PhoneScreenShare: ObservableObject { private var inbox: BroadcastInbox? func listen(for call: Call, leave: @escaping () -> Void) { guard inbox == nil, let group = BroadcastPath.appGroup, let path = BroadcastPath.socket(appGroup: group) else { return } let inbox = BroadcastInbox(path: path) inbox.onConnection = { [weak call] connection in MainActor.assumeIsolated { guard let call, call.state?.phase == "admitted", !call.sharingScreen else { connection.close() return } let capture = BroadcastScreenCapture(connection) // A broadcast stopped from Control Center while the app is in the // background ends the only reason the call was still up there. capture.afterEnded = { if UIApplication.shared.applicationState == .background { leave() } } Task { await call.shareScreen(capture) } } } do { try inbox.start() self.inbox = inbox } catch { // No App Group on this build (the simulator's unsigned one, say): // the button still shows, and the extension says it could not // reach a call. Nothing else in the call depends on this. } } func stop() { inbox?.stop() inbox = nil } } /// The system's broadcast button, dressed as the call's other controls. /// /// RPSystemBroadcastPickerView is the only way an app may start a /// broadcast, and it only responds to a tap on its own button. So the /// picker is the button: its image is replaced with the call's symbol and /// tint, and it is sized like a CallActionStyle control beside the others. /// `preferredExtension` makes the sheet offer Calls' own extension rather /// than every broadcast service on the phone. struct BroadcastButton: UIViewRepresentable { static let extensionIdentifier = Bundle.main.bundleIdentifier.map { $0 + ".broadcast" } func makeUIView(context: Context) -> RPSystemBroadcastPickerView { let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44)) picker.preferredExtension = Self.extensionIdentifier picker.showsMicrophoneButton = false picker.accessibilityLabel = "Share your screen" style(picker) return picker } func updateUIView(_ picker: RPSystemBroadcastPickerView, context: Context) { style(picker) } private func style(_ picker: RPSystemBroadcastPickerView) { guard let button = picker.subviews.compactMap({ $0 as? UIButton }).first else { return } let symbol = UIImage( systemName: "rectangle.on.rectangle", withConfiguration: UIImage.SymbolConfiguration(textStyle: .title3)) button.setImage(symbol, for: .normal) button.tintColor = .systemOrange button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.12) button.layer.cornerRadius = 14 button.accessibilityLabel = "Share your screen" button.frame = picker.bounds button.autoresizingMask = [.flexibleWidth, .flexibleHeight] } }