You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中无需全局添加onReceive实现来电界面全局弹窗方案

问题

我正在开发一款聊天应用,来电时需要展示来电界面。目前通过NotificationCenter实现了来电接听逻辑,相关代码如下:

定义通知与Modal数据结构

public extension Notification.Name {
static let incomingCallView = Notification.Name("incomingCallView")
}

struct ModalData {
let onDismiss: (() -> Void)?
let content: (Binding<Bool>) -> AnyView

init<Content: View>(onDismiss: (() -> Void)? = nil,
                    @ViewBuilder content: @escaping (Binding<Bool>) -> Content) {
    self.onDismiss = onDismiss
    self.content = { AnyView(content($0)) }
}

static let empty = ModalData { _ in EmptyView() }
}

CXProvider代理中的来电处理实现

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
  guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
    action.fail()
    return
  }
    
    callOption.ringingPeriod?.invalidate()
    callOption.ringingPeriod = nil
    socket.inizalizeWebrtc(webRTCClient: WebRTCClient(iceServers: Config.default.webRTCIceServers))
    socket.webRTCClient?.offer {(sdp) in
    self.socket.sendSdp(sdp: sdp, type: "offer", callType: self.callOption.callType == .videoCall
                            ? "video"
                            : "audio"
        )
     
    }
 
    
    call.answer()

   NotificationCenter.default.post(name: .incomingCallView,
                                   object: ModalData(onDismiss: {

    }) { isPresented in

            IncomingCallScreen(callScreenIsPresented: isPresented)
        
       })


  action.fulfill()
}

当前问题:必须在所有视图中添加以下代码才能接收到通知并弹出来电界面,否则无法触发展示:

.onReceive(NotificationCenter.default.publisher(for: .incomingCallView)) { notif in
              if let data = notif.object as? ModalData {
                modalData = data
                incomingCallView = true
              }
            }
            .fullScreenCover(isPresented: $incomingCallView,
                             onDismiss: modalData.onDismiss) {
              modalData.content($incomingCallView)
            }

希望找到无需在所有视图中添加.onReceive就能全局展示来电界面的解决方案。


解决方案

方案1:用全局环境对象管理来电状态

创建专门的ObservableObject管理来电状态,注入到App根环境后,所有子视图无需重复监听,仅在根容器处理一次展示逻辑。

  1. 来电状态管理类:
class CallStateManager: ObservableObject {
    @Published var shouldShowIncomingCall: Bool = false
    @Published var currentModalData: ModalData = .empty
    
    init() {
        NotificationCenter.default.addObserver(forName: .incomingCallView, object: nil, queue: .main) { [weak self] notification in
            guard let data = notification.object as? ModalData else { return }
            self?.currentModalData = data
            self?.shouldShowIncomingCall = true
        }
    }
    
    func dismissCallView() {
        shouldShowIncomingCall = false
        currentModalData.onDismiss?()
        currentModalData = .empty
    }
}
  1. 在App入口注入环境对象:
@main
struct ChatApp: App {
    @StateObject private var callStateManager = CallStateManager()
    
    var body: some Scene {
        WindowGroup {
            RootView()
                .environmentObject(callStateManager)
        }
    }
}
  1. 根视图中添加全局fullScreenCover:
struct RootView: View {
    @EnvironmentObject var callStateManager: CallStateManager
    
    var body: some View {
        MainContentView() // 你的主内容视图(如TabView/NavigationStack)
            .fullScreenCover(isPresented: $callStateManager.shouldShowIncomingCall,
                             onDismiss: callStateManager.currentModalData.onDismiss) {
                callStateManager.currentModalData.content($callStateManager.shouldShowIncomingCall)
            }
    }
}

方案2:全局Overlay容器视图

通过自定义根容器,将来电界面作为全局Overlay添加,同样仅需在根容器处理一次状态逻辑。

  1. 复用方案1的CallStateManager,创建全局Overlay容器:
struct GlobalOverlayContainer<Content: View>: View {
    @EnvironmentObject var callStateManager: CallStateManager
    let content: Content
    
    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }
    
    var body: some View {
        content
            .overlay(
                Group {
                    if callStateManager.shouldShowIncomingCall {
                        callStateManager.currentModalData.content($callStateManager.shouldShowIncomingCall)
                            .edgesIgnoringSafeArea(.all)
                    }
                }
            )
    }
}
  1. App入口使用该容器:
@main
struct ChatApp: App {
    @StateObject private var callStateManager = CallStateManager()
    
    var body: some Scene {
        WindowGroup {
            GlobalOverlayContainer {
                MainContentView()
            }
            .environmentObject(callStateManager)
        }
    }
}

方案3:直接用UIWindow展示(跨SwiftUI/UIKit)

完全脱离SwiftUI视图栈限制,直接创建UIWindow展示来电界面,不受当前视图层级影响。

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    // ... 原有业务代码 ...
    
    call.answer()
    
    DispatchQueue.main.async {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.windowLevel = .alert + 1 // 确保界面层级最高
        // 给来电界面添加关闭回调
        let callView = IncomingCallScreen(callScreenIsPresented: .constant(true)) {
            window.isHidden = true
            window.rootViewController = nil
            callManager.activeCallWindow = nil // 释放引用避免内存泄漏
        }
        window.rootViewController = UIHostingController(rootView: callView)
        window.makeKeyAndVisible()
        
        // 在CallManager或全局单例中持有window引用
        callManager.activeCallWindow = window
    }
    
    action.fulfill()
}

内容的提问来源于stack exchange,提问作者Mostafa ALZOUBI

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 08:35:29