持久化用户id

This commit is contained in:
anlicheng 2025-06-26 17:36:10 +08:00
parent b95420ef74
commit 6323b1d2c9
9 changed files with 169 additions and 42 deletions

View File

@ -0,0 +1,97 @@
//
// KeychainHelper.swift
// dimensionhub
//
// Created by on 2025/6/26.
//
import Foundation
import Security
import UIKit
struct KeychainHelper {
// MARK: -
static func getPersistentUserId() -> String {
let keychainAccount = "com.jihe.dimensionhub.deviceUUID"
// 1. Keychain
if let uuid = load(key: keychainAccount) {
return uuid
}
// 2. UUID
let newUUID = generatorUserId()
// 3. Keychain
let data = newUUID.data(using: .utf8)!
if !save(key: keychainAccount, data: data) {
NSLog("save uuid to keychain error")
}
return newUUID
}
/// Keychain
private static func load(key: String) -> String? {
if let data = loadFromSecurity(key: key) {
return String(data: data, encoding: .utf8)
}
return nil
}
/// Keychain
static func delete(key: String) -> Bool {
let query = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
] as [String: Any]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess || status == errSecItemNotFound
}
// MARK: -
private static func save(key: String, data: Data) -> Bool {
let query = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecValueData as String: data
] as [String: Any]
//
SecItemDelete(query as CFDictionary)
//
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
private static func loadFromSecurity(key: String) -> Data? {
let query = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String: kSecMatchLimitOne
] as [String: Any]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == errSecSuccess {
return dataTypeRef as? Data
}
return nil
}
// uuid
private static func generatorUserId() -> String {
if let uuid = UIDevice.current.identifierForVendor?.uuidString {
return uuid.lowercased()
} else {
return UUID().uuidString.lowercased()
}
}
}

View File

@ -26,12 +26,4 @@ struct Utils {
return attributedString.string return attributedString.string
} }
static func defaultUserId() -> String {
if let uuid = UIDevice.current.identifierForVendor?.uuidString {
return uuid.lowercased()
} else {
return UUID().uuidString.lowercased()
}
}
} }

View File

