feat: 键盘动画

This commit is contained in:
jinyaqiu 2025-08-21 19:22:52 +08:00
parent edef20028d
commit 58f560c714
4 changed files with 194 additions and 38 deletions

View File

@ -1,15 +1,24 @@
import SwiftUI import SwiftUI
import Combine
public struct AvatarPicker: View { public struct AvatarPicker: View {
@StateObject private var uploadManager = MediaUploadManager() @StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false @State private var showMediaPicker = false
@State private var isUploading = false @State private var isUploading = false
@State private var uploadedFileId: String? = nil
@State private var uploadTask: AnyCancellable?
@Binding var selectedImage: UIImage? @Binding var selectedImage: UIImage?
@Binding var showUsername: Bool @Binding var showUsername: Bool
@Binding var isKeyboardVisible: Bool
public init(selectedImage: Binding<UIImage?>, showUsername: Binding<Bool>) { //
var onFileUploaded: ((String) -> Void)? = nil
public init(selectedImage: Binding<UIImage?>, showUsername: Binding<Bool>, isKeyboardVisible: Binding<Bool>, onFileUploaded: ((String) -> Void)? = nil) {
self._selectedImage = selectedImage self._selectedImage = selectedImage
self._showUsername = showUsername self._showUsername = showUsername
self._isKeyboardVisible = isKeyboardVisible
self.onFileUploaded = onFileUploaded
} }
public var body: some View { public var body: some View {
@ -23,12 +32,12 @@ public struct AvatarPicker: View {
Image(uiImage: selectedImage) Image(uiImage: selectedImage)
.resizable() .resizable()
.scaledToFill() .scaledToFill()
.frame(width: 225, height: 225) .frame(width: isKeyboardVisible ? 125 : 225, height: isKeyboardVisible ? 125 : 225)
.clipShape(RoundedRectangle(cornerRadius: 20)) .clipShape(RoundedRectangle(cornerRadius: 20))
} else { } else {
// Default SVG avatar // Default SVG avatar
SVGImage(svgName: "Avatar") SVGImage(svgName: "Avatar")
.frame(width: 225, height: 225) .frame(width: isKeyboardVisible ? 125 : 225, height: isKeyboardVisible ? 125 : 225)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
@ -71,17 +80,31 @@ public struct AvatarPicker: View {
if !uploadManager.selectedMedia.isEmpty { if !uploadManager.selectedMedia.isEmpty {
isUploading = true isUploading = true
uploadManager.startUpload() uploadManager.startUpload()
// 使 Combine
uploadTask = uploadManager.$uploadStatus
.receive(on: DispatchQueue.main)
.sink { _ in
if uploadManager.isAllUploaded {
isUploading = false
if let firstResult = uploadManager.getUploadResults().values.first {
self.uploadedFileId = firstResult
self.onFileUploaded?(firstResult)
}
uploadTask?.cancel()
}
}
} }
} }
) )
} }
.onChange(of: uploadManager.uploadStatus) { _ in .onDisappear {
if let firstMedia = uploadManager.selectedMedia.first, uploadTask?.cancel()
case .image(let image) = firstMedia, }
uploadManager.isAllUploaded { .onChange(of: uploadManager.selectedMedia) { newMedia in
if let firstMedia = newMedia.first,
case .image(let image) = firstMedia {
selectedImage = image selectedImage = image
isUploading = false
uploadManager.clearAllMedia()
} }
} }
} }

View File

