feat: 优化视图组件与数据迁移逻辑

- 移除DataMigrationManager类,简化数据迁移逻辑。
- 在FeedListView和MeView中新增图片预览功能,提升用户体验。
- 更新OptimizedDynamicCardView以支持图片点击回调,增强交互性。
- 新增PreviewItem结构体以管理图片预览状态,提升代码可读性与维护性。
- 清理AppDelegate中的冗余代码,优化启动流程。
This commit is contained in:
edwinQQQ
2025-07-25 16:22:38 +08:00
parent 815091a2ff
commit 79fc03b52a
10 changed files with 174 additions and 562 deletions

View File

@@ -9,6 +9,15 @@
"version" : "1.0.3"
}
},
{
"identity" : "liquidglass",
"kind" : "remoteSourceControl",
"location" : "https://github.com/BarredEwe/LiquidGlass.git",
"state" : {
"revision" : "d5bf927a08a97c2d94db7ef71f1e15f8532d1005",
"version" : "0.7.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",

View File

@@ -16,13 +16,15 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.20.2"),
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", branch: "main")
.package(url: "https://github.com/pointfreeco/swift-case-paths.git", branch: "main"),
.package(url: "https://github.com/BarredEwe/LiquidGlass.git", from: "0.7.0")
],
targets: [
.target(
name: "yana",
dependencies: [
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
"LiquidGlass"
],
path: "yana",
),

View File

@@ -88,7 +88,7 @@
"location" : "https://github.com/pointfreeco/swift-navigation",
"state" : {
"revision" : "ae208d1a5cf33aee1d43734ea780a09ada6e2a21",
"version" : "2.3.1"
"version" : "2.3.2"
}
},
{

View File

@@ -3,84 +3,10 @@ import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
private func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) async -> Bool {
// isPerceptionCheckingEnabled = false
// UserDefaults Keychain
DataMigrationManager.performStartupMigration()
//
await UserInfoManager.preloadCache()
//
// NetworkManager.shared.networkStatusChanged = { status in
// print("🌍 \(status)")
// }
#if DEBUG
// 🔍 DESOC
// print("🔐 使OCDES")
// DESEncryptOCTest.runInAppDelegate()
// - 使
// let testURL = URL(string: "http://192.168.10.211:8080/oauth/token")!
// var request = URLRequest(url: testURL)
// request.httpMethod = "POST"
// request.setValue("application/json", forHTTPHeaderField: "Content-Type")
// request.setValue("application/json", forHTTPHeaderField: "Accept")
// request.setValue("zh-Hant", forHTTPHeaderField: "Accept-Language")
//
// //
// let testParameters: [String: Any] = [
// "ispType": "65535",
// "phone": "3+TbIQYiwIk=",
// "netType": 2,
// "channel": "molistar_enterprise",
// "version": "20.20.61",
// "pub_sign": "2E7C50AA17A20B32A0023F20B7ECE108",
// "osVersion": "16.4",
// "deviceId": "b715b75715e3417c9c70e72bbe502c6c",
// "grant_type": "password",
// "os": "iOS",
// "app": "youmi",
// "password": "nTW/lEgupIQ=",
// "client_id": "erban-client",
// "lang": "zh-Hant-CN",
// "client_secret": "uyzjdhds",
// "Accept-Language": "zh-Hant",
// "model": "iPhone XR",
// "appVersion": "1.0.0"
// ]
//
// do {
// let jsonData = try JSONSerialization.data(withJSONObject: testParameters, options: .prettyPrinted)
// request.httpBody = jsonData
//
// print("🛠 URLSession")
// print("📍 : \(testURL.absoluteString)")
// print("📦 : \(String(data: jsonData, encoding: .utf8) ?? "")")
//
// URLSession.shared.dataTask(with: request) { data, response, error in
// DispatchQueue.main.async {
// let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
// let responseString = data != nil ? String(data: data!, encoding: .utf8) ?? "" : ""
//
// print("""
// === ===
// 🔗 URL: \(testURL.absoluteString)
// 📊 : \(statusCode)
// : \(error?.localizedDescription ?? "")
// 📦 : \(data?.count ?? 0) bytes
// 📄 : \(responseString)
// ==================
// """)
// }
// }.resume()
// } catch {
// print(" JSON: \(error.localizedDescription)")
// }
#endif
// NIMConfigurationManager.setupNimSDK()
return true

View File

@@ -50,8 +50,6 @@ struct MainFeature {
MeFeature()
}
Reduce { state, action in
debugInfoSync("MainFeature action: \(action)")
debugInfoSync("MainFeature state: \(state)")
switch action {
case .onAppear:
return .run { send in
@@ -84,7 +82,6 @@ struct MainFeature {
let nickname = userInfo?.nick ?? ""
state.appSettingState = AppSettingFeature.State(nickname: nickname, avatarURL: avatarURL, userInfo: userInfo)
state.navigationPath.append(.appSetting)
debugInfoSync("\(state.navigationPath)")
return .none
case .me:
return .none

View File

@@ -1,357 +0,0 @@
import Foundation
///
///
/// UserDefaults Keychain
///
///
///
/// 1.
/// 2. Keychain
/// 3.
/// 4.
@MainActor
final class DataMigrationManager {
// MARK: -
static let shared = DataMigrationManager()
private init() {}
// MARK: -
private let migrationCompleteKey = "keychain_migration_completed_v1"
// MARK: -
private enum LegacyStorageKeys {
static let userId = "user_id"
static let accessToken = "access_token"
static let userInfo = "user_info"
static let accountModel = "account_model"
static let appLanguage = "AppLanguage"
}
// MARK: -
enum MigrationResult {
case completed //
case alreadyMigrated //
case noDataToMigrate //
case failed(Error) //
var description: String {
switch self {
case .completed:
return "数据迁移完成"
case .alreadyMigrated:
return "数据已经迁移过"
case .noDataToMigrate:
return "没有需要迁移的数据"
case .failed(let error):
return "迁移失败: \(error.localizedDescription)"
}
}
}
// MARK: -
///
/// - Returns:
func performMigration() -> MigrationResult {
debugInfoSync("🔄 开始检查数据迁移...")
//
if isMigrationCompleted() {
debugInfoSync("✅ 数据已经迁移过,跳过迁移")
return .alreadyMigrated
}
//
let legacyData = collectLegacyData()
if legacyData.isEmpty {
debugInfoSync(" 没有发现需要迁移的数据")
markMigrationCompleted()
return .noDataToMigrate
}
debugInfoSync("📦 发现需要迁移的数据: \(legacyData.keys.joined(separator: ", "))")
do {
//
try migrateToKeychain(legacyData)
//
try verifyMigration(legacyData)
//
cleanupLegacyData(legacyData.keys)
//
markMigrationCompleted()
debugInfoSync("✅ 数据迁移完成")
return .completed
} catch {
debugErrorSync("❌ 数据迁移失败: \(error)")
return .failed(error)
}
}
///
func forceMigration() -> MigrationResult {
resetMigrationStatus()
return performMigration()
}
// MARK: -
///
private func isMigrationCompleted() -> Bool {
return UserDefaults.standard.bool(forKey: migrationCompleteKey)
}
///
private func markMigrationCompleted() {
UserDefaults.standard.set(true, forKey: migrationCompleteKey)
UserDefaults.standard.synchronize()
}
///
private func resetMigrationStatus() {
UserDefaults.standard.removeObject(forKey: migrationCompleteKey)
UserDefaults.standard.synchronize()
}
///
private func collectLegacyData() -> [String: Any] {
let userDefaults = UserDefaults.standard
var legacyData: [String: Any] = [:]
//
if let userId = userDefaults.string(forKey: LegacyStorageKeys.userId) {
legacyData[LegacyStorageKeys.userId] = userId
}
if let accessToken = userDefaults.string(forKey: LegacyStorageKeys.accessToken) {
legacyData[LegacyStorageKeys.accessToken] = accessToken
}
if let userInfoData = userDefaults.data(forKey: LegacyStorageKeys.userInfo) {
legacyData[LegacyStorageKeys.userInfo] = userInfoData
}
if let accountModelData = userDefaults.data(forKey: LegacyStorageKeys.accountModel) {
legacyData[LegacyStorageKeys.accountModel] = accountModelData
}
if let appLanguage = userDefaults.string(forKey: LegacyStorageKeys.appLanguage) {
legacyData[LegacyStorageKeys.appLanguage] = appLanguage
}
return legacyData
}
/// Keychain
private func migrateToKeychain(_ legacyData: [String: Any]) throws {
let keychain = KeychainManager.shared
// AccountModel
if let accountModelData = legacyData[LegacyStorageKeys.accountModel] as? Data {
do {
let accountModel = try JSONDecoder().decode(AccountModel.self, from: accountModelData)
try keychain.store(accountModel, forKey: "account_model")
debugInfoSync("✅ AccountModel 迁移成功")
} catch {
debugErrorSync("❌ AccountModel 迁移失败: \(error)")
// AccountModel
try migrateAccountModelFromIndependentFields(legacyData)
}
} else {
// AccountModel
try migrateAccountModelFromIndependentFields(legacyData)
}
// UserInfo
if let userInfoData = legacyData[LegacyStorageKeys.userInfo] as? Data {
do {
let userInfo = try JSONDecoder().decode(UserInfo.self, from: userInfoData)
try keychain.store(userInfo, forKey: "user_info")
debugInfoSync("✅ UserInfo 迁移成功")
} catch {
debugErrorSync("❌ UserInfo 迁移失败: \(error)")
throw error
}
}
//
if let appLanguage = legacyData[LegacyStorageKeys.appLanguage] as? String {
try keychain.storeString(appLanguage, forKey: "AppLanguage")
debugInfoSync("✅ 语言设置迁移成功")
}
}
/// AccountModel
private func migrateAccountModelFromIndependentFields(_ legacyData: [String: Any]) throws {
guard let userId = legacyData[LegacyStorageKeys.userId] as? String,
let accessToken = legacyData[LegacyStorageKeys.accessToken] as? String else {
debugInfoSync(" 没有足够的独立字段来重建 AccountModel")
return
}
let accountModel = AccountModel(
uid: userId,
jti: nil,
tokenType: "bearer",
refreshToken: nil,
netEaseToken: nil,
accessToken: accessToken,
expiresIn: nil,
scope: nil,
ticket: nil
)
try KeychainManager.shared.store(accountModel, forKey: "account_model")
debugInfoSync("✅ 从独立字段重建 AccountModel 成功")
}
///
private func verifyMigration(_ legacyData: [String: Any]) throws {
let keychain = KeychainManager.shared
// AccountModel
if legacyData[LegacyStorageKeys.accountModel] != nil ||
(legacyData[LegacyStorageKeys.userId] != nil && legacyData[LegacyStorageKeys.accessToken] != nil) {
let accountModel: AccountModel? = try keychain.retrieve(AccountModel.self, forKey: "account_model")
guard accountModel != nil else {
throw MigrationError.verificationFailed("AccountModel 验证失败")
}
}
// UserInfo
if legacyData[LegacyStorageKeys.userInfo] != nil {
let userInfo: UserInfo? = try keychain.retrieve(UserInfo.self, forKey: "user_info")
guard userInfo != nil else {
throw MigrationError.verificationFailed("UserInfo 验证失败")
}
}
//
if legacyData[LegacyStorageKeys.appLanguage] != nil {
let appLanguage = try keychain.retrieveString(forKey: "AppLanguage")
guard appLanguage != nil else {
throw MigrationError.verificationFailed("语言设置验证失败")
}
}
debugInfoSync("✅ 迁移数据验证成功")
}
///
private func cleanupLegacyData(_ keys: Dictionary<String, Any>.Keys) {
let userDefaults = UserDefaults.standard
for key in keys {
userDefaults.removeObject(forKey: key)
debugInfoSync("🗑️ 清理旧数据: \(key)")
}
userDefaults.synchronize()
debugInfoSync("✅ 旧数据清理完成")
}
}
// MARK: -
enum MigrationError: Error, LocalizedError {
case verificationFailed(String)
case dataCorrupted(String)
case keychainError(Error)
var errorDescription: String? {
switch self {
case .verificationFailed(let message):
return "验证失败: \(message)"
case .dataCorrupted(let message):
return "数据损坏: \(message)"
case .keychainError(let error):
return "Keychain 错误: \(error.localizedDescription)"
}
}
}
// MARK: -
extension DataMigrationManager {
///
/// AppDelegate App
static func performStartupMigration() {
let migrationResult = DataMigrationManager.shared.performMigration()
switch migrationResult {
case .completed:
debugInfoSync("🎉 应用启动时数据迁移完成")
case .alreadyMigrated:
break //
case .noDataToMigrate:
break //
case .failed(let error):
debugErrorSync("⚠️ 应用启动时数据迁移失败: \(error)")
//
}
}
}
// MARK: -
#if DEBUG
extension DataMigrationManager {
///
func debugPrintLegacyData() {
let legacyData = collectLegacyData()
debugInfoSync("🔍 旧版本数据:")
for (key, value) in legacyData {
debugInfoSync(" - \(key): \(type(of: value))")
}
}
///
func debugCreateLegacyData() {
let userDefaults = UserDefaults.standard
userDefaults.set("test_user_123", forKey: LegacyStorageKeys.userId)
userDefaults.set("test_access_token", forKey: LegacyStorageKeys.accessToken)
userDefaults.set("zh-Hans", forKey: LegacyStorageKeys.appLanguage)
userDefaults.synchronize()
debugInfoSync("🧪 已创建测试用的旧版本数据")
}
///
func debugClearAllData() {
// Keychain
do {
try KeychainManager.shared.clearAll()
} catch {
debugErrorSync("❌ 清除 Keychain 数据失败: \(error)")
}
// UserDefaults
let userDefaults = UserDefaults.standard
let allKeys = [
LegacyStorageKeys.userId,
LegacyStorageKeys.accessToken,
LegacyStorageKeys.userInfo,
LegacyStorageKeys.accountModel,
LegacyStorageKeys.appLanguage,
migrationCompleteKey
]
for key in allKeys {
userDefaults.removeObject(forKey: key)
}
userDefaults.synchronize()
debugInfoSync("🧪 已清除所有迁移相关数据")
}
}
#endif

View File

@@ -7,108 +7,105 @@ struct OptimizedDynamicCardView: View {
let moment: MomentsInfo
let allMoments: [MomentsInfo]
let currentIndex: Int
//
let onImageTap: (_ images: [String], _ index: Int) -> Void
//
@State private var showPreview = false
@State private var previewImageUrls: [String] = []
@State private var previewIndex: Int = 0
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int) {
init(moment: MomentsInfo, allMoments: [MomentsInfo], currentIndex: Int, onImageTap: @escaping (_ images: [String], _ index: Int) -> Void) {
self.moment = moment
self.allMoments = allMoments
self.currentIndex = currentIndex
self.onImageTap = onImageTap
}
public var body: some View {
VStack(alignment: .leading, spacing: 12) {
//
HStack(alignment: .top) {
//
CachedAsyncImage(url: moment.avatar) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Circle()
.fill(Color.gray.opacity(0.3))
.overlay(
Text(String(moment.nick.prefix(1)))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
)
}
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(moment.nick)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
Text("ID: \(moment.uid)")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
Spacer()
// VIP
Text(formatDisplayTime(moment.publishTime))
.font(.system(size: 12, weight: .bold))
.foregroundColor(.white.opacity(0.8))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.white.opacity(0.15))
.cornerRadius(4)
}
//
if !moment.content.isEmpty {
Text(moment.content)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.leading)
.padding(.leading, 40 + 8) //
}
//
if let images = moment.dynamicResList, !images.isEmpty {
OptimizedImageGrid(images: images) { tappedIndex in
previewImageUrls = images.map { $0.resUrl ?? "" }
previewIndex = tappedIndex
showPreview = true
}
.padding(.bottom, images.count == 2 ? 16 : 0) //
}
//
HStack(spacing: 20) {
// Like
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: moment.isLike ? "heart.fill" : "heart")
.font(.system(size: 16))
Text("\(moment.likeCount)")
.font(.system(size: 14))
ZStack {
//
RoundedRectangle(cornerRadius: 12)
.fill(Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.white.opacity(0.1), lineWidth: 1)
)
.shadow(color: Color(red: 0.43, green: 0.43, blue: 0.43, opacity: 0.34), radius: 10.7, x: 0, y: 1.9)
//
VStack(alignment: .leading, spacing: 12) {
//
HStack(alignment: .top) {
//
CachedAsyncImage(url: moment.avatar) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Circle()
.fill(Color.gray.opacity(0.3))
.overlay(
Text(String(moment.nick.prefix(1)))
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
)
}
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
.frame(width: 40, height: 40)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(moment.nick)
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
Text("ID: \(moment.uid)")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.6))
}
Spacer()
// VIP
Text(formatDisplayTime(moment.publishTime))
.font(.system(size: 12, weight: .bold))
.foregroundColor(.white.opacity(0.8))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.white.opacity(0.15))
.cornerRadius(4)
}
Spacer()
//
if !moment.content.isEmpty {
Text(moment.content)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.leading)
.padding(.leading, 40 + 8) //
}
//
if let images = moment.dynamicResList, !images.isEmpty {
OptimizedImageGrid(images: images) { tappedIndex in
let urls = images.map { $0.resUrl ?? "" }
onImageTap(urls, tappedIndex)
}
.padding(.bottom, images.count == 2 ? 46 : 0) //
}
//
HStack(spacing: 20) {
// Like
Button(action: {}) {
HStack(spacing: 4) {
Image(systemName: moment.isLike ? "heart.fill" : "heart")
.font(.system(size: 16))
Text("\(moment.likeCount)")
.font(.system(size: 14))
}
.foregroundColor(moment.isLike ? .red : .white.opacity(0.8))
}
Spacer()
}
.padding(.top, 8)
}
.padding(.top, 8)
.padding(16)
}
.padding(16)
.background(
Color.white.opacity(0.1)
.cornerRadius(12)
)
.onAppear {
preloadNearbyImages()
}
//
.fullScreenCover(isPresented: $showPreview) {
ImagePreviewPager(images: previewImageUrls, currentIndex: $previewIndex) {
showPreview = false
previewImageUrls = []
}
}
}
private func formatTime(_ timestamp: Int) -> String {

View File

@@ -0,0 +1,7 @@
import Foundation
struct PreviewItem: Identifiable, Equatable {
let id = UUID()
let images: [String]
let index: Int
}

View File

@@ -1,9 +1,10 @@
import SwiftUI
import ComposableArchitecture
//import OptimizedDynamicCardView //
struct FeedListView: View {
let store: StoreOf<FeedListFeature>
//
@State private var previewItem: PreviewItem? = nil
var body: some View {
WithViewStore(self.store, observe: { $0 }) { viewStore in
@@ -19,43 +20,35 @@ struct FeedListView: View {
.ignoresSafeArea(.all)
VStack(alignment: .center, spacing: 0) {
//
HStack {
Button(action: {
viewStore.send(.testButtonTapped)
}) {
Text("测试")
.font(.system(size: 14))
ZStack {
HStack {
Spacer(minLength: 0)
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.blue.opacity(0.7))
.cornerRadius(8)
.frame(maxWidth: .infinity, alignment: .center)
Spacer(minLength: 0)
}
Spacer(minLength: 0)
Spacer(minLength: 0)
Text(NSLocalizedString("feedList.title", comment: "Enjoy your Life Time"))
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
.frame(maxWidth: .infinity, alignment: .center)
Spacer(minLength: 0)
Button(action: {
viewStore.send(.editFeedButtonTapped)
}) {
Image("add icon")
.resizable()
.frame(width: 36, height: 36)
HStack {
Spacer(minLength: 0)
Button(action: {
viewStore.send(.editFeedButtonTapped)
}) {
Image("add icon")
.resizable()
.frame(width: 40, height: 40)
}
}
}
.padding(.horizontal, 20)
.padding(.top, geometry.safeAreaInsets.top)
//
Image(systemName: "heart.fill")
.font(.system(size: 60))
.foregroundColor(.red)
.padding(.top, 40)
Image("Volume")
.frame(width: 56, height: 41)
.padding(.top, 16)
Text(NSLocalizedString("feedList.slogan", comment: "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)
.multilineTextAlignment(.leading)
.foregroundColor(.white.opacity(0.9))
.padding(.horizontal, 30)
.padding(.bottom, 30)
@@ -81,7 +74,14 @@ struct FeedListView: View {
WithPerceptionTracking {
LazyVStack(spacing: 16) {
ForEach(Array(viewStore.moments.enumerated()), id: \ .element.dynamicId) { index, moment in
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
OptimizedDynamicCardView(
moment: moment,
allMoments: viewStore.moments,
currentIndex: index,
onImageTap: { images, tappedIndex in
previewItem = PreviewItem(images: images, index: tappedIndex)
}
)
//
if index == viewStore.moments.count - 1, viewStore.hasMore, !viewStore.isLoadingMore {
Color.clear
@@ -97,6 +97,8 @@ struct FeedListView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.padding(.vertical, 8)
}
//
Color.clear.frame(height: 120)
}
.padding(.horizontal, 16)
.padding(.top, 10)
@@ -133,6 +135,12 @@ struct FeedListView: View {
}
)
}
//
.fullScreenCover(item: $previewItem) { item in
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
previewItem = nil
}
}
}
}
}

View File

@@ -3,6 +3,8 @@ import ComposableArchitecture
struct MeView: View {
let store: StoreOf<MeFeature>
//
@State private var previewItem: PreviewItem? = nil
var body: some View {
WithPerceptionTracking {
@@ -14,8 +16,8 @@ struct MeView: View {
.frame(maxWidth: .infinity, maxHeight: .infinity)
.clipped()
.ignoresSafeArea(.all)
VStack(spacing: 0) {
//
//
VStack {
HStack {
Spacer()
WithViewStore(self.store, observe: { $0 }) { viewStore in
@@ -23,13 +25,16 @@ struct MeView: View {
viewStore.send(.settingButtonTapped)
}) {
Image(systemName: "gearshape")
.font(.system(size: 22, weight: .medium))
.font(.system(size: 33, weight: .medium))
.foregroundColor(.white)
}
.padding(.trailing, 16)
.padding(.top, 8)
}
}
Spacer()
}
VStack(spacing: 16) {
//
WithViewStore(self.store, observe: { $0 }) { viewStore in
if viewStore.isLoadingUserInfo {
@@ -61,6 +66,7 @@ struct MeView: View {
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
.padding(.top, 0)
.frame(height: 130)
} else {
Spacer().frame(height: 130)
@@ -96,9 +102,17 @@ struct MeView: View {
ScrollView {
WithPerceptionTracking {
LazyVStack(spacing: 12) {
ForEach(Array(viewStore.moments.enumerated()), id: \.element.dynamicId) { index, moment in
OptimizedDynamicCardView(moment: moment, allMoments: viewStore.moments, currentIndex: index)
.padding(.horizontal, 12)
ForEach(viewStore.moments.indices, id: \ .self) { index in
let moment = viewStore.moments[index]
OptimizedDynamicCardView(
moment: moment,
allMoments: viewStore.moments,
currentIndex: index,
onImageTap: { images, tappedIndex in
previewItem = PreviewItem(images: images, index: tappedIndex)
}
)
.padding(.horizontal, 12)
}
if viewStore.hasMore {
ProgressView()
@@ -106,6 +120,8 @@ struct MeView: View {
viewStore.send(.loadMore)
}
}
//
Color.clear.frame(height: 120)
}
.padding(.top, 8)
}
@@ -118,11 +134,18 @@ struct MeView: View {
Spacer()
}
.frame(maxWidth: .infinity, alignment: .top)
.padding(.top, 8)
}
}
.onAppear {
ViewStore(self.store, observe: { $0 }).send(.onAppear)
}
//
.fullScreenCover(item: $previewItem) { item in
ImagePreviewPager(images: item.images, currentIndex: .constant(item.index)) {
previewItem = nil
}
}
}
}
}