@ -7,7 +7,7 @@
import SwiftUI import SwiftUI
struct DetailView: View { struct DetailView: View {
@AppStorage("userId") private var userId: String = Utils.defaultUserId() @Environment(\.userId) private var userId
@State var detailModel = DetailModel() @State var detailModel = DetailModel()
@State var showAllSummary: Bool = false @State var showAllSummary: Bool = false

View File

@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
struct FollowListView: View { struct FollowListView: View {
@AppStorage("userId") private var userId: String = Utils.defaultUserId() @Environment(\.userId) private var userId
@State var followModel = FollowListModel() @State var followModel = FollowListModel()
@State var isMoreLoading: Bool = false @State var isMoreLoading: Bool = false

View File

@ -62,7 +62,7 @@ final class DateNavModel {
} }
struct DateNavView: View { struct DateNavView: View {
@AppStorage("userId") private var userId: String = Utils.defaultUserId() @Environment(\.userId) private var userId
@State var navModel = DateNavModel() @State var navModel = DateNavModel()
@Binding var selectGroupId: String @Binding var selectGroupId: String

View File

@ -11,7 +11,7 @@ import Combine
struct IndexMainView: View { struct IndexMainView: View {
@Environment(\.modelContext) private var modelContext @Environment(\.modelContext) private var modelContext
@EnvironmentObject var appNavigation: AppNavigation @EnvironmentObject var appNavigation: AppNavigation
@AppStorage("userId") private var userId: String = Utils.defaultUserId() @Environment(\.userId) private var userId
@State var indexModel = IndexModel() @State var indexModel = IndexModel()
@State private var scrollID: IndexModel.ScrollTarget? = nil @State private var scrollID: IndexModel.ScrollTarget? = nil

View File

@ -7,7 +7,7 @@
import SwiftUI import SwiftUI
struct ListView: View { struct ListView: View {
@AppStorage("userId") private var userId: String = Utils.defaultUserId() @Environment(\.userId) private var userId
@State var detailModel = ListModel() @State var detailModel = ListModel()
let id: Int let id: Int

View File

@ -10,7 +10,7 @@ import SwiftData
struct SearchView: View { struct SearchView: View {
@Environment(\.modelContext) var modelContext @Environment(\.modelContext) var modelContext
@AppStorage("userId") private var userId: String = Utils.defaultUserId() @Environment(\.userId) private var userId
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@State var searchText: String = "" @State var searchText: String = ""
@ -19,6 +19,12 @@ struct SearchView: View {
@FocusState private var isFocused: Bool @FocusState private var isFocused: Bool
@State private var isSearching = false @State private var isSearching = false
//
@State private var showSearchResult: Bool = false
//
@State private var showKeyboard: Bool = true
var body: some View { var body: some View {
VStack(spacing: 12) { VStack(spacing: 12) {
HStack(spacing: 12) { HStack(spacing: 12) {
@ -74,22 +80,36 @@ struct SearchView: View {
.frame(height: 50) .frame(height: 50)
.padding(.top, 12) .padding(.top, 12)
if searchModel.dramaGroups.isEmpty { if showSearchResult {
Spacer() if searchModel.dramaGroups.isEmpty {
Text("什么都没有找到") Spacer()
.font(.system(size: 18)) Text("什么都没有找到")
.foregroundColor(Color(hex: "#333333")) .font(.system(size: 18))
Spacer() .foregroundColor(Color(hex: "#333333"))
} else { Spacer()
ScrollView(.vertical, showsIndicators: false) { } else {
LazyVStack(alignment: .center, spacing: 10) { ScrollView(.vertical, showsIndicators: false) {
ForEach(searchModel.dramaGroups, id: \.group_id) { group in LazyVStack(alignment: .center, spacing: 10) {
SearchDramaGroupView(group: group) ForEach(searchModel.dramaGroups, id: \.group_id) { group in
SearchDramaGroupView(group: group)
}
} }
.padding(.top, 8)
} }
.padding(.top, 8) .onTapGesture {
isFocused = false
}
.simultaneousGesture(
DragGesture().onChanged{ _ in
isFocused = false
}
)
} }
} else {
//
Spacer()
} }
} }
.navigationTitle("") .navigationTitle("")
.navigationBarBackButtonHidden(true) .navigationBarBackButtonHidden(true)
@ -97,8 +117,10 @@ struct SearchView: View {
.ignoresSafeArea(.keyboard, edges: .bottom) // .ignoresSafeArea(.keyboard, edges: .bottom) //
.onAppear { .onAppear {
// //
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { if showKeyboard {
isFocused = true DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
isFocused = true
}
} }
} }
} }
@ -108,7 +130,15 @@ struct SearchView: View {
if !trimmed.isEmpty { if !trimmed.isEmpty {
Task.detached { Task.detached {
await searchModel.search(userId: userId, name: trimmed) await searchModel.search(userId: userId, name: trimmed)
DispatchQueue.main.async {
//
self.showSearchResult = true
}
} }
//
self.showKeyboard = false
// //
// let history = SearchHistory(keyword: trimmed, timestamp: Date()) // let history = SearchHistory(keyword: trimmed, timestamp: Date())
// modelContext.insert(history) // modelContext.insert(history)

View File

@ -33,11 +33,8 @@ struct dimensionhubApp: App {
}() }()
init() { init() {
if let userId = UserDefaults.standard.string(forKey: "userId") { let userId = KeychainHelper.getPersistentUserId()
print("user_id is: \(userId)") print("user_id is: \(userId)")
} else {
UserDefaults.standard.set(Utils.defaultUserId(), forKey: "userId")
}
} }
var body: some Scene { var body: some Scene {
@ -54,6 +51,7 @@ struct dimensionhubApp: App {
SearchView() SearchView()
} }
} }
.environment(\.userId, KeychainHelper.getPersistentUserId())
} }
.navigationViewStyle(.stack) .navigationViewStyle(.stack)
.tint(.black) .tint(.black)
@ -79,20 +77,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().delegate = self
// //
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {granted, error in UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {granted, error in
print("通知权限 granted: \(granted)") NSLog("通知权限 granted: \(granted)")
guard granted else { guard granted else {
return return
} }
// //
UNUserNotificationCenter.current().getNotificationSettings { settings in UNUserNotificationCenter.current().getNotificationSettings { settings in
print("通知设置: \(settings)") NSLog("通知设置: \(settings)")
guard settings.authorizationStatus == .authorized else { guard settings.authorizationStatus == .authorized else {
return return
} }
print("通知设置: authorized!!!!") NSLog("通知设置: authorized!!!!")
DispatchQueue.main.async { DispatchQueue.main.async {
// //
@ -108,12 +106,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined() let token = tokenParts.joined()
print("Device Token: \(token)") NSLog("Device Token: \(token)")
// deviceToken // deviceToken
Task { Task {
guard let userId = UserDefaults.standard.string(forKey: "userId") else { let userId = KeychainHelper.getPersistentUserId()
return
}
let sendResult = await API.sendDeviceTokenToServer(userId: userId, token: token, as: String.self) let sendResult = await API.sendDeviceTokenToServer(userId: userId, token: token, as: String.self)
switch sendResult { switch sendResult {
@ -127,7 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// //
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("注册远程通知失败: \(error)") NSLog("注册远程通知失败: \(error)")
} }
// App // App
@ -191,8 +187,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private func handleDeepLink(_ link: String) { private func handleDeepLink(_ link: String) {
// //
print("处理深度链接: \(link)") NSLog("处理深度链接: \(link)")
} }
} }
// userId
struct UserId: EnvironmentKey {
static let defaultValue: String = KeychainHelper.getPersistentUserId()
}
extension EnvironmentValues {
var userId: String {
get { self[UserId.self] }
set { self[UserId.self] = newValue }
}
}