Migrate from iOS 17 Observation to iOS 16-compatible Combine architecture.
Lower deployment target to iOS 16, replace @Observable with ObservableObject, add navigation and UI compatibility shims, and include login smoke UI tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
/// 直播服务协议,定义直播管理和直播相册接口能力。
|
||||
@MainActor
|
||||
@ -26,10 +26,9 @@ protocol LiveServing {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播 API,封装手动直播和直播相册网络请求。
|
||||
final class LiveAPI {
|
||||
@ObservationIgnored private let client: APIClient
|
||||
private let client: APIClient
|
||||
|
||||
/// 初始化直播 API,并注入共享网络客户端。
|
||||
init(client: APIClient) {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
/// 直播播放地址解析器,避免把 RTMP 推流地址误当作播放地址。
|
||||
enum LivePlaybackURLResolver {
|
||||
@ -44,13 +44,12 @@ enum LivePlaybackState: Equatable {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播播放器 ViewModel,管理系统播放器的 URL、播放和释放状态。
|
||||
final class LivePlaybackViewModel {
|
||||
var state: LivePlaybackState = .empty
|
||||
var errorMessage: String?
|
||||
final class LivePlaybackViewModel: ObservableObject {
|
||||
@Published var state: LivePlaybackState = .empty
|
||||
@Published var errorMessage: String?
|
||||
|
||||
@ObservationIgnored private(set) var player: AVPlayer?
|
||||
@Published private(set) var player: AVPlayer?
|
||||
|
||||
var playableURL: URL? {
|
||||
switch state {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import Network
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
/// 直播推流权限状态。
|
||||
enum LivePushPermissionState: Equatable {
|
||||
@ -158,23 +158,22 @@ final class SystemLiveNetworkMonitor: LiveNetworkMonitoring {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 推流准备 ViewModel,检查权限、网络和默认 SDK 可用性。
|
||||
final class LivePushReadinessViewModel {
|
||||
var cameraPermission: LivePushPermissionState = .unknown
|
||||
var microphonePermission: LivePushPermissionState = .unknown
|
||||
var networkState: LivePushNetworkState = .unknown
|
||||
var prepared = false
|
||||
var running = false
|
||||
var errorMessage: String?
|
||||
final class LivePushReadinessViewModel: ObservableObject {
|
||||
@Published var cameraPermission: LivePushPermissionState = .unknown
|
||||
@Published var microphonePermission: LivePushPermissionState = .unknown
|
||||
@Published var networkState: LivePushNetworkState = .unknown
|
||||
@Published var prepared = false
|
||||
@Published var running = false
|
||||
@Published var errorMessage: String?
|
||||
|
||||
let adapterName: String
|
||||
let sdkStatusText: String
|
||||
|
||||
@ObservationIgnored private let permissionProvider: any LivePermissionProviding
|
||||
@ObservationIgnored private let networkMonitor: any LiveNetworkMonitoring
|
||||
@ObservationIgnored private let adapter: any LivePushAdapter
|
||||
@ObservationIgnored private var pushURL: URL?
|
||||
private let permissionProvider: any LivePermissionProviding
|
||||
private let networkMonitor: any LiveNetworkMonitoring
|
||||
private let adapter: any LivePushAdapter
|
||||
@Published private var pushURL: URL?
|
||||
|
||||
init(
|
||||
permissionProvider: any LivePermissionProviding = SystemLivePermissionProvider(),
|
||||
|
||||
@ -6,22 +6,21 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Observation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播管理 ViewModel,负责直播列表、详情、创建和控制动作。
|
||||
final class LiveManagementViewModel {
|
||||
var items: [LiveEntity] = []
|
||||
var detail: LiveEntity?
|
||||
var loading = false
|
||||
var loadingMore = false
|
||||
var hasMore = false
|
||||
var page = 1
|
||||
var errorMessage: String?
|
||||
final class LiveManagementViewModel: ObservableObject {
|
||||
@Published var items: [LiveEntity] = []
|
||||
@Published var detail: LiveEntity?
|
||||
@Published var loading = false
|
||||
@Published var loadingMore = false
|
||||
@Published var hasMore = false
|
||||
@Published var page = 1
|
||||
@Published var errorMessage: String?
|
||||
|
||||
@ObservationIgnored private let pageSize = 10
|
||||
@ObservationIgnored private var total = 0
|
||||
private let pageSize = 10
|
||||
@Published private var total = 0
|
||||
|
||||
/// 进行中的直播数量。
|
||||
var liveRunningCount: Int {
|
||||
@ -136,13 +135,12 @@ final class LiveManagementViewModel {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播详情 ViewModel,负责直播详情页动作后的详情刷新。
|
||||
final class LiveDetailViewModel {
|
||||
var detail: LiveEntity
|
||||
var loading = false
|
||||
var actionInFlight = false
|
||||
var errorMessage: String?
|
||||
final class LiveDetailViewModel: ObservableObject {
|
||||
@Published var detail: LiveEntity
|
||||
@Published var loading = false
|
||||
@Published var actionInFlight = false
|
||||
@Published var errorMessage: String?
|
||||
|
||||
init(detail: LiveEntity) {
|
||||
self.detail = detail
|
||||
@ -192,20 +190,19 @@ final class LiveDetailViewModel {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播相册 ViewModel,负责相册列表、筛选、新建和删除。
|
||||
final class LiveAlbumViewModel {
|
||||
var folders: [LiveAlbumFolderItem] = []
|
||||
var startDate: Date?
|
||||
var endDate: Date?
|
||||
var loading = false
|
||||
var loadingMore = false
|
||||
var hasMore = false
|
||||
var page = 1
|
||||
var errorMessage: String?
|
||||
final class LiveAlbumViewModel: ObservableObject {
|
||||
@Published var folders: [LiveAlbumFolderItem] = []
|
||||
@Published var startDate: Date?
|
||||
@Published var endDate: Date?
|
||||
@Published var loading = false
|
||||
@Published var loadingMore = false
|
||||
@Published var hasMore = false
|
||||
@Published var page = 1
|
||||
@Published var errorMessage: String?
|
||||
|
||||
@ObservationIgnored private let pageSize = 10
|
||||
@ObservationIgnored private var total = 0
|
||||
private let pageSize = 10
|
||||
@Published private var total = 0
|
||||
|
||||
/// 设置开始时间并校验时间顺序。
|
||||
func setStartDate(_ date: Date) throws {
|
||||
@ -301,14 +298,13 @@ final class LiveAlbumViewModel {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 新建直播相册 ViewModel,负责本地素材上传和创建相册。
|
||||
final class LiveAlbumCreateViewModel {
|
||||
var name = ""
|
||||
var localFiles: [LiveAlbumLocalUploadFile] = []
|
||||
var submitting = false
|
||||
var uploadProgress = 0
|
||||
var errorMessage: String?
|
||||
final class LiveAlbumCreateViewModel: ObservableObject {
|
||||
@Published var name = ""
|
||||
@Published var localFiles: [LiveAlbumLocalUploadFile] = []
|
||||
@Published var submitting = false
|
||||
@Published var uploadProgress = 0
|
||||
@Published var errorMessage: String?
|
||||
|
||||
/// 添加本地待上传素材。
|
||||
func addLocalFiles(_ files: [LiveAlbumLocalUploadFile]) {
|
||||
@ -368,15 +364,14 @@ final class LiveAlbumCreateViewModel {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
/// 直播相册预览 ViewModel,负责加载相册详情和删除当前素材。
|
||||
final class LiveAlbumPreviewViewModel {
|
||||
final class LiveAlbumPreviewViewModel: ObservableObject {
|
||||
let folderId: Int
|
||||
var folder: LiveAlbumFolderItem?
|
||||
var files: [LiveAlbumFileItem] = []
|
||||
var currentIndex: Int
|
||||
var loading = false
|
||||
var errorMessage: String?
|
||||
@Published var folder: LiveAlbumFolderItem?
|
||||
@Published var files: [LiveAlbumFileItem] = []
|
||||
@Published var currentIndex: Int
|
||||
@Published var loading = false
|
||||
@Published var errorMessage: String?
|
||||
|
||||
init(folderId: Int, startIndex: Int = 0, summary: LiveAlbumFolderItem? = nil) {
|
||||
self.folderId = folderId
|
||||
|
||||
@ -12,12 +12,12 @@ import UniformTypeIdentifiers
|
||||
|
||||
/// 直播相册首页,展示相册列表、筛选和上传入口。
|
||||
struct LiveAlbumView: View {
|
||||
@Environment(AccountContext.self) private var accountContext
|
||||
@Environment(LiveAPI.self) private var liveAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@EnvironmentObject private var accountContext: AccountContext
|
||||
@Environment(\.liveAPI) private var liveAPI
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
|
||||
@State private var viewModel = LiveAlbumViewModel()
|
||||
@StateObject private var viewModel = LiveAlbumViewModel()
|
||||
@State private var showStartPicker = false
|
||||
@State private var showEndPicker = false
|
||||
@State private var showCreatePage = false
|
||||
@ -29,7 +29,7 @@ struct LiveAlbumView: View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: AppMetrics.Spacing.medium) {
|
||||
if viewModel.folders.isEmpty {
|
||||
ContentUnavailableView("暂无素材", systemImage: "photo.stack")
|
||||
AppContentUnavailableView("暂无素材", systemImage: "photo.stack")
|
||||
.frame(maxWidth: .infinity, minHeight: 300)
|
||||
} else {
|
||||
ForEach(viewModel.folders) { folder in
|
||||
@ -286,13 +286,13 @@ private struct LiveDatePickerSheet: View {
|
||||
|
||||
/// 新建直播相册页面,支持选择本地图片/视频并上传。
|
||||
struct LiveAlbumCreateView: View {
|
||||
@Environment(AccountContext.self) private var accountContext
|
||||
@Environment(LiveAPI.self) private var liveAPI
|
||||
@Environment(OSSUploadService.self) private var uploadService
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@EnvironmentObject private var accountContext: AccountContext
|
||||
@Environment(\.liveAPI) private var liveAPI
|
||||
@Environment(\.ossUploadService) private var uploadService
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var viewModel = LiveAlbumCreateViewModel()
|
||||
@StateObject private var viewModel = LiveAlbumCreateViewModel()
|
||||
@State private var pickerItems: [PhotosPickerItem] = []
|
||||
let onCreated: () -> Void
|
||||
|
||||
@ -330,7 +330,7 @@ struct LiveAlbumCreateView: View {
|
||||
.background(Color(hex: 0xF5F7FA))
|
||||
.navigationTitle("上传素材")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onChange(of: pickerItems) { _, newItems in
|
||||
.onChange(of: pickerItems) { newItems in
|
||||
Task { await importPickerItems(newItems) }
|
||||
}
|
||||
}
|
||||
@ -338,7 +338,7 @@ struct LiveAlbumCreateView: View {
|
||||
@ViewBuilder
|
||||
private var localFileGrid: some View {
|
||||
if viewModel.localFiles.isEmpty {
|
||||
ContentUnavailableView("未选择素材", systemImage: "photo")
|
||||
AppContentUnavailableView("未选择素材", systemImage: "photo")
|
||||
.frame(maxWidth: .infinity, minHeight: 180)
|
||||
.background(.white, in: RoundedRectangle(cornerRadius: AppMetrics.CornerRadius.card))
|
||||
} else {
|
||||
@ -392,21 +392,21 @@ struct LiveAlbumCreateView: View {
|
||||
|
||||
/// 直播相册预览页面。
|
||||
struct LiveAlbumPreviewView: View {
|
||||
@Environment(LiveAPI.self) private var liveAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@Environment(\.liveAPI) private var liveAPI
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var viewModel: LiveAlbumPreviewViewModel
|
||||
@StateObject private var viewModel: LiveAlbumPreviewViewModel
|
||||
@State private var showDeleteConfirm = false
|
||||
|
||||
init(folderId: Int, startIndex: Int, summary: LiveAlbumFolderItem?) {
|
||||
_viewModel = State(initialValue: LiveAlbumPreviewViewModel(folderId: folderId, startIndex: startIndex, summary: summary))
|
||||
_viewModel = StateObject(wrappedValue: LiveAlbumPreviewViewModel(folderId: folderId, startIndex: startIndex, summary: summary))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
if viewModel.files.isEmpty {
|
||||
ContentUnavailableView("没有可预览的素材", systemImage: "photo")
|
||||
AppContentUnavailableView("没有可预览的素材", systemImage: "photo")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black)
|
||||
} else {
|
||||
@ -459,12 +459,12 @@ struct LiveAlbumPreviewView: View {
|
||||
|
||||
private struct LiveAlbumPreviewPage: View {
|
||||
let file: LiveAlbumFileItem
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@State private var playbackViewModel: LivePlaybackViewModel
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@StateObject private var playbackViewModel: LivePlaybackViewModel
|
||||
|
||||
init(file: LiveAlbumFileItem) {
|
||||
self.file = file
|
||||
_playbackViewModel = State(initialValue: LivePlaybackViewModel(urlString: file.url))
|
||||
_playbackViewModel = StateObject(wrappedValue: LivePlaybackViewModel(urlString: file.url))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@ -10,12 +10,12 @@ import SwiftUI
|
||||
|
||||
/// 直播管理首页,展示直播列表和控制入口。
|
||||
struct LiveManagementView: View {
|
||||
@Environment(AccountContext.self) private var accountContext
|
||||
@Environment(LiveAPI.self) private var liveAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@EnvironmentObject private var accountContext: AccountContext
|
||||
@Environment(\.liveAPI) private var liveAPI
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
|
||||
@State private var viewModel = LiveManagementViewModel()
|
||||
@StateObject private var viewModel = LiveManagementViewModel()
|
||||
@State private var showAddPage = false
|
||||
|
||||
var body: some View {
|
||||
@ -24,7 +24,7 @@ struct LiveManagementView: View {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: AppMetrics.Spacing.medium) {
|
||||
if viewModel.items.isEmpty {
|
||||
ContentUnavailableView("暂无直播", systemImage: "dot.radiowaves.left.and.right")
|
||||
AppContentUnavailableView("暂无直播", systemImage: "dot.radiowaves.left.and.right")
|
||||
.frame(maxWidth: .infinity, minHeight: 280)
|
||||
} else {
|
||||
ForEach(viewModel.items) { item in
|
||||
@ -231,9 +231,9 @@ private struct LiveSmallAction: View {
|
||||
|
||||
/// 添加直播页面。
|
||||
struct LiveAddView: View {
|
||||
@Environment(AccountContext.self) private var accountContext
|
||||
@Environment(LiveAPI.self) private var liveAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@EnvironmentObject private var accountContext: AccountContext
|
||||
@Environment(\.liveAPI) private var liveAPI
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var title = ""
|
||||
@ -351,18 +351,18 @@ struct LiveAddView: View {
|
||||
|
||||
/// 直播详情页面,展示推流信息和控制动作。
|
||||
struct LiveDetailView: View {
|
||||
@Environment(LiveAPI.self) private var liveAPI
|
||||
@Environment(ToastCenter.self) private var toastCenter
|
||||
@Environment(\.liveAPI) private var liveAPI
|
||||
@EnvironmentObject private var toastCenter: ToastCenter
|
||||
@Environment(\.globalLoading) private var globalLoading
|
||||
@State private var viewModel: LiveDetailViewModel
|
||||
@State private var playbackViewModel: LivePlaybackViewModel
|
||||
@State private var pushReadinessViewModel = LivePushReadinessViewModel()
|
||||
@StateObject private var viewModel: LiveDetailViewModel
|
||||
@StateObject private var playbackViewModel: LivePlaybackViewModel
|
||||
@StateObject private var pushReadinessViewModel = LivePushReadinessViewModel()
|
||||
@State private var copied = false
|
||||
let onChanged: () -> Void
|
||||
|
||||
init(initialDetail: LiveEntity, onChanged: @escaping () -> Void) {
|
||||
_viewModel = State(initialValue: LiveDetailViewModel(detail: initialDetail))
|
||||
_playbackViewModel = State(initialValue: LivePlaybackViewModel(live: initialDetail))
|
||||
_viewModel = StateObject(wrappedValue: LiveDetailViewModel(detail: initialDetail))
|
||||
_playbackViewModel = StateObject(wrappedValue: LivePlaybackViewModel(live: initialDetail))
|
||||
self.onChanged = onChanged
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user