Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
325 lines
9.7 KiB
Swift
325 lines
9.7 KiB
Swift
//
|
||
// LivePushReadinessViewModel.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/25.
|
||
//
|
||
|
||
import AVFoundation
|
||
import Foundation
|
||
import Network
|
||
|
||
/// 直播推流权限状态。
|
||
enum LivePushPermissionState: Equatable {
|
||
case unknown
|
||
case granted
|
||
case denied
|
||
|
||
var displayText: String {
|
||
switch self {
|
||
case .unknown: "未检查"
|
||
case .granted: "已授权"
|
||
case .denied: "未授权"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 直播推流网络状态。
|
||
enum LivePushNetworkState: Equatable {
|
||
case unknown
|
||
case unavailable
|
||
case wifi
|
||
case cellular
|
||
case other
|
||
|
||
var displayText: String {
|
||
switch self {
|
||
case .unknown: "检测中"
|
||
case .unavailable: "网络不可用"
|
||
case .wifi: "Wi-Fi"
|
||
case .cellular: "蜂窝网络"
|
||
case .other: "其他网络"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 推流 SDK 适配协议。后续接入真实 RTMP/RTC SDK 时替换默认实现。
|
||
protocol LivePushAdapter {
|
||
var name: String { get }
|
||
var isAvailable: Bool { get }
|
||
/// prepare 业务逻辑。
|
||
func prepare(pushURL: URL) async throws
|
||
/// start 业务逻辑。
|
||
func start() async throws
|
||
/// stop 业务逻辑。
|
||
func stop() async throws
|
||
/// dispose 业务逻辑。
|
||
func dispose() async
|
||
}
|
||
|
||
/// 权限提供者协议,隔离 AVFoundation 便于测试。
|
||
protocol LivePermissionProviding {
|
||
/// camera权限相关逻辑。
|
||
func cameraPermission() async -> LivePushPermissionState
|
||
/// microphone权限相关逻辑。
|
||
func microphonePermission() async -> LivePushPermissionState
|
||
}
|
||
|
||
/// 网络监听协议,隔离 NWPathMonitor 便于测试。
|
||
protocol LiveNetworkMonitoring: AnyObject {
|
||
var currentState: LivePushNetworkState { get }
|
||
/// start 业务逻辑。
|
||
func start(_ onChange: @escaping @Sendable (LivePushNetworkState) -> Void)
|
||
/// stop 业务逻辑。
|
||
func stop()
|
||
}
|
||
|
||
/// LivePushReadiness错误类型定义。
|
||
enum LivePushReadinessError: LocalizedError, Equatable {
|
||
case missingPushURL
|
||
case invalidPushURL
|
||
case permissionDenied
|
||
case networkUnavailable
|
||
case sdkUnavailable
|
||
|
||
var errorDescription: String? {
|
||
switch self {
|
||
case .missingPushURL:
|
||
"暂无推流地址"
|
||
case .invalidPushURL:
|
||
"推流地址格式无效"
|
||
case .permissionDenied:
|
||
"请先开启相机和麦克风权限"
|
||
case .networkUnavailable:
|
||
"当前网络不可用"
|
||
case .sdkUnavailable:
|
||
"当前版本未接入真推流 SDK"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// UnsupportedLivePushAdapter,业务类型定义。
|
||
struct UnsupportedLivePushAdapter: LivePushAdapter {
|
||
let name = "未接入推流 SDK"
|
||
let isAvailable = false
|
||
|
||
/// prepare 业务逻辑。
|
||
func prepare(pushURL: URL) async throws {
|
||
throw LivePushReadinessError.sdkUnavailable
|
||
}
|
||
|
||
/// start 业务逻辑。
|
||
func start() async throws {
|
||
throw LivePushReadinessError.sdkUnavailable
|
||
}
|
||
|
||
/// stop 业务逻辑。
|
||
func stop() async throws {}
|
||
|
||
/// dispose 业务逻辑。
|
||
func dispose() async {}
|
||
}
|
||
|
||
/// SystemLivePermission能力提供者,封装外部依赖。
|
||
struct SystemLivePermissionProvider: LivePermissionProviding {
|
||
/// camera权限相关逻辑。
|
||
func cameraPermission() async -> LivePushPermissionState {
|
||
await permission(for: .video)
|
||
}
|
||
|
||
/// microphone权限相关逻辑。
|
||
func microphonePermission() async -> LivePushPermissionState {
|
||
await permission(for: .audio)
|
||
}
|
||
|
||
/// permission 业务逻辑。
|
||
private func permission(for mediaType: AVMediaType) async -> LivePushPermissionState {
|
||
switch AVCaptureDevice.authorizationStatus(for: mediaType) {
|
||
case .authorized:
|
||
return .granted
|
||
case .notDetermined:
|
||
let granted = await AVCaptureDevice.requestAccess(for: mediaType)
|
||
return granted ? .granted : .denied
|
||
case .denied, .restricted:
|
||
return .denied
|
||
@unknown default:
|
||
return .denied
|
||
}
|
||
}
|
||
}
|
||
|
||
/// SystemLiveNetworkMonitor 类型定义。
|
||
final class SystemLiveNetworkMonitor: LiveNetworkMonitoring {
|
||
|
||
var onChange: (() -> Void)?
|
||
private let monitor = NWPathMonitor()
|
||
private let queue = DispatchQueue(label: "com.suixinkan.live.network")
|
||
private(set) var currentState: LivePushNetworkState = .unknown
|
||
|
||
/// start 业务逻辑。
|
||
func start(_ onChange: @escaping @Sendable (LivePushNetworkState) -> Void) {
|
||
monitor.pathUpdateHandler = { [weak self] path in
|
||
let state = Self.state(from: path)
|
||
self?.currentState = state
|
||
onChange(state)
|
||
}
|
||
monitor.start(queue: queue)
|
||
}
|
||
|
||
/// stop 业务逻辑。
|
||
func stop() {
|
||
monitor.cancel()
|
||
}
|
||
|
||
/// state 业务逻辑。
|
||
private static func state(from path: NWPath) -> LivePushNetworkState {
|
||
guard path.status == .satisfied else { return .unavailable }
|
||
if path.usesInterfaceType(.wifi) { return .wifi }
|
||
if path.usesInterfaceType(.cellular) { return .cellular }
|
||
return .other
|
||
}
|
||
}
|
||
|
||
@MainActor
|
||
/// 推流准备 ViewModel,检查权限、网络和默认 SDK 可用性。
|
||
final class LivePushReadinessViewModel {
|
||
var onChange: (() -> Void)?
|
||
var cameraPermission: LivePushPermissionState = .unknown { didSet { onChange?() } }
|
||
var microphonePermission: LivePushPermissionState = .unknown { didSet { onChange?() } }
|
||
var networkState: LivePushNetworkState = .unknown { didSet { onChange?() } }
|
||
var prepared = false { didSet { onChange?() } }
|
||
var running = false { didSet { onChange?() } }
|
||
var errorMessage: String? { didSet { onChange?() } }
|
||
|
||
let adapterName: String
|
||
let sdkStatusText: String
|
||
|
||
private let permissionProvider: any LivePermissionProviding
|
||
private let networkMonitor: any LiveNetworkMonitoring
|
||
private let adapter: any LivePushAdapter
|
||
private var pushURL: URL?
|
||
|
||
/// 初始化实例。
|
||
init(
|
||
permissionProvider: any LivePermissionProviding = SystemLivePermissionProvider(),
|
||
networkMonitor: any LiveNetworkMonitoring = SystemLiveNetworkMonitor(),
|
||
adapter: any LivePushAdapter = UnsupportedLivePushAdapter()
|
||
) {
|
||
self.permissionProvider = permissionProvider
|
||
self.networkMonitor = networkMonitor
|
||
self.adapter = adapter
|
||
self.adapterName = adapter.name
|
||
self.sdkStatusText = adapter.isAvailable ? "已接入" : "未接入真推流 SDK"
|
||
self.networkState = networkMonitor.currentState
|
||
}
|
||
|
||
/// 配置展示内容。
|
||
func configure(pushURL rawValue: String) {
|
||
let value = rawValue.liveTrimmed
|
||
guard !value.isEmpty else {
|
||
pushURL = nil
|
||
errorMessage = LivePushReadinessError.missingPushURL.localizedDescription
|
||
return
|
||
}
|
||
guard let url = URL(string: value), url.scheme?.isEmpty == false else {
|
||
pushURL = nil
|
||
errorMessage = LivePushReadinessError.invalidPushURL.localizedDescription
|
||
return
|
||
}
|
||
pushURL = url
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// startMonitoring相关逻辑。
|
||
func startMonitoring() {
|
||
networkState = networkMonitor.currentState
|
||
networkMonitor.start { [weak self] state in
|
||
Task { @MainActor in
|
||
self?.networkState = state
|
||
}
|
||
}
|
||
}
|
||
|
||
/// stopMonitoring相关逻辑。
|
||
func stopMonitoring() {
|
||
networkMonitor.stop()
|
||
}
|
||
|
||
/// refreshPermissions相关逻辑。
|
||
func refreshPermissions() async {
|
||
async let camera = permissionProvider.cameraPermission()
|
||
async let microphone = permissionProvider.microphonePermission()
|
||
cameraPermission = await camera
|
||
microphonePermission = await microphone
|
||
}
|
||
|
||
/// runDiagnostics相关逻辑。
|
||
func runDiagnostics() throws {
|
||
do {
|
||
try validateReadiness()
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
throw error
|
||
}
|
||
guard adapter.isAvailable else {
|
||
errorMessage = LivePushReadinessError.sdkUnavailable.localizedDescription
|
||
throw LivePushReadinessError.sdkUnavailable
|
||
}
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// prepare 业务逻辑。
|
||
func prepare() async throws {
|
||
try validateReadiness()
|
||
guard let pushURL else { throw LivePushReadinessError.missingPushURL }
|
||
do {
|
||
try await adapter.prepare(pushURL: pushURL)
|
||
prepared = true
|
||
errorMessage = nil
|
||
} catch {
|
||
errorMessage = error.localizedDescription
|
||
throw error
|
||
}
|
||
}
|
||
|
||
/// start推送相关逻辑。
|
||
func startPush() async throws {
|
||
try validateReadiness()
|
||
guard adapter.isAvailable else {
|
||
errorMessage = LivePushReadinessError.sdkUnavailable.localizedDescription
|
||
throw LivePushReadinessError.sdkUnavailable
|
||
}
|
||
try await adapter.start()
|
||
running = true
|
||
prepared = true
|
||
errorMessage = nil
|
||
}
|
||
|
||
/// stop推送相关逻辑。
|
||
func stopPush() async {
|
||
try? await adapter.stop()
|
||
running = false
|
||
}
|
||
|
||
/// dispose 业务逻辑。
|
||
func dispose() async {
|
||
stopMonitoring()
|
||
await adapter.dispose()
|
||
running = false
|
||
prepared = false
|
||
}
|
||
|
||
/// 校验Readiness输入或状态。
|
||
private func validateReadiness() throws {
|
||
guard pushURL != nil else {
|
||
throw LivePushReadinessError.missingPushURL
|
||
}
|
||
guard cameraPermission != .denied, microphonePermission != .denied else {
|
||
throw LivePushReadinessError.permissionDenied
|
||
}
|
||
guard networkState != .unavailable else {
|
||
throw LivePushReadinessError.networkUnavailable
|
||
}
|
||
}
|
||
}
|