@ -1,16 +1,81 @@
import SwiftUI import SwiftUI
// MARK: - UIApplication Extension for Hiding Keyboard
#if canImport(UIKit)
extension View {
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
#endif
struct UserInfo: View { struct UserInfo: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var keyboardHeight: CGFloat = 0
@State private var isKeyboardVisible = false
// Sample user data - replace with your actual data model // Sample user data - replace with your actual data model
@State private var userName = "" @State private var userName = ""
@State private var userEmail = "memo@example.com"
@State private var notificationsEnabled = true @State private var notificationsEnabled = true
@State private var darkModeEnabled = false @State private var darkModeEnabled = false
@State private var showLogoutAlert = false @State private var showLogoutAlert = false
@State private var avatarImage: UIImage? @State private var avatarImage: UIImage? = nil
@State private var showUsername: Bool = false @State private var uploadedFileId: String? = nil
@State private var showUsername = false
private struct MessageView: View {
let isKeyboardVisible: Bool
private var singleLineText: some View {
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
.font(Typography.font(for: .caption))
.foregroundColor(.black)
.lineLimit(1)
.truncationMode(.tail)
.padding(3)
}
private var multiLineText: some View {
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
.font(Typography.font(for: .caption))
.foregroundColor(.black)
.fixedSize(horizontal: false, vertical: true)
.padding(3)
}
private var background: some View {
LinearGradient(
gradient: Gradient(colors: [
Color(red: 1.0, green: 0.97, blue: 0.87),
.white,
Color(red: 1.0, green: 0.97, blue: 0.84)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
var body: some View {
HStack {
if isKeyboardVisible {
singleLineText
.transition(.asymmetric(
insertion: .opacity.combined(with: .scale(scale: 0.95)),
removal: .opacity.combined(with: .scale(scale: 1.05))
))
} else {
multiLineText
.transition(.asymmetric(
insertion: .opacity.combined(with: .scale(scale: 1.05)),
removal: .opacity.combined(with: .scale(scale: 0.95))
))
}
}
.background(background)
.animation(.spring(response: 0.3, dampingFraction: 1), value: isKeyboardVisible)
}
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -25,26 +90,14 @@ struct UserInfo: View {
Spacer() Spacer()
} }
.padding() .padding()
HStack(spacing: 20) {
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.") //
.font(Typography.font(for: .caption)) MessageView(isKeyboardVisible: isKeyboardVisible).padding(.bottom, 6)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading) if !isKeyboardVisible {
.padding(.vertical, 10) Spacer()
.background(
LinearGradient(
gradient: Gradient(colors: [
Color(red: 1.0, green: 0.97, blue: 0.87),
.white,
Color(red: 1.0, green: 0.97, blue: 0.84)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
} }
.padding(10)
Spacer()
VStack(spacing: 20) { VStack(spacing: 20) {
// Title // Title
Text(showUsername ? "Add Your Avatar" : "Whats Your Name") Text(showUsername ? "Add Your Avatar" : "Whats Your Name")
@ -55,7 +108,13 @@ struct UserInfo: View {
ZStack { ZStack {
AvatarPicker( AvatarPicker(
selectedImage: $avatarImage, selectedImage: $avatarImage,
showUsername: $showUsername showUsername: $showUsername,
isKeyboardVisible: $isKeyboardVisible,
onFileUploaded: { fileId in
// ID
print("Uploaded file ID: \(fileId)")
uploadedFileId = fileId
}
) )
} }
.padding(.top, 20) .padding(.top, 20)
@ -93,11 +152,28 @@ struct UserInfo: View {
.padding() .padding()
.background(Color(.white)) .background(Color(.white))
.cornerRadius(20) .cornerRadius(20)
Spacer()
if !isKeyboardVisible {
Spacer()
}
Button(action: { Button(action: {
showUsername = true if showUsername {
print("调接口将头像和username传到后端", uploadedFileId ?? "")
// username
UserService.shared.updateUsername(userName, userId: uploadedFileId ?? "") { result in
switch result {
case .success(let response):
print("Username updated: \(response.message ?? "")")
case .failure(let error):
print("Error updating username: \(error.localizedDescription)")
}
}
} else {
showUsername = true
}
}) { }) {
Text("Continue") Text(showUsername ? "Open" : "Continue")
.font(Typography.font(for: .body)) .font(Typography.font(for: .body))
.fontWeight(.bold) .fontWeight(.bold)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -109,10 +185,23 @@ struct UserInfo: View {
) )
} }
.padding() .padding()
} }
.padding() .padding()
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA .background(Color(red: 0.98, green: 0.98, blue: 0.98)) // #FAFAFA
.onTapGesture {
hideKeyboard()
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
withAnimation {
isKeyboardVisible = true
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
withAnimation {
isKeyboardVisible = false
}
}
} }
} }

View File

@ -0,0 +1,44 @@
import Foundation
import os.log
// MARK: - Request/Response Models
struct UpdateUsernameRequest: Codable {
let username: String
let userId: String
}
struct UpdateUsernameResponse: Codable {
let success: Bool
let message: String?
}
// MARK: - UserService
class UserService {
static let shared = UserService()
private let networkService = NetworkService.shared
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.example.app", category: "UserService")
private init() {}
func updateUsername(_ username: String, userId: String, completion: @escaping (Result<UpdateUsernameResponse, NetworkError>) -> Void) {
let parameters: [String: Any] = [
"username": username,
"avatar_file_id": userId
]
logger.info("🔄 开始更新用户信息: 用户名=\(username), 头像ID=\(userId)")
networkService.postWithToken(
path: "/iam/user/info",
parameters: parameters
) { [weak self] (result: Result<UpdateUsernameResponse, NetworkError>) in
switch result {
case .success(let response):
self?.logger.info("✅ 用户信息更新成功: \(response.message ?? "")")
case .failure(let error):
self?.logger.error("❌ 用户信息更新失败: \(error.localizedDescription)")
}
completion(result)
}
}
}