feat: 添加设置功能和动态视图
- 新增设置功能模块,包含用户信息管理和设置选项。 - 实现动态视图,展示用户动态内容。 - 更新HomeView以支持设置页面的展示和动态视图的切换。 - 添加底部导航栏,增强用户体验。 - 更新相关视图和组件,确保一致的UI风格和交互体验。
This commit is contained in:
@@ -47,6 +47,8 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
4C4C8FBE2DE5AF9200384527 /* yanaAPITests */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = yanaAPITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -269,14 +271,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-yana/Pods-yana-frameworks.sh\"\n";
|
||||
|
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "logo.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
BIN
yana/Assets.xcassets/AppIcon.appiconset/logo.png
Normal file
BIN
yana/Assets.xcassets/AppIcon.appiconset/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 802 KiB |
6
yana/Assets.xcassets/Home/Contents.json
Normal file
6
yana/Assets.xcassets/Home/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
21
yana/Assets.xcassets/Home/add icon.imageset/Contents.json
vendored
Normal file
21
yana/Assets.xcassets/Home/add icon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "发布@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
yana/Assets.xcassets/Home/add icon.imageset/发布@3x.png
vendored
Normal file
BIN
yana/Assets.xcassets/Home/add icon.imageset/发布@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
yana/Assets.xcassets/Home/feed selected.imageset/3@3x.png
vendored
Normal file
BIN
yana/Assets.xcassets/Home/feed selected.imageset/3@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
21
yana/Assets.xcassets/Home/feed selected.imageset/Contents.json
vendored
Normal file
21
yana/Assets.xcassets/Home/feed selected.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "3@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
yana/Assets.xcassets/Home/feed unselected.imageset/3@3x (1).png
vendored
Normal file
BIN
yana/Assets.xcassets/Home/feed unselected.imageset/3@3x (1).png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
21
yana/Assets.xcassets/Home/feed unselected.imageset/Contents.json
vendored
Normal file
21
yana/Assets.xcassets/Home/feed unselected.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "3@3x (1).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
yana/Assets.xcassets/Home/me selected.imageset/5@3x (1).png
vendored
Normal file
BIN
yana/Assets.xcassets/Home/me selected.imageset/5@3x (1).png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
21
yana/Assets.xcassets/Home/me selected.imageset/Contents.json
vendored
Normal file
21
yana/Assets.xcassets/Home/me selected.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "5@3x (1).png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
yana/Assets.xcassets/Home/me unselected.imageset/5@3x.png
vendored
Normal file
BIN
yana/Assets.xcassets/Home/me unselected.imageset/5@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
21
yana/Assets.xcassets/Home/me unselected.imageset/Contents.json
vendored
Normal file
21
yana/Assets.xcassets/Home/me unselected.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "5@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@@ -9,6 +9,10 @@ struct HomeFeature {
|
||||
var userInfo: UserInfo?
|
||||
var accountModel: AccountModel?
|
||||
var error: String?
|
||||
|
||||
// 设置页面相关状态
|
||||
var isSettingPresented = false
|
||||
var settingState = SettingFeature.State()
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
@@ -19,9 +23,17 @@ struct HomeFeature {
|
||||
case accountModelLoaded(AccountModel?)
|
||||
case logoutTapped
|
||||
case logout
|
||||
|
||||
// 设置页面相关actions
|
||||
case settingDismissed
|
||||
case setting(SettingFeature.Action)
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Scope(state: \.settingState, action: \.setting) {
|
||||
SettingFeature()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
@@ -59,6 +71,14 @@ struct HomeFeature {
|
||||
// 发送通知返回登录页面
|
||||
NotificationCenter.default.post(name: .homeLogout, object: nil)
|
||||
return .none
|
||||
|
||||
case .settingDismissed:
|
||||
state.isSettingPresented = false
|
||||
return .none
|
||||
|
||||
case .setting:
|
||||
// 由子reducer处理
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
75
yana/Features/SettingFeature.swift
Normal file
75
yana/Features/SettingFeature.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
@Reducer
|
||||
struct SettingFeature {
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var userInfo: UserInfo?
|
||||
var accountModel: AccountModel?
|
||||
var isLoading = false
|
||||
var error: String?
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case loadUserInfo
|
||||
case userInfoLoaded(UserInfo?)
|
||||
case loadAccountModel
|
||||
case accountModelLoaded(AccountModel?)
|
||||
case logoutTapped
|
||||
case logout
|
||||
case dismissTapped
|
||||
}
|
||||
|
||||
var body: some ReducerOf<Self> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
return .concatenate(
|
||||
.send(.loadUserInfo),
|
||||
.send(.loadAccountModel)
|
||||
)
|
||||
|
||||
case .loadUserInfo:
|
||||
let userInfo = UserInfoManager.getUserInfo()
|
||||
return .send(.userInfoLoaded(userInfo))
|
||||
|
||||
case let .userInfoLoaded(userInfo):
|
||||
state.userInfo = userInfo
|
||||
return .none
|
||||
|
||||
case .loadAccountModel:
|
||||
let accountModel = UserInfoManager.getAccountModel()
|
||||
return .send(.accountModelLoaded(accountModel))
|
||||
|
||||
case let .accountModelLoaded(accountModel):
|
||||
state.accountModel = accountModel
|
||||
return .none
|
||||
|
||||
case .logoutTapped:
|
||||
return .send(.logout)
|
||||
|
||||
case .logout:
|
||||
state.isLoading = true
|
||||
|
||||
// 清除所有认证数据
|
||||
UserInfoManager.clearAllAuthenticationData()
|
||||
|
||||
// 发送通知返回登录页面
|
||||
NotificationCenter.default.post(name: .homeLogout, object: nil)
|
||||
return .none
|
||||
|
||||
case .dismissTapped:
|
||||
// 发送关闭设置页面的通知
|
||||
NotificationCenter.default.post(name: .settingsDismiss, object: nil)
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Extension
|
||||
extension Notification.Name {
|
||||
static let settingsDismiss = Notification.Name("settingsDismiss")
|
||||
}
|
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>E-PARTi</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>E-PARTi</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
73
yana/Views/Components/BottomTabView.swift
Normal file
73
yana/Views/Components/BottomTabView.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
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 "sparkles"
|
||||
case .me:
|
||||
return "person"
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIconName: String {
|
||||
switch self {
|
||||
case .feed:
|
||||
return "sparkles"
|
||||
case .me:
|
||||
return "person.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}) {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: selectedTab == tab ? tab.selectedIconName : tab.iconName)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(selectedTab == tab ? .purple : .gray)
|
||||
|
||||
Text(tab.title)
|
||||
.font(.caption2)
|
||||
.foregroundColor(selectedTab == tab ? .purple : .gray)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 8)
|
||||
.background(
|
||||
.ultraThinMaterial,
|
||||
in: Rectangle()
|
||||
)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.frame(height: 0.5)
|
||||
.foregroundColor(.black.opacity(0.2)),
|
||||
alignment: .top
|
||||
)
|
||||
}
|
||||
}
|
134
yana/Views/FeedView.swift
Normal file
134
yana/Views/FeedView.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FeedView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// 顶部标题
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Enjoy your Life Time")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
// 心脏图标
|
||||
Image(systemName: "heart.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.red)
|
||||
.padding(.top, 40)
|
||||
|
||||
// 励志文字
|
||||
Text("The disease is like a cruel ruler,\nand time is our most precious treasure.\nEvery moment we live is a victory\nagainst the inevitable.")
|
||||
.font(.system(size: 16))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.top, 20)
|
||||
|
||||
// 模拟动态卡片
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(0..<3) { index in
|
||||
DynamicCardView(index: index)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 30)
|
||||
|
||||
// 底部安全区域
|
||||
Color.clear.frame(height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 动态卡片组件
|
||||
struct DynamicCardView: View {
|
||||
let index: Int
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
// 用户信息行
|
||||
HStack(spacing: 12) {
|
||||
// 头像
|
||||
Circle()
|
||||
.fill(Color.blue.opacity(0.6))
|
||||
.frame(width: 40, height: 40)
|
||||
.overlay(
|
||||
Text("👤")
|
||||
.font(.system(size: 20))
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
Text("NAMENAMENAME....")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("ID:7271557")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
|
||||
Text("09/12")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
// 内容文字
|
||||
Text("这是动态内容 \(index + 1)。今天是美好的一天,分享一些生活中的点点滴滴。")
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(nil)
|
||||
|
||||
// 图片网格(模拟)
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 3), spacing: 8) {
|
||||
ForEach(0..<3) { imageIndex in
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.overlay(
|
||||
Image(systemName: "photo")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 互动按钮
|
||||
HStack(spacing: 20) {
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "message")
|
||||
.font(.system(size: 16))
|
||||
Text("354")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Button(action: {}) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "heart")
|
||||
.font(.system(size: 16))
|
||||
Text("354")
|
||||
.font(.system(size: 14))
|
||||
}
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.padding(16)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
}
|
||||
}
|
@@ -4,111 +4,70 @@ import ComposableArchitecture
|
||||
struct HomeView: View {
|
||||
let store: StoreOf<HomeFeature>
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
@State private var selectedTab: Tab = .feed
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片 - 使用"bg"图片,全屏显示
|
||||
// 使用 "bg" 图片作为背景
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.clipped()
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Navigation Bar 标题区域
|
||||
Text("home.title".localized)
|
||||
.font(.custom("PingFang SC-Semibold", size: 16))
|
||||
.foregroundColor(.white)
|
||||
.frame(
|
||||
width: 158,
|
||||
height: 22,
|
||||
alignment: .center
|
||||
) // 参考代码中的尺寸
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal)
|
||||
|
||||
// 中间内容区域
|
||||
VStack(spacing: 32) {
|
||||
// 顶部导航区域
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
// 用户信息区域
|
||||
VStack(spacing: 16) {
|
||||
// 优先显示 UserInfo 中的用户名,否则显示通用欢迎信息
|
||||
if let userInfo = store.userInfo, let userName = userInfo.username {
|
||||
Text("欢迎, \(userName)")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("欢迎")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// 显示用户ID信息:优先 UserInfo,其次 AccountModel
|
||||
if let userInfo = store.userInfo, let userId = userInfo.userId {
|
||||
Text("ID: \(userId)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
} else if let accountModel = store.accountModel, let uid = accountModel.uid {
|
||||
Text("UID: \(uid)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
|
||||
// 显示账户状态(如果有 AccountModel)
|
||||
if let accountModel = store.accountModel {
|
||||
VStack(spacing: 4) {
|
||||
if accountModel.hasValidSession {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("已登录")
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
.font(.caption)
|
||||
} else if accountModel.hasValidAuthentication {
|
||||
HStack {
|
||||
Image(systemName: "clock.circle.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("认证中")
|
||||
.foregroundColor(.white.opacity(0.9))
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.black.opacity(0.3))
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 登出按钮
|
||||
// 右上角加号按钮
|
||||
Button(action: {
|
||||
store.send(.logoutTapped)
|
||||
// 加号按钮操作
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.right.square")
|
||||
Text("退出登录")
|
||||
}
|
||||
.font(.body)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.red.opacity(0.7))
|
||||
.cornerRadius(8)
|
||||
Image(systemName: "plus")
|
||||
.font(.system(size: 20, weight: .medium))
|
||||
.foregroundColor(.red)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(
|
||||
Color.white.opacity(0.2)
|
||||
.cornerRadius(18)
|
||||
)
|
||||
}
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 10)
|
||||
|
||||
// 主要内容区域
|
||||
ZStack {
|
||||
// 根据选中的 tab 显示不同的视图
|
||||
switch selectedTab {
|
||||
case .feed:
|
||||
FeedView()
|
||||
.transition(.opacity)
|
||||
case .me:
|
||||
MeView()
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
||||
// 底部导航栏
|
||||
BottomTabView(selectedTab: $selectedTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { store.isSettingPresented },
|
||||
set: { _ in store.send(.settingDismissed) }
|
||||
)) {
|
||||
SettingView(store: store.scope(state: \.settingState, action: \.setting))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
107
yana/Views/MeView.swift
Normal file
107
yana/Views/MeView.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MeView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// 顶部标题
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("我的")
|
||||
.font(.system(size: 22, weight: .semibold))
|
||||
.foregroundColor(.white)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 20)
|
||||
|
||||
// 用户头像区域
|
||||
VStack(spacing: 16) {
|
||||
Circle()
|
||||
.fill(Color.white.opacity(0.2))
|
||||
.frame(width: 80, height: 80)
|
||||
.overlay(
|
||||
Image(systemName: "person.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.white)
|
||||
)
|
||||
|
||||
Text("用户昵称")
|
||||
.font(.system(size: 18, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Text("ID: 123456789")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.7))
|
||||
}
|
||||
.padding(.top, 30)
|
||||
|
||||
// 功能菜单
|
||||
VStack(spacing: 12) {
|
||||
MenuItemView(icon: "gearshape", title: "设置", action: {})
|
||||
MenuItemView(icon: "person.circle", title: "个人信息", action: {})
|
||||
MenuItemView(icon: "heart", title: "我的收藏", action: {})
|
||||
MenuItemView(icon: "clock", title: "浏览历史", action: {})
|
||||
MenuItemView(icon: "questionmark.circle", title: "帮助与反馈", action: {})
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 40)
|
||||
|
||||
// 退出登录按钮
|
||||
Button(action: {}) {
|
||||
HStack {
|
||||
Image(systemName: "rectangle.portrait.and.arrow.right")
|
||||
.font(.system(size: 16))
|
||||
Text("退出登录")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 30)
|
||||
|
||||
// 底部安全区域
|
||||
Color.clear.frame(height: 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 菜单项组件
|
||||
struct MenuItemView: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 20))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 24)
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14))
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.frame(height: 56)
|
||||
.background(
|
||||
Color.white.opacity(0.1)
|
||||
.cornerRadius(12)
|
||||
)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
199
yana/Views/SettingView.swift
Normal file
199
yana/Views/SettingView.swift
Normal file
@@ -0,0 +1,199 @@
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct SettingView: View {
|
||||
let store: StoreOf<SettingFeature>
|
||||
@ObservedObject private var localizationManager = LocalizationManager.shared
|
||||
|
||||
var body: some View {
|
||||
WithPerceptionTracking {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
// 背景图片 - 使用"bg"图片,全屏显示
|
||||
Image("bg")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.ignoresSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Navigation Bar
|
||||
HStack {
|
||||
// 返回按钮
|
||||
Button(action: {
|
||||
store.send(.dismissTapped)
|
||||
}) {
|
||||
Image(systemName: "chevron.left")
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.padding(.leading, 16)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 标题
|
||||
Text("设置")
|
||||
.font(.custom("PingFang SC-Semibold", size: 16))
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
// 占位符,保持标题居中
|
||||
Color.clear
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.top, 8)
|
||||
.padding(.horizontal)
|
||||
|
||||
// 内容区域
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
// 用户信息卡片
|
||||
VStack(spacing: 16) {
|
||||
// 头像区域
|
||||
Image(systemName: "person.circle.fill")
|
||||
.font(.system(size: 60))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
|
||||
// 用户信息
|
||||
VStack(spacing: 8) {
|
||||
if let userInfo = store.userInfo, let userName = userInfo.username {
|
||||
Text(userName)
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
} else {
|
||||
Text("用户")
|
||||
.font(.title2.weight(.semibold))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
// 显示用户ID
|
||||
if let userInfo = store.userInfo, let userId = userInfo.userId {
|
||||
Text("ID: \(userId)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
} else if let accountModel = store.accountModel, let uid = accountModel.uid {
|
||||
Text("UID: \(uid)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
.padding(.horizontal, 20)
|
||||
.background(Color.black.opacity(0.3))
|
||||
.cornerRadius(16)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 32)
|
||||
|
||||
// 设置选项列表
|
||||
VStack(spacing: 12) {
|
||||
// 语言设置
|
||||
SettingRowView(
|
||||
icon: "globe",
|
||||
title: "语言设置",
|
||||
action: {
|
||||
// TODO: 实现语言设置
|
||||
}
|
||||
)
|
||||
|
||||
// 关于我们
|
||||
SettingRowView(
|
||||
icon: "info.circle",
|
||||
title: "关于我们",
|
||||
action: {
|
||||
// TODO: 实现关于页面
|
||||
}
|
||||
)
|
||||
|
||||
// 版本信息
|
||||
HStack {
|
||||
Image(systemName: "app.badge")
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.frame(width: 24)
|
||||
|
||||
Text("版本信息")
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("1.0.0")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.black.opacity(0.2))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Spacer(minLength: 50)
|
||||
|
||||
// 退出登录按钮
|
||||
Button(action: {
|
||||
store.send(.logoutTapped)
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "arrow.right.square")
|
||||
Text("退出登录")
|
||||
}
|
||||
.font(.body.weight(.medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.red.opacity(0.7))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
store.send(.onAppear)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setting Row View
|
||||
struct SettingRowView: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.frame(width: 24)
|
||||
|
||||
Text(title)
|
||||
.foregroundColor(.white)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.white.opacity(0.6))
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 16)
|
||||
.background(Color.black.opacity(0.2))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingView(
|
||||
store: Store(
|
||||
initialState: SettingFeature.State()
|
||||
) {
|
||||
SettingFeature()
|
||||
}
|
||||
)
|
||||
}
|
269
项目问题排查与解决流程.md
Normal file
269
项目问题排查与解决流程.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Yana 项目问题排查与解决流程文档
|
||||
|
||||
## 目录
|
||||
1. [问题概述](#问题概述)
|
||||
2. [解决流程](#解决流程)
|
||||
3. [技术细节](#技术细节)
|
||||
4. [最终解决方案](#最终解决方案)
|
||||
5. [预防措施](#预防措施)
|
||||
6. [常见问题FAQ](#常见问题faq)
|
||||
|
||||
---
|
||||
|
||||
## 问题概述
|
||||
|
||||
### 初始错误
|
||||
**错误信息**: `"Could not compute dependency graph: unable to load transferred PIF: The workspace contains multiple references with the same GUID"`
|
||||
|
||||
**问题表现**:
|
||||
- 项目无法启动
|
||||
- Xcode 无法计算依赖图
|
||||
- 出现 GUID 冲突错误
|
||||
|
||||
### 根本原因分析
|
||||
1. **混合包管理系统**: 项目同时使用了 Swift Package Manager (SPM) 和 CocoaPods
|
||||
2. **缓存冲突**: Xcode DerivedData 与 SPM 状态不同步
|
||||
3. **TCA 结构问题**: 代码中 HomeFeature 缺少必要的状态和 Action 定义
|
||||
|
||||
---
|
||||
|
||||
## 解决流程
|
||||
|
||||
### 第一阶段:GUID 冲突解决
|
||||
|
||||
#### 步骤 1: 清理缓存
|
||||
```bash
|
||||
# 清理 Xcode DerivedData
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
|
||||
# 重置 Swift Package Manager
|
||||
swift package reset
|
||||
swift package resolve
|
||||
```
|
||||
|
||||
#### 步骤 2: 重新安装 CocoaPods
|
||||
```bash
|
||||
pod install --clean-install
|
||||
```
|
||||
|
||||
#### 步骤 3: 验证项目解析
|
||||
```bash
|
||||
xcodebuild -workspace yana.xcworkspace -list
|
||||
```
|
||||
|
||||
### 第二阶段:TCA 结构修复
|
||||
|
||||
#### 问题识别
|
||||
- `HomeFeature.State` 缺少 `isSettingPresented` 和 `settingState` 属性
|
||||
- `HomeFeature.Action` 缺少 `settingDismissed` 和 `setting` actions
|
||||
- `HomeView.swift` 中的 `store.scope()` 调用语法错误
|
||||
|
||||
#### 修复步骤
|
||||
|
||||
**1. 修复 HomeFeature.swift**
|
||||
```swift
|
||||
@ObservableState
|
||||
struct State: Equatable {
|
||||
var isInitialized = false
|
||||
var userInfo: UserInfo?
|
||||
var accountModel: AccountModel?
|
||||
var error: String?
|
||||
|
||||
// 添加设置页面相关状态
|
||||
var isSettingPresented = false
|
||||
var settingState = SettingFeature.State()
|
||||
}
|
||||
|
||||
enum Action: Equatable {
|
||||
case onAppear
|
||||
case loadUserInfo
|
||||
case userInfoLoaded(UserInfo?)
|
||||
case loadAccountModel
|
||||
case accountModelLoaded(AccountModel?)
|
||||
case logoutTapped
|
||||
case logout
|
||||
|
||||
// 添加设置页面相关actions
|
||||
case settingDismissed
|
||||
case setting(SettingFeature.Action)
|
||||
}
|
||||
```
|
||||
|
||||
**2. 添加子 Reducer**
|
||||
```swift
|
||||
var body: some ReducerOf<Self> {
|
||||
Scope(state: \.settingState, action: \.setting) {
|
||||
SettingFeature()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
// ... existing cases ...
|
||||
|
||||
case .settingDismissed:
|
||||
state.isSettingPresented = false
|
||||
return .none
|
||||
|
||||
case .setting:
|
||||
// 由子reducer处理
|
||||
return .none
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. 修复 HomeView.swift**
|
||||
```swift
|
||||
.sheet(isPresented: Binding(
|
||||
get: { store.isSettingPresented },
|
||||
set: { _ in store.send(.settingDismissed) }
|
||||
)) {
|
||||
SettingView(store: store.scope(state: \.settingState, action: \.setting))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 依赖管理配置
|
||||
|
||||
**Swift Package Manager (Package.swift)**:
|
||||
- ComposableArchitecture: 1.20.2+
|
||||
- 其他依赖根据需要添加
|
||||
|
||||
**CocoaPods (Podfile)**:
|
||||
- Alamofire (网络请求)
|
||||
- SDWebImage (图像加载)
|
||||
- CocoaLumberjack (日志)
|
||||
- 其他 UI 相关库
|
||||
|
||||
### TCA 架构模式
|
||||
|
||||
```
|
||||
Feature
|
||||
├── State (数据状态)
|
||||
├── Action (用户操作)
|
||||
├── Reducer (状态转换逻辑)
|
||||
└── Dependencies (外部依赖)
|
||||
```
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
yana/
|
||||
├── Features/ # TCA Feature 定义
|
||||
├── Views/ # SwiftUI 视图
|
||||
├── APIs/ # 网络 API 层
|
||||
├── Utils/ # 工具类
|
||||
└── Managers/ # 管理器类
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最终解决方案
|
||||
|
||||
### 命令执行顺序
|
||||
```bash
|
||||
# 1. 清理环境
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
swift package reset
|
||||
|
||||
# 2. 重新解析依赖
|
||||
swift package resolve
|
||||
pod install --clean-install
|
||||
|
||||
# 3. 验证项目
|
||||
xcodebuild -workspace yana.xcworkspace -scheme yana -configuration Debug build
|
||||
```
|
||||
|
||||
### 关键代码修改
|
||||
1. **HomeFeature.swift**: 添加设置相关状态管理
|
||||
2. **HomeView.swift**: 修复 TCA store 绑定语法
|
||||
3. **SettingFeature.swift**: 确保 Action 完整性
|
||||
|
||||
### 构建结果
|
||||
✅ **编译成功**: Exit code 0
|
||||
⚠️ **警告信息**: 仅 Swift 6 兼容性警告,不影响运行
|
||||
|
||||
---
|
||||
|
||||
## 预防措施
|
||||
|
||||
### 开发规范
|
||||
1. **统一包管理**: 优先使用一种包管理工具
|
||||
2. **定期清理**: 定期清理 DerivedData 避免缓存问题
|
||||
3. **代码审查**: 确保 TCA Feature 结构完整
|
||||
4. **版本控制**: 及时提交关键配置文件
|
||||
|
||||
### 监控指标
|
||||
- [ ] 项目编译时间 < 30s
|
||||
- [ ] 无编译错误
|
||||
- [ ] 依赖解析正常
|
||||
- [ ] TCA 结构完整
|
||||
|
||||
### 工具使用
|
||||
```bash
|
||||
# 项目健康检查脚本
|
||||
check_project() {
|
||||
echo "🔍 检查项目状态..."
|
||||
xcodebuild -workspace yana.xcworkspace -list > /dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ 项目解析正常"
|
||||
else
|
||||
echo "❌ 项目解析失败,需要执行清理流程"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题FAQ
|
||||
|
||||
### Q1: 再次出现 GUID 冲突怎么办?
|
||||
**A**: 执行完整清理流程:
|
||||
```bash
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
swift package reset && swift package resolve
|
||||
pod install --clean-install
|
||||
```
|
||||
|
||||
### Q2: TCA Reducer 编译错误如何处理?
|
||||
**A**: 检查以下项目:
|
||||
- State 属性完整性
|
||||
- Action 枚举完整性
|
||||
- Reducer body 中的 case 处理
|
||||
- 子 Reducer 的 Scope 配置
|
||||
|
||||
### Q3: 如何避免混合包管理器问题?
|
||||
**A**:
|
||||
- 尽量使用单一包管理工具
|
||||
- 如需混合使用,确保依赖版本兼容
|
||||
- 定期更新依赖并测试
|
||||
|
||||
### Q4: Swift 6 兼容性警告如何处理?
|
||||
**A**:
|
||||
- 短期:可以忽略,不影响功能
|
||||
- 长期:逐步迁移到 Swift 6 Sendable 模式
|
||||
|
||||
### Q5: 项目构建缓慢怎么办?
|
||||
**A**:
|
||||
- 使用 `xcodebuild -quiet` 减少输出
|
||||
- 开启 Xcode Build System 并行构建
|
||||
- 定期清理 DerivedData
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
本次问题解决涉及以下关键技术点:
|
||||
1. **Xcode 项目配置管理**
|
||||
2. **Swift Package Manager 与 CocoaPods 共存**
|
||||
3. **TCA (The Composable Architecture) 最佳实践**
|
||||
4. **iOS 开发环境故障排除**
|
||||
|
||||
通过系统性的排查和修复,项目现已恢复正常运行状态。建议团队建立定期维护机制,避免类似问题再次发生。
|
||||
|
||||
---
|
||||
|
||||
**文档更新时间**: 2025-07-10
|
||||
**适用版本**: iOS 15.6+, Swift 6, TCA 1.20.2+
|
||||
**维护者**: AI Assistant & 开发团队
|
Reference in New Issue
Block a user