Advance UIKit rewrite with AMap integration and core UI modules.
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>
This commit is contained in:
@ -10,10 +10,12 @@ import Foundation
|
||||
|
||||
/// 直播播放地址解析器,避免把 RTMP 推流地址误当作播放地址。
|
||||
enum LivePlaybackURLResolver {
|
||||
/// playable链接相关逻辑。
|
||||
static func playableURL(from live: LiveEntity) -> URL? {
|
||||
playableURL(from: live.playbackURLCandidates)
|
||||
}
|
||||
|
||||
/// playable链接相关逻辑。
|
||||
static func playableURL(from candidates: [String]) -> URL? {
|
||||
for candidate in candidates {
|
||||
if let url = playableURL(from: candidate) {
|
||||
@ -23,6 +25,7 @@ enum LivePlaybackURLResolver {
|
||||
return nil
|
||||
}
|
||||
|
||||
/// playable链接相关逻辑。
|
||||
static func playableURL(from rawValue: String) -> URL? {
|
||||
let value = rawValue.liveTrimmed
|
||||
guard !value.isEmpty, let url = URL(string: value) else { return nil }
|
||||
@ -65,6 +68,7 @@ final class LivePlaybackViewModel {
|
||||
return false
|
||||
}
|
||||
|
||||
/// 初始化实例。
|
||||
init(live: LiveEntity? = nil, urlString: String? = nil) {
|
||||
if let live {
|
||||
load(live: live)
|
||||
@ -73,14 +77,17 @@ final class LivePlaybackViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载数据。
|
||||
func load(live: LiveEntity) {
|
||||
load(url: LivePlaybackURLResolver.playableURL(from: live))
|
||||
}
|
||||
|
||||
/// 加载数据。
|
||||
func load(urlString: String) {
|
||||
load(url: LivePlaybackURLResolver.playableURL(from: urlString))
|
||||
}
|
||||
|
||||
/// play 业务逻辑。
|
||||
func play() {
|
||||
guard let url = playableURL else { return }
|
||||
if player == nil {
|
||||
@ -90,12 +97,14 @@ final class LivePlaybackViewModel {
|
||||
state = .playing(url)
|
||||
}
|
||||
|
||||
/// pause 业务逻辑。
|
||||
func pause() {
|
||||
guard let url = playableURL else { return }
|
||||
player?.pause()
|
||||
state = .ready(url)
|
||||
}
|
||||
|
||||
/// 刷新。
|
||||
func reload() {
|
||||
guard let url = playableURL else { return }
|
||||
releasePlayer()
|
||||
@ -103,6 +112,7 @@ final class LivePlaybackViewModel {
|
||||
state = .ready(url)
|
||||
}
|
||||
|
||||
/// release 业务逻辑。
|
||||
func release() {
|
||||
releasePlayer()
|
||||
if let url = playableURL {
|
||||
@ -112,6 +122,7 @@ final class LivePlaybackViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载数据。
|
||||
private func load(url: URL?) {
|
||||
releasePlayer()
|
||||
guard let url else {
|
||||
@ -124,6 +135,7 @@ final class LivePlaybackViewModel {
|
||||
state = .ready(url)
|
||||
}
|
||||
|
||||
/// releasePlayer相关逻辑。
|
||||
private func releasePlayer() {
|
||||
player?.pause()
|
||||
player = nil
|
||||
|
||||
@ -47,25 +47,34 @@ enum LivePushNetworkState: Equatable {
|
||||
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
|
||||
@ -89,32 +98,41 @@ enum LivePushReadinessError: LocalizedError, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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:
|
||||
@ -130,6 +148,7 @@ struct SystemLivePermissionProvider: LivePermissionProviding {
|
||||
}
|
||||
}
|
||||
|
||||
/// SystemLiveNetworkMonitor 类型定义。
|
||||
final class SystemLiveNetworkMonitor: LiveNetworkMonitoring {
|
||||
|
||||
var onChange: (() -> Void)?
|
||||
@ -137,6 +156,7 @@ final class SystemLiveNetworkMonitor: LiveNetworkMonitoring {
|
||||
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)
|
||||
@ -146,10 +166,12 @@ final class SystemLiveNetworkMonitor: LiveNetworkMonitoring {
|
||||
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 }
|
||||
@ -177,6 +199,7 @@ final class LivePushReadinessViewModel {
|
||||
private let adapter: any LivePushAdapter
|
||||
private var pushURL: URL?
|
||||
|
||||
/// 初始化实例。
|
||||
init(
|
||||
permissionProvider: any LivePermissionProviding = SystemLivePermissionProvider(),
|
||||
networkMonitor: any LiveNetworkMonitoring = SystemLiveNetworkMonitor(),
|
||||
@ -190,6 +213,7 @@ final class LivePushReadinessViewModel {
|
||||
self.networkState = networkMonitor.currentState
|
||||
}
|
||||
|
||||
/// 配置展示内容。
|
||||
func configure(pushURL rawValue: String) {
|
||||
let value = rawValue.liveTrimmed
|
||||
guard !value.isEmpty else {
|
||||
@ -206,6 +230,7 @@ final class LivePushReadinessViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// startMonitoring相关逻辑。
|
||||
func startMonitoring() {
|
||||
networkState = networkMonitor.currentState
|
||||
networkMonitor.start { [weak self] state in
|
||||
@ -215,10 +240,12 @@ final class LivePushReadinessViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// stopMonitoring相关逻辑。
|
||||
func stopMonitoring() {
|
||||
networkMonitor.stop()
|
||||
}
|
||||
|
||||
/// refreshPermissions相关逻辑。
|
||||
func refreshPermissions() async {
|
||||
async let camera = permissionProvider.cameraPermission()
|
||||
async let microphone = permissionProvider.microphonePermission()
|
||||
@ -226,6 +253,7 @@ final class LivePushReadinessViewModel {
|
||||
microphonePermission = await microphone
|
||||
}
|
||||
|
||||
/// runDiagnostics相关逻辑。
|
||||
func runDiagnostics() throws {
|
||||
do {
|
||||
try validateReadiness()
|
||||
@ -240,6 +268,7 @@ final class LivePushReadinessViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// prepare 业务逻辑。
|
||||
func prepare() async throws {
|
||||
try validateReadiness()
|
||||
guard let pushURL else { throw LivePushReadinessError.missingPushURL }
|
||||
@ -253,6 +282,7 @@ final class LivePushReadinessViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// start推送相关逻辑。
|
||||
func startPush() async throws {
|
||||
try validateReadiness()
|
||||
guard adapter.isAvailable else {
|
||||
@ -265,11 +295,13 @@ final class LivePushReadinessViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// stop推送相关逻辑。
|
||||
func stopPush() async {
|
||||
try? await adapter.stop()
|
||||
running = false
|
||||
}
|
||||
|
||||
/// dispose 业务逻辑。
|
||||
func dispose() async {
|
||||
stopMonitoring()
|
||||
await adapter.dispose()
|
||||
@ -277,6 +309,7 @@ final class LivePushReadinessViewModel {
|
||||
prepared = false
|
||||
}
|
||||
|
||||
/// 校验Readiness输入或状态。
|
||||
private func validateReadiness() throws {
|
||||
guard pushURL != nil else {
|
||||
throw LivePushReadinessError.missingPushURL
|
||||
|
||||
@ -104,6 +104,7 @@ final class LiveManagementViewModel {
|
||||
await reload(api: api, scenicId: scenicId, showLoading: false)
|
||||
}
|
||||
|
||||
/// 加载Page数据。
|
||||
private func loadPage(api: any LiveServing, scenicId: Int, page: Int) async throws {
|
||||
let response = try await api.liveList(scenicId: scenicId, page: page, pageSize: pageSize)
|
||||
if page == 1 {
|
||||
@ -118,6 +119,7 @@ final class LiveManagementViewModel {
|
||||
hasMore = items.count < total
|
||||
}
|
||||
|
||||
/// 重置状态。
|
||||
private func reset() {
|
||||
clearListAndDetail()
|
||||
loading = false
|
||||
@ -125,6 +127,7 @@ final class LiveManagementViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 清空列表并详情。
|
||||
private func clearListAndDetail() {
|
||||
items = []
|
||||
detail = nil
|
||||
@ -143,6 +146,7 @@ final class LiveDetailViewModel {
|
||||
var actionInFlight = false { didSet { onChange?() } }
|
||||
var errorMessage: String? { didSet { onChange?() } }
|
||||
|
||||
/// 初始化实例。
|
||||
init(detail: LiveEntity) {
|
||||
self.detail = detail
|
||||
}
|
||||
@ -265,6 +269,7 @@ final class LiveAlbumViewModel {
|
||||
await reload(api: api, scenicId: scenicId, showLoading: false)
|
||||
}
|
||||
|
||||
/// 加载Page数据。
|
||||
private func loadPage(api: any LiveServing, scenicId: Int, page: Int) async throws {
|
||||
let response = try await api.liveAlbumList(
|
||||
scenicId: scenicId,
|
||||
@ -284,6 +289,7 @@ final class LiveAlbumViewModel {
|
||||
hasMore = folders.count < total
|
||||
}
|
||||
|
||||
/// 重置状态。
|
||||
private func reset() {
|
||||
clearFolders()
|
||||
loading = false
|
||||
@ -291,6 +297,7 @@ final class LiveAlbumViewModel {
|
||||
errorMessage = nil
|
||||
}
|
||||
|
||||
/// 清空Folders。
|
||||
private func clearFolders() {
|
||||
folders = []
|
||||
page = 1
|
||||
@ -377,6 +384,7 @@ final class LiveAlbumPreviewViewModel {
|
||||
var loading = false { didSet { onChange?() } }
|
||||
var errorMessage: String? { didSet { onChange?() } }
|
||||
|
||||
/// 初始化实例。
|
||||
init(folderId: Int, startIndex: Int = 0, summary: LiveAlbumFolderItem? = nil) {
|
||||
self.folderId = folderId
|
||||
currentIndex = max(startIndex, 0)
|
||||
|
||||
Reference in New Issue
Block a user