feat: 键盘动画

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

View File

@ -1,17 +1,26 @@
import SwiftUI
import Combine
public struct AvatarPicker: View {
@StateObject private var uploadManager = MediaUploadManager()
@State private var showMediaPicker = false
@State private var isUploading = false
@State private var uploadedFileId: String? = nil
@State private var uploadTask: AnyCancellable?
@Binding var selectedImage: UIImage?
@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._showUsername = showUsername
self._isKeyboardVisible = isKeyboardVisible
self.onFileUploaded = onFileUploaded
}
public var body: some View {
VStack(spacing: 20) {
// Avatar Image
@ -23,12 +32,12 @@ public struct AvatarPicker: View {
Image(uiImage: selectedImage)
.resizable()
.scaledToFill()
.frame(width: 225, height: 225)
.frame(width: isKeyboardVisible ? 125 : 225, height: isKeyboardVisible ? 125 : 225)
.clipShape(RoundedRectangle(cornerRadius: 20))
} else {
// Default SVG avatar
SVGImage(svgName: "Avatar")
.frame(width: 225, height: 225)
.frame(width: isKeyboardVisible ? 125 : 225, height: isKeyboardVisible ? 125 : 225)
.contentShape(Rectangle())
}
@ -71,17 +80,31 @@ public struct AvatarPicker: View {
if !uploadManager.selectedMedia.isEmpty {
isUploading = true
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
if let firstMedia = uploadManager.selectedMedia.first,
case .image(let image) = firstMedia,
uploadManager.isAllUploaded {
.onDisappear {
uploadTask?.cancel()
}
.onChange(of: uploadManager.selectedMedia) { newMedia in
if let firstMedia = newMedia.first,
case .image(let image) = firstMedia {
selectedImage = image
isUploading = false
uploadManager.clearAllMedia()
}
}
}

View File

@ -1,17 +1,82 @@
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 {
@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
@State private var userName = ""
@State private var userEmail = "memo@example.com"
@State private var notificationsEnabled = true
@State private var darkModeEnabled = false
@State private var showLogoutAlert = false
@State private var avatarImage: UIImage?
@State private var showUsername: Bool = false
@State private var avatarImage: UIImage? = nil
@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 {
VStack(spacing: 0) {
HStack {
@ -25,26 +90,14 @@ struct UserInfo: View {
Spacer()
}
.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))
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 10)
.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
)
)
//
MessageView(isKeyboardVisible: isKeyboardVisible).padding(.bottom, 6)
if !isKeyboardVisible {
Spacer()
}
.padding(10)
Spacer()
VStack(spacing: 20) {
// Title
Text(showUsername ? "Add Your Avatar" : "Whats Your Name")
@ -55,7 +108,13 @@ struct UserInfo: View {
ZStack {
AvatarPicker(
selectedImage: $avatarImage,
showUsername: $showUsername
showUsername: $showUsername,
isKeyboardVisible: $isKeyboardVisible,
onFileUploaded: { fileId in
// ID
print("Uploaded file ID: \(fileId)")
uploadedFileId = fileId
}
)
}
.padding(.top, 20)
@ -87,17 +146,34 @@ struct UserInfo: View {
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.themePrimaryLight)
)
)
}
}
.padding()
.background(Color(.white))
.cornerRadius(20)
Spacer()
if !isKeyboardVisible {
Spacer()
}
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))
.fontWeight(.bold)
.frame(maxWidth: .infinity)
@ -109,10 +185,23 @@ struct UserInfo: View {
)
}
.padding()
}
}
.padding()
.navigationBarTitleDisplayMode(.inline)
.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)
}
}
}