feat: 更新Splash视图及登录模型逻辑

- 将SplashV2替换为SplashPage,优化应用启动流程。
- 在LoginModels中将用户ID参数更改为加密后的ID,增强安全性。
- 更新AppConfig中的API基础URL,确保与生产环境一致。
- 在CommonComponents中添加底部Tab栏图标映射逻辑,提升用户体验。
- 新增NineGridImagePicker组件,支持多图选择功能,优化CreateFeedPage的图片选择逻辑。
- 移除冗余的BottomTabView组件,简化视图结构,提升代码整洁性。
- 在MainPage中整合创建动态页面的逻辑,优化导航体验。
- 新增MePage视图,提供用户信息管理功能,增强应用的可用性。
This commit is contained in:
edwinQQQ
2025-09-26 10:53:00 +08:00
parent 90a840c5f3
commit 6b960f53b4
12 changed files with 446 additions and 371 deletions

View File

@@ -392,7 +392,7 @@ struct LoginHelper {
debugInfoSync(" 加密后密码: \(encryptedPassword)")
return IDLoginAPIRequest(
phone: userID,
phone: encryptedID,
password: encryptedPassword
)
}

View File

@@ -15,7 +15,7 @@ struct AppConfig {
static var baseURL: String {
switch current {
case .development:
return "http://beta.api.molistar.xyz";//"http://beta.api.pekolive.com"
return "http://beta.api.pekolive.com"
case .production:
return "https://api.epartylive.com"
}

View File

@@ -25,29 +25,49 @@ struct BottomTabBar: View {
var contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0)
var horizontalPadding: CGFloat = 0
// 使 BottomTabView.swift
private func assetIconName(for item: TabBarItem, isSelected: Bool) -> String? {
switch item.id {
case "feed":
return isSelected ? "feed selected" : "feed unselected"
case "me":
return isSelected ? "me selected" : "me unselected"
default:
return nil
}
}
var body: some View {
HStack(spacing: 0) {
HStack(spacing: 8) {
ForEach(items) { item in
Button(action: {
selectedId = item.id
onSelect(item.id)
}) {
VStack(spacing: 4) {
Image(systemName: item.systemIconName)
.font(.system(size: 24))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
Text(item.title)
.font(.system(size: 12))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
Group {
if let name = assetIconName(for: item, isSelected: selectedId == item.id) {
Image(name)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
} else {
Image(systemName: item.systemIconName)
.font(.system(size: 24))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
}
}
}
.frame(maxWidth: .infinity)
.padding(contentPadding)
.contentShape(Rectangle())
}
}
.padding(.horizontal, 8) // 8
.padding(.horizontal, horizontalPadding)
.background(LiquidGlassBackground())
.clipShape(Capsule())
.contentShape(Capsule())
.onTapGesture { /* 穿 */ }
.overlay(
Capsule()
.stroke(Color.white.opacity(0.12), lineWidth: 0.5)

View File

@@ -1,4 +1,5 @@
import SwiftUI
import PhotosUI
@MainActor
final class CreateFeedViewModel: ObservableObject {
@@ -11,19 +12,21 @@ final class CreateFeedViewModel: ObservableObject {
}
struct CreateFeedPage: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var viewModel = CreateFeedViewModel()
let onDismiss: () -> Void
// MARK: - UI State
@FocusState private var isTextEditorFocused: Bool
@State private var isShowingSourceSheet: Bool = false
@State private var isShowingImagePicker: Bool = false
@State private var imagePickerSource: UIImagePickerController.SourceType = .photoLibrary
@State private var isShowingPreview: Bool = false
@State private var previewIndex: Int = 0
private let maxCharacters: Int = 500
private let gridSpacing: CGFloat = 8
private let gridCornerRadius: CGFloat = 16
var body: some View {
GeometryReader { _ in
GeometryReader { geometry in
ZStack {
Color(hex: 0x0C0527)
.ignoresSafeArea()
@@ -33,13 +36,18 @@ struct CreateFeedPage: View {
}
VStack(spacing: 16) {
HStack {
Button(action: onDismiss) {
Button(action: {
onDismiss()
dismiss()
}) {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.system(size: 18, weight: .medium))
.frame(width: 44, height: 44, alignment: .center)
.contentShape(Rectangle())
}
Spacer()
Text(LocalizedString("createFeed.title", comment: "Image & Text Publish"))
Text(LocalizedString ("createFeed.title", comment: "Image & Text Publish"))
.foregroundColor(.white)
.font(.system(size: 18, weight: .medium))
Spacer()
@@ -58,6 +66,8 @@ struct CreateFeedPage: View {
}
.padding(.horizontal, 16)
.padding(.top, 12)
.contentShape(Rectangle())
.zIndex(10)
ZStack(alignment: .topLeading) {
RoundedRectangle(cornerRadius: 8).fill(Color(hex: 0x1C143A))
@@ -74,6 +84,7 @@ struct CreateFeedPage: View {
.scrollContentBackground(.hidden)
.focused($isTextEditorFocused)
.frame(height: 200)
.zIndex(1) //
//
VStack { Spacer() }
@@ -94,58 +105,17 @@ struct CreateFeedPage: View {
}
}
//
HStack(alignment: .top, spacing: 12) {
Button {
isShowingSourceSheet = true
} label: {
ZStack {
RoundedRectangle(cornerRadius: 16)
.fill(Color(hex: 0x1C143A))
.frame(width: 180, height: 180)
Image(systemName: "plus")
.foregroundColor(.white.opacity(0.6))
.font(.system(size: 36, weight: .semibold))
}
NineGridImagePicker(
images: $viewModel.selectedImages,
maxCount: 9,
cornerRadius: gridCornerRadius,
spacing: gridSpacing,
horizontalPadding: 20,
onTapImage: { index in
previewIndex = index
isShowingPreview = true
}
.buttonStyle(.plain)
//
if !viewModel.selectedImages.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(viewModel.selectedImages.indices, id: \.self) { index in
Image(uiImage: viewModel.selectedImages[index])
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(RoundedRectangle(cornerRadius: 12))
.clipped()
}
}
}
.frame(height: 180)
}
}
.padding(.horizontal, 20)
.confirmationDialog(LocalizedString("createFeed.chooseSource", comment: "Choose Source"), isPresented: $isShowingSourceSheet, titleVisibility: .visible) {
Button(LocalizedString("createFeed.source.album", comment: "Photo Library")) {
imagePickerSource = .photoLibrary
isShowingImagePicker = true
}
Button(LocalizedString("createFeed.source.camera", comment: "Camera")) {
if UIImagePickerController.isSourceTypeAvailable(.camera) {
imagePickerSource = .camera
isShowingImagePicker = true
}
}
Button(LocalizedString("common.cancel", comment: "Cancel"), role: .cancel) {}
}
.sheet(isPresented: $isShowingImagePicker) {
ImagePicker(sourceType: imagePickerSource) { image in
viewModel.selectedImages.append(image)
}
}
)
if let error = viewModel.errorMessage {
Text(error)
@@ -158,53 +128,103 @@ struct CreateFeedPage: View {
}
}
.navigationBarBackButtonHidden(true)
.fullScreenCover(isPresented: $isShowingPreview) {
ZStack {
Color.black.ignoresSafeArea()
VStack(spacing: 0) {
HStack {
Spacer()
Button {
isShowingPreview = false
} label: {
Image(systemName: "xmark")
.foregroundColor(.white)
.font(.system(size: 18, weight: .medium))
.padding(12)
}
}
.padding(.top, 8)
TabView(selection: $previewIndex) {
ForEach(viewModel.selectedImages.indices, id: \.self) { idx in
ZStack {
Color.black
Image(uiImage: viewModel.selectedImages[idx])
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.tag(idx)
}
}
.tabViewStyle(.page(indexDisplayMode: .automatic))
}
}
}
}
private func publish() {
viewModel.isPublishing = true
viewModel.errorMessage = nil
Task { @MainActor in
try? await Task.sleep(nanoseconds: 500_000_000)
viewModel.isPublishing = false
onDismiss()
}
}
}
let apiService: any APIServiceProtocol & Sendable = LiveAPIService()
do {
// 1)
var resList: [ResListItem] = []
if !viewModel.selectedImages.isEmpty {
for image in viewModel.selectedImages {
if let url = await COSManager.shared.uploadUIImage(image, apiService: apiService) {
if let cg = image.cgImage {
let item = ResListItem(resUrl: url, width: cg.width, height: cg.height, format: "jpeg")
resList.append(item)
} else {
// 0
let item = ResListItem(resUrl: url, width: 0, height: 0, format: "jpeg")
resList.append(item)
}
} else {
viewModel.isPublishing = false
viewModel.errorMessage = "图片上传失败"
return
}
}
}
// MARK: - UIKit Image Picker Wrapper
private struct ImagePicker: UIViewControllerRepresentable {
let sourceType: UIImagePickerController.SourceType
let onImagePicked: (UIImage) -> Void
// 2)
let trimmed = viewModel.content.trimmingCharacters(in: .whitespacesAndNewlines)
let userId = await UserInfoManager.getCurrentUserId() ?? ""
let type = resList.isEmpty ? "0" : "2" // 0: , 2: /
let request = await PublishFeedRequest.make(
content: trimmed,
uid: userId,
type: type,
resList: resList.isEmpty ? nil : resList
)
let response = try await apiService.request(request)
func makeCoordinator() -> Coordinator {
Coordinator(onImagePicked: onImagePicked)
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.allowsEditing = false
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let onImagePicked: (UIImage) -> Void
init(onImagePicked: @escaping (UIImage) -> Void) {
self.onImagePicked = onImagePicked
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = (info[.originalImage] as? UIImage) ?? (info[.editedImage] as? UIImage) {
onImagePicked(image)
// 3)
if response.code == 200 {
viewModel.isPublishing = false
NotificationCenter.default.post(name: .init("CreateFeedPublished"), object: nil)
onDismiss()
dismiss()
} else {
viewModel.isPublishing = false
viewModel.errorMessage = response.message.isEmpty ? "发布失败" : response.message
}
} catch {
viewModel.isPublishing = false
viewModel.errorMessage = error.localizedDescription
}
picker.dismiss(animated: true)
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
private func removeImage(at index: Int) {
guard viewModel.selectedImages.indices.contains(index) else { return }
viewModel.selectedImages.remove(at: index)
if isShowingPreview {
if previewIndex >= viewModel.selectedImages.count { previewIndex = max(0, viewModel.selectedImages.count - 1) }
if viewModel.selectedImages.isEmpty { isShowingPreview = false }
}
}
}

View File

@@ -5,6 +5,7 @@ import SwiftUI
struct MainPage: View {
@StateObject private var viewModel = MainViewModel()
let onLogout: () -> Void
@State private var isPresentingCreatePage: Bool = false
var body: some View {
NavigationStack(path: $viewModel.navigationPath) {
@@ -12,16 +13,18 @@ struct MainPage: View {
ZStack {
//
LoginBackgroundView()
//
mainContentView(geometry: geometry)
.frame(maxWidth: .infinity, maxHeight: .infinity)
// 使 TabView
TabView(selection: $viewModel.selectedTab) {
MomentListHomePage(onCreateTapped: { isPresentingCreatePage = true })
.tag(MainViewModel.Tab.feed)
MePage(onLogout: onLogout)
.tag(MainViewModel.Tab.me)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.frame(maxWidth: .infinity, maxHeight: .infinity)
VStack {
HStack {
Spacer()
//
topRightButton
}
Spacer()
//
BottomTabBar(
@@ -45,29 +48,6 @@ struct MainPage: View {
}
}
}
.navigationDestination(for: String.self) { _ in EmptyView() }
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .setting:
SettingPage(
onBack: {
viewModel.navigationPath.removeLast()
},
onLogout: {
viewModel.onLogoutTapped()
}
)
.navigationBarHidden(true)
case .publish:
CreateFeedPage(
onDismiss: {
viewModel.navigationPath.removeLast()
}
)
default:
EmptyView()
}
}
}
.onAppear {
viewModel.onLogout = onLogout
@@ -77,73 +57,15 @@ struct MainPage: View {
}
viewModel.onAppear()
}
.fullScreenCover(isPresented: $isPresentingCreatePage) {
CreateFeedPage {
isPresentingCreatePage = false
}
}
.onChange(of: viewModel.isLoggedOut) { _, isLoggedOut in
if isLoggedOut {
onLogout()
}
}
}
// MARK: - UI Components
private func mainContentView(geometry: GeometryProxy) -> some View {
Group {
switch viewModel.selectedTab {
case .feed:
MomentListHomePage()
case .me:
TempMePage()
}
}
}
//
// MARK: -
private var topRightButton: some View {
Button(action: {
viewModel.onTopRightButtonTapped()
}) {
Group {
switch viewModel.selectedTab {
case .feed:
Image("add icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
case .me:
Image(systemName: "gearshape")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 40, height: 40)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
}
}
.padding(.trailing, 16)
.padding(.top, 8)
}
}
// MARK: - MeView ()
struct TempMePage: View {
var body: some View {
VStack {
Text("Me View")
.font(.title)
.foregroundColor(.white)
Text("This is a simplified MeView")
.font(.body)
.foregroundColor(.white.opacity(0.8))
}
}
}
//#Preview {
// MainPage(onLogout: {})
//}

45
yana/MVVM/MePage.swift Normal file
View File

@@ -0,0 +1,45 @@
import SwiftUI
struct MePage: View {
let onLogout: () -> Void
@State private var isShowingSettings: Bool = false
var body: some View {
ZStack(alignment: .topTrailing) {
VStack {
Text("Me View")
.font(.title)
.foregroundColor(.white)
Text("This is a simplified MeView")
.font(.body)
.foregroundColor(.white.opacity(0.8))
}
Button(action: {
isShowingSettings = true
}) {
Image(systemName: "gearshape")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 40, height: 40)
.background(Color.black.opacity(0.3))
.clipShape(Circle())
}
.padding(.trailing, 16)
.padding(.top, 8)
}
.sheet(isPresented: $isShowingSettings) {
SettingPage(
onBack: { isShowingSettings = false },
onLogout: {
isShowingSettings = false
onLogout()
}
)
.navigationBarHidden(true)
}
}
}

View File

@@ -1,6 +1,6 @@
import SwiftUI
struct SplashV2: View {
struct SplashPage: View {
@State private var showLogin = false
@State private var showMain = false
@State private var hasCheckedAuth = false

View File

@@ -15,65 +15,85 @@ struct MomentListBackgroundView: View {
// MARK: - MomentListHomePage
struct MomentListHomePage: View {
@StateObject private var viewModel = MomentListHomeViewModel()
let onCreateTapped: () -> Void
// MARK: -
@State private var previewItem: PreviewItem? = nil
@State private var previewCurrentIndex: Int = 0
var body: some View {
GeometryReader { geometry in
ZStack {
//
MomentListBackgroundView()
// MARK: -
// MainPage TabView
VStack(alignment: .center, spacing: 0) {
//
var body: some View {
ZStack {
//
MomentListBackgroundView()
VStack(alignment: .center, spacing: 0) {
// +
ZStack {
//
Text(LocalizedString("feedList.title", comment: "Enjoy your Life Time"))
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, 60)
// Volume
Image("Volume")
.frame(width: 56, height: 41)
.padding(.top, 16)
// +
HStack {
Spacer()
Button {
debugInfoSync(" MomentListHomePage: 点击添加按钮")
onCreateTapped()
} label: {
Image("add icon")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
}
.padding(.trailing, 16)
}
}
.frame(height: 56)
//
Text(LocalizedString("feedList.slogan",
comment: ""))
.font(.system(size: 16))
.multilineTextAlignment(.leading)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.bottom, 30)
// Volume
if !viewModel.moments.isEmpty {
ScrollView {
VStack(spacing: 0) {
// Volume +
Image("Volume")
.frame(width: 56, height: 41)
.padding(.top, 16)
Text(LocalizedString("feedList.slogan",
comment: ""))
.font(.system(size: 16))
.multilineTextAlignment(.leading)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.bottom, 30)
//
if !viewModel.moments.isEmpty {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(Array(viewModel.moments.enumerated()), id: \.element.dynamicId) { index, moment in
MomentListItem(
moment: moment,
onImageTap: { images, tappedIndex in
//
previewCurrentIndex = tappedIndex
previewItem = PreviewItem(images: images, index: tappedIndex)
debugInfoSync("📸 MomentListHomePage: 图片被点击")
debugInfoSync(" 动态索引: \(index)")
debugInfoSync(" 图片索引: \(tappedIndex)")
debugInfoSync(" 图片数量: \(images.count)")
}
)
.padding(.leading, 16)
.padding(.trailing, 32)
.onAppear {
//
if index == viewModel.moments.count - 3 {
viewModel.loadMoreData()
}
ForEach(Array(viewModel.moments.enumerated()), id: \.element.dynamicId) { index, moment in
MomentListItem(
moment: moment,
onImageTap: { images, tappedIndex in
//
previewCurrentIndex = tappedIndex
previewItem = PreviewItem(images: images, index: tappedIndex)
debugInfoSync("📸 MomentListHomePage: 图片被点击")
debugInfoSync(" 动态索引: \(index)")
debugInfoSync(" 图片索引: \(tappedIndex)")
debugInfoSync(" 图片数量: \(images.count)")
}
)
.padding(.leading, 16)
.padding(.trailing, 32)
.onAppear {
//
if index == viewModel.moments.count - 3 {
viewModel.loadMoreData()
}
}
}
//
if viewModel.isLoadingMore {
@@ -98,55 +118,58 @@ struct MomentListHomePage: View {
}
.padding(.bottom, 160) //
}
.refreshable {
//
viewModel.refreshData()
}
.onAppear {
//
debugInfoSync("📱 MomentListHomePage: 显示动态列表")
debugInfoSync(" 动态数量: \(viewModel.moments.count)")
debugInfoSync(" 是否有更多: \(viewModel.hasMore)")
debugInfoSync(" 是否正在加载更多: \(viewModel.isLoadingMore)")
}
} else if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.top, 20)
} else if let error = viewModel.error {
VStack(spacing: 16) {
Text(error)
.font(.system(size: 14))
.foregroundColor(.red)
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
//
Button(action: {
viewModel.refreshData()
}) {
Text("重试")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.white.opacity(0.2))
.cornerRadius(8)
}
}
.padding(.top, 20)
}
.refreshable {
//
viewModel.refreshData()
}
.onAppear {
//
debugInfoSync("📱 MomentListHomePage: 显示动态列表")
debugInfoSync(" 动态数量: \(viewModel.moments.count)")
debugInfoSync(" 是否有更多: \(viewModel.hasMore)")
debugInfoSync(" 是否正在加载更多: \(viewModel.isLoadingMore)")
}
} else if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.top, 20)
} else if let error = viewModel.error {
VStack(spacing: 16) {
Text(error)
.font(.system(size: 14))
.foregroundColor(.red)
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
Spacer()
//
Button(action: {
viewModel.refreshData()
}) {
Text("重试")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 8)
.background(Color.white.opacity(0.2))
.cornerRadius(8)
}
}
.padding(.top, 20)
}
Spacer()
}
.safeAreaPadding(.top, 8)
}
.ignoresSafeArea()
.onAppear {
viewModel.onAppear()
}
// MARK: -
.fullScreenCover(item: $previewItem) { item in
.onReceive(NotificationCenter.default.publisher(for: .init("CreateFeedPublished"))) { _ in
viewModel.refreshData()
}
// MARK: - 使 sheet
.sheet(item: $previewItem) { item in
ImagePreviewPager(
images: item.images as [String],
currentIndex: $previewCurrentIndex
@@ -155,5 +178,6 @@ struct MomentListHomePage: View {
debugInfoSync("📸 MomentListHomePage: 图片预览已关闭")
}
}
//
}
}

View File

@@ -23,7 +23,6 @@ struct MomentListItem: View {
var body: some View {
ZStack {
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.clear)
.overlay(

View File

@@ -0,0 +1,123 @@
import SwiftUI
import PhotosUI
struct NineGridImagePicker: View {
@Binding var images: [UIImage]
var maxCount: Int = 9
var cornerRadius: CGFloat = 16
var spacing: CGFloat = 8
var horizontalPadding: CGFloat = 20
var onTapImage: (Int) -> Void = { _ in }
@State private var pickerItems: [PhotosPickerItem] = []
var body: some View {
GeometryReader { geometry in
let columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: 3)
let columnsCount: CGFloat = 3
let totalSpacing = spacing * (columnsCount - 1)
let availableWidth = geometry.size.width - horizontalPadding * 2
let cellSide = (availableWidth - totalSpacing) / columnsCount
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(0..<maxCount, id: \.self) { index in
ZStack {
// DEBUG
#if DEBUG
if index >= images.count && !(index == images.count && images.count < maxCount) {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(Color.white.opacity(0.08))
}
#endif
if index < images.count {
//
ZStack(alignment: .topTrailing) {
Image(uiImage: images[index])
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(RoundedRectangle(cornerRadius: cornerRadius))
.onTapGesture { onTapImage(index) }
Button {
removeImage(at: index)
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.background(Circle().fill(Color.black.opacity(0.4)))
.font(.system(size: 16, weight: .bold))
}
.padding(6)
.buttonStyle(.plain)
}
} else if index == images.count && images.count < maxCount {
//
PhotosPicker(
selection: $pickerItems,
maxSelectionCount: maxCount - images.count,
selectionBehavior: .ordered,
matching: .images
) {
ZStack {
RoundedRectangle(cornerRadius: cornerRadius)
.fill(Color(hex: 0x1C143A))
Image(systemName: "plus")
.foregroundColor(.white.opacity(0.6))
.font(.system(size: 32, weight: .semibold))
}
}
.onChange(of: pickerItems) { _, newItems in
handlePickerItems(newItems)
}
}
}
.frame(height: cellSide)
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
.contentShape(RoundedRectangle(cornerRadius: cornerRadius))
}
}
.padding(.horizontal, horizontalPadding)
}
.frame(height: gridHeight(forCount: max(images.count, 1)))
}
private func gridHeight(forCount count: Int) -> CGFloat {
//
// 3 = ceil(count / 3.0) GeometryReader
let screenWidth = UIScreen.main.bounds.width
let columnsCount: CGFloat = 3
let totalSpacing = spacing * (columnsCount - 1)
let availableWidth = screenWidth - horizontalPadding * 2
let side = (availableWidth - totalSpacing) / columnsCount
let rows = ceil(CGFloat(count) / 3.0)
let totalRowSpacing = spacing * max(rows - 1, 0)
return side * rows + totalRowSpacing
}
private func handlePickerItems(_ items: [PhotosPickerItem]) {
guard !items.isEmpty else { return }
Task { @MainActor in
var appended: [UIImage] = []
for item in items {
if images.count + appended.count >= maxCount { break }
if let data = try? await item.loadTransferable(type: Data.self),
let image = UIImage(data: data) {
appended.append(image)
}
}
if !appended.isEmpty {
images.append(contentsOf: appended)
}
pickerItems = []
}
}
private func removeImage(at index: Int) {
guard images.indices.contains(index) else { return }
images.remove(at: index)
}
}

View File

@@ -1,78 +0,0 @@
import SwiftUI
// MARK: - Tab
enum Tab: Int, CaseIterable {
case feed = 0
case me = 1
var title: String {
switch self {
case .feed:
return "动态"
case .me:
return "我的"
}
}
var iconName: String {
switch self {
case .feed:
return "feed unselected"
case .me:
return "me unselected"
}
}
var selectedIconName: String {
switch self {
case .feed:
return "feed selected"
case .me:
return "me selected"
}
}
}
// MARK: - BottomTabView
struct BottomTabView: View {
@Binding var selectedTab: Tab
var body: some View {
HStack(spacing: 0) {
ForEach(Tab.allCases, id: \.rawValue) { tab in
Button(action: {
selectedTab = tab
}) {
Image(selectedTab == tab ? tab.selectedIconName : tab.iconName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
.frame(maxWidth: .infinity)
}
.buttonStyle(PlainButtonStyle())
}
}
.frame(height: 60)
.padding(.horizontal, 16)
.background(
RoundedRectangle(cornerRadius: 30)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 30)
.stroke(Color.white.opacity(0.1), lineWidth: 0.5)
)
.shadow(
color: Color.black.opacity(0.34),
radius: 10.7,
x: 0,
y: 1.9
)
)
.padding(.horizontal, 15)
}
}
#Preview {
BottomTabView(selectedTab: .constant(.feed))
.background(Color.purple) // 便
}

View File

@@ -23,7 +23,7 @@ struct yanaApp: App {
var body: some Scene {
WindowGroup {
SplashV2()
SplashPage()
}
}
}