diff --git a/yana/Features/AppSettingFeature.swift b/yana/Features/AppSettingFeature.swift
index 3362432..d91a770 100644
--- a/yana/Features/AppSettingFeature.swift
+++ b/yana/Features/AppSettingFeature.swift
@@ -29,6 +29,8 @@ struct AppSettingFeature {
self.avatarURL = avatarURL
self.userInfo = userInfo
}
+ // 新增:TCA驱动图片选择弹窗
+ var showImagePicker: Bool = false
}
enum Action: Equatable {
@@ -61,6 +63,8 @@ struct AppSettingFeature {
case nicknameInputChanged(String)
case nicknameEditAlert(Bool)
case testPushTapped
+ // 新增:TCA驱动图片选择弹窗
+ case setShowImagePicker(Bool)
}
@Dependency(\.apiService) var apiService
@@ -231,6 +235,9 @@ struct AppSettingFeature {
return .none
case .testPushTapped:
return .none
+ case .setShowImagePicker(let show):
+ state.showImagePicker = show
+ return .none
}
}
}
diff --git a/yana/Info.plist b/yana/Info.plist
index d5b7ef5..3aae323 100644
--- a/yana/Info.plist
+++ b/yana/Info.plist
@@ -9,6 +9,8 @@
NSWiFiUsageDescription
应用需要访问 Wi-Fi 信息以提供网络相关功能
+ NSCameraUsageDescription
+ 需要使用相机拍照上传图片
UIAppFonts
Bayon-Regular.ttf
diff --git a/yana/Views/AppSettingView.swift b/yana/Views/AppSettingView.swift
index 585b669..24d8a58 100644
--- a/yana/Views/AppSettingView.swift
+++ b/yana/Views/AppSettingView.swift
@@ -16,41 +16,106 @@ struct AppSettingView: View {
initialState: ImagePickerWithPreviewReducer.State(inner: ImagePickerWithPreviewState(selectionMode: .single)),
reducer: { ImagePickerWithPreviewReducer() }
)
- @State private var showImagePicker = false
@State private var showNicknameAlert = false
@State private var nicknameInput = ""
+ @State private var showImagePickerSheet = false
+ @State private var showActionSheet = false
+ @State private var showPhotoPicker = false
+ @State private var showCamera = false
+ @State private var selectedPhotoItems: [PhotosPickerItem] = []
+ @State private var selectedImages: [UIImage] = []
+ @State private var cameraImage: UIImage? = nil
+ @State private var previewIndex: Int = 0
+ @State private var showPreview = false
+ @State private var isLoading = false
+ @State private var errorMessage: String? = nil
var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
- ZStack {
- mainContent(viewStore: viewStore)
- if showImagePicker {
- WithPerceptionTracking{
- ImagePickerWithPreviewView(store: pickerStore) { images in
- if let image = images.first, let data = image.jpegData(compressionQuality: 0.8) {
- viewStore.send(.avatarSelected(data))
- }
- showImagePicker = false
+ WithPerceptionTracking {
+ ZStack {
+ mainContent(viewStore: viewStore)
+ }
+ .confirmationDialog(
+ "请选择图片来源",
+ isPresented: $showActionSheet,
+ titleVisibility: .visible
+ ) {
+ Button("拍照") { showCamera = true }
+ Button("从相册选择") { showPhotoPicker = true }
+ Button("取消", role: .cancel) {}
+ }
+ .photosPicker(
+ isPresented: $showPhotoPicker,
+ selection: $selectedPhotoItems,
+ maxSelectionCount: 1,
+ matching: .images
+ )
+ .sheet(isPresented: $showCamera) {
+ CameraPicker { image in
+ if let image = image {
+ selectedImages = [image]
+ showPreview = true
}
- .zIndex(10)
+ showCamera = false
}
}
- }
- .navigationBarHidden(true)
- .alert("修改昵称", isPresented: $showNicknameAlert) {
- nicknameAlertContent(viewStore: viewStore)
- } message: {
- Text("昵称最长15个字符")
- }
- .sheet(isPresented: userAgreementBinding(viewStore: viewStore)) {
- WebView(url: URL(string: "https://www.yana.com/user-agreement")!)
- }
- .sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) {
- WebView(url: URL(string: "https://www.yana.com/privacy-policy")!)
- }
- .onChange(of: showImagePicker) { newValue in
- if newValue {
- pickerStore.send(.inner(.showActionSheet(true)))
+ .fullScreenCover(isPresented: $showPreview) {
+ ImagePreviewView(
+ images: selectedImages,
+ currentIndex: .constant(0),
+ onConfirm: {
+ if let image = selectedImages.first, let data = image.jpegData(compressionQuality: 0.8) {
+ viewStore.send(AppSettingFeature.Action.avatarSelected(data))
+ }
+ showPreview = false
+ },
+ onCancel: {
+ showPreview = false
+ }
+ )
+ }
+ .onChange(of: selectedPhotoItems) { items in
+ guard !items.isEmpty else { return }
+ isLoading = true
+ selectedImages = []
+ let group = DispatchGroup()
+ var tempImages: [UIImage] = []
+ for item in items {
+ group.enter()
+ item.loadTransferable(type: Data.self) { result in
+ defer { group.leave() }
+ if let data = try? result.get(), let uiImage = UIImage(data: data) {
+ tempImages.append(uiImage)
+ }
+ }
+ }
+ DispatchQueue.global().async {
+ group.wait()
+ DispatchQueue.main.async {
+ selectedImages = tempImages
+ isLoading = false
+ showPreview = !tempImages.isEmpty
+ }
+ }
+ }
+ .alert(isPresented: Binding(
+ get: { errorMessage != nil },
+ set: { if !$0 { errorMessage = nil } }
+ )) {
+ Alert(title: Text("错误"), message: Text(errorMessage ?? ""), dismissButton: .default(Text("确定")))
+ }
+ .navigationBarHidden(true)
+ .alert("修改昵称", isPresented: $showNicknameAlert) {
+ nicknameAlertContent(viewStore: viewStore)
+ } message: {
+ Text("昵称最长15个字符")
+ }
+ .sheet(isPresented: userAgreementBinding(viewStore: viewStore)) {
+ WebView(url: URL(string: "https://www.yana.com/user-agreement")!)
+ }
+ .sheet(isPresented: privacyPolicyBinding(viewStore: viewStore)) {
+ WebView(url: URL(string: "https://www.yana.com/privacy-policy")!)
}
}
}
@@ -85,12 +150,9 @@ struct AppSettingView: View {
ZStack(alignment: .bottomTrailing) {
avatarImageView(viewStore: viewStore)
.onTapGesture {
- showImagePicker = true
- }
- cameraButton
- .onTapGesture {
- showImagePicker = true
+ showActionSheet = true
}
+ cameraButton(viewStore: viewStore)
}
.padding(.top, 24)
}
@@ -98,7 +160,7 @@ struct AppSettingView: View {
// MARK: - 头像图片视图
@ViewBuilder
private func avatarImageView(viewStore: ViewStoreOf) -> some View {
- if viewStore.isLoadingUserInfo {
+ if viewStore.isUploadingAvatar || viewStore.isLoadingUserInfo {
loadingAvatarView
} else if let avatarURLString = viewStore.avatarURL, !avatarURLString.isEmpty, let avatarURL = URL(string: avatarURLString) {
networkAvatarView(url: avatarURL)
@@ -145,8 +207,10 @@ struct AppSettingView: View {
}
// MARK: - 相机按钮
- private var cameraButton: some View {
- Button(action: {}) {
+ private func cameraButton(viewStore: ViewStoreOf) -> some View {
+ Button(action: {
+ showActionSheet = true
+ }) {
ZStack {
Circle().fill(Color.purple).frame(width: 36, height: 36)
Image(systemName: "camera.fill")
@@ -336,7 +400,7 @@ struct AppSettingView: View {
}
// MARK: - 图片处理
- private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping (Data?) -> Void) {
+ private func loadAndProcessImage(item: PhotosPickerItem, completion: @escaping @Sendable (Data?) -> Void) {
item.loadTransferable(type: Data.self) { result in
guard let data = try? result.get(), let uiImage = UIImage(data: data) else {
completion(nil)
diff --git a/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewReducer.swift b/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewReducer.swift
index 90de29d..942dd6e 100644
--- a/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewReducer.swift
+++ b/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewReducer.swift
@@ -15,7 +15,7 @@ public struct ImagePickerWithPreviewState: Equatable {
public var showCamera: Bool = false
public var showPreview: Bool = false
public var isLoading: Bool = false
- public var errorMessage: String?
+ public var errorMessage: String? = nil
public var selectedPhotoItems: [PhotosPickerItem] = []
public var selectedImages: [UIImage] = []
public var cameraImage: UIImage? = nil
@@ -41,6 +41,7 @@ public enum ImagePickerWithPreviewAction: Equatable {
case setPreviewIndex(Int)
case setShowCamera(Bool)
case setShowPhotoPicker(Bool)
+ case reset
}
public enum ImageSource: Equatable {
@@ -128,6 +129,10 @@ public struct ImagePickerWithPreviewReducer: Reducer {
case .setShowPhotoPicker(let show):
state.inner.showPhotoPicker = show
return .none
+ case .reset:
+ let mode = state.inner.selectionMode
+ state.inner = ImagePickerWithPreviewState(selectionMode: mode)
+ return .none
}
}
}
diff --git a/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift b/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift
index 96ef997..6c38195 100644
--- a/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift
+++ b/yana/Views/Components/ImagePickerWithPreview/ImagePickerWithPreviewView.swift
@@ -5,13 +5,15 @@ import PhotosUI
public struct ImagePickerWithPreviewView: View {
let store: StoreOf
let onUpload: ([UIImage]) -> Void
+ let onCancel: () -> Void
@State private var loadedImages: [UIImage] = []
@State private var isLoadingImages: Bool = false
- public init(store: StoreOf, onUpload: @escaping ([UIImage]) -> Void) {
+ public init(store: StoreOf, onUpload: @escaping ([UIImage]) -> Void, onCancel: @escaping () -> Void) {
self.store = store
self.onUpload = onUpload
+ self.onCancel = onCancel
}
public var body: some View {
@@ -20,7 +22,8 @@ public struct ImagePickerWithPreviewView: View {
Color.clear
LoadingView(isLoading: viewStore.inner.isLoading || isLoadingImages)
}
- .modifier(ActionSheetModifier(viewStore: viewStore))
+ .background(.clear)
+ .modifier(ActionSheetModifier(viewStore: viewStore, onCancel: onCancel))
.modifier(CameraSheetModifier(viewStore: viewStore))
.modifier(PhotosPickerModifier(viewStore: viewStore, loadedImages: $loadedImages, isLoadingImages: $isLoadingImages))
.modifier(PreviewCoverModifier(viewStore: viewStore, loadedImages: loadedImages, onUpload: onUpload))
@@ -46,6 +49,7 @@ private struct LoadingView: View {
private struct ActionSheetModifier: ViewModifier {
let viewStore: ViewStoreOf
+ let onCancel: () -> Void
func body(content: Content) -> some View {
content.confirmationDialog(
"请选择图片来源",
@@ -57,7 +61,7 @@ private struct ActionSheetModifier: ViewModifier {
) {
Button("拍照") { viewStore.send(.inner(.selectSource(.camera))) }
Button("从相册选择") { viewStore.send(.inner(.selectSource(.photoLibrary))) }
- Button("取消", role: .cancel) {}
+ Button("取消", role: .cancel) { onCancel() }
}
}
}
@@ -181,4 +185,4 @@ private struct ErrorToastModifier: ViewModifier {
}
)
}
-}
\ No newline at end of file
+}
diff --git a/yana/Views/MainView.swift b/yana/Views/MainView.swift
index 6105287..2a9ea4d 100644
--- a/yana/Views/MainView.swift
+++ b/yana/Views/MainView.swift
@@ -33,18 +33,7 @@ struct InternalMainView: View {
GeometryReader { geometry in
contentView(geometry: geometry, viewStore: viewStore)
.navigationDestination(for: MainFeature.Destination.self) { destination in
- let view: AnyView
- switch destination {
- case .appSetting:
- if let appSettingStore = store.scope(state: \.appSettingState, action: \.appSettingAction) {
- view = AnyView(AppSettingView(store: appSettingStore))
- } else {
- view = AnyView(Text("appSettingState is nil"))
- }
- case .testView:
- view = AnyView(TestView())
- }
- return view
+ DestinationView(destination: destination, store: self.store)
}
.onChange(of: path) { newPath in
viewStore.send(.navigationPathChanged(newPath))
@@ -63,6 +52,28 @@ struct InternalMainView: View {
}
}
+ struct DestinationView: View {
+ let destination: MainFeature.Destination
+ let store: StoreOf
+
+ var body: some View {
+ switch destination {
+ case .appSetting:
+ IfLetStore(
+ store.scope(state: \.appSettingState, action: \.appSettingAction),
+ then: { store in
+ WithPerceptionTracking {
+ AppSettingView(store: store)
+ }
+ },
+ else: { Text("appSettingState is nil") }
+ )
+ case .testView:
+ TestView()
+ }
+ }
+ }
+
private func contentView(geometry: GeometryProxy, viewStore: ViewStoreOf) -> some View {
WithPerceptionTracking {
ZStack {
@@ -74,31 +85,45 @@ struct InternalMainView: View {
.clipped()
.ignoresSafeArea(.all)
// 主内容
- ZStack {
- FeedListView(store: store.scope(
- state: \.feedList,
- action: \.feedList
- ))
- .isHidden(viewStore.selectedTab != .feed)
- MeView(
- store: store.scope(
- state: \.me,
- action: \.me
- )
- )
- .isHidden(viewStore.selectedTab != .other)
- }
+ MainContentView(
+ store: store,
+ selectedTab: viewStore.selectedTab
+ )
.frame(maxWidth: .infinity, maxHeight: .infinity)
// 底部导航栏
VStack {
Spacer()
- BottomTabView(selectedTab: viewStore.binding(
- get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
- send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
- ))
+ BottomTabView(selectedTab: viewStore.binding(
+ get: { Tab(rawValue: $0.selectedTab.rawValue) ?? .feed },
+ send: { MainFeature.Action.selectTab(MainFeature.Tab(rawValue: $0.rawValue) ?? .feed) }
+ ))
}
.padding(.bottom, geometry.safeAreaInsets.bottom + 60)
}
}
}
}
+
+struct MainContentView: View {
+ let store: StoreOf
+ let selectedTab: MainFeature.Tab
+ var body: some View {
+ Group {
+ if selectedTab == .feed {
+ FeedListView(store: store.scope(
+ state: \.feedList,
+ action: \.feedList
+ ))
+ } else if selectedTab == .other {
+ MeView(
+ store: store.scope(
+ state: \.me,
+ action: \.me
+ )
+ )
+ } else {
+ EmptyView()
+ }
+ }
+ }
+}