feat: wan
This commit is contained in:
parent
c786c3192a
commit
e539bb37d2
Binary file not shown.
@ -41,3 +41,19 @@ struct LoginResponseData: Codable {
|
|||||||
|
|
||||||
/// 认证响应模型
|
/// 认证响应模型
|
||||||
typealias AuthResponse = BaseResponse<LoginResponseData>
|
typealias AuthResponse = BaseResponse<LoginResponseData>
|
||||||
|
|
||||||
|
/// 用户信息响应数据
|
||||||
|
struct UserInfoData: Codable {
|
||||||
|
let userId: String
|
||||||
|
let username: String
|
||||||
|
let avatarFileId: String?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case userId = "user_id"
|
||||||
|
case username
|
||||||
|
case avatarFileId = "avatar_file_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户信息响应模型
|
||||||
|
typealias UserInfoResponse = BaseResponse<UserInfoData>
|
||||||
|
|||||||
@ -7,57 +7,89 @@ public struct AvatarPicker: View {
|
|||||||
@Binding var selectedImage: UIImage?
|
@Binding var selectedImage: UIImage?
|
||||||
@Binding var showUsername: Bool
|
@Binding var showUsername: Bool
|
||||||
@Binding var isKeyboardVisible: Bool
|
@Binding var isKeyboardVisible: Bool
|
||||||
|
@Binding var uploadedFileId: String?
|
||||||
|
|
||||||
public init(selectedImage: Binding<UIImage?>, showUsername: Binding<Bool>, isKeyboardVisible: Binding<Bool>) {
|
// Animation state
|
||||||
|
@State private var isAnimating = false
|
||||||
|
|
||||||
|
public init(selectedImage: Binding<UIImage?>, showUsername: Binding<Bool>, isKeyboardVisible: Binding<Bool>, uploadedFileId: Binding<String?>) {
|
||||||
self._selectedImage = selectedImage
|
self._selectedImage = selectedImage
|
||||||
self._showUsername = showUsername
|
self._showUsername = showUsername
|
||||||
self._isKeyboardVisible = isKeyboardVisible
|
self._isKeyboardVisible = isKeyboardVisible
|
||||||
|
self._uploadedFileId = uploadedFileId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var avatarSize: CGFloat {
|
||||||
|
isKeyboardVisible ? 125 : 225
|
||||||
|
}
|
||||||
|
|
||||||
|
private var borderWidth: CGFloat {
|
||||||
|
isKeyboardVisible ? 3 : 4
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Avatar Image
|
// Avatar Image Button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||||
showMediaPicker = true
|
showMediaPicker = true
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
ZStack {
|
ZStack {
|
||||||
if let selectedImage = selectedImage {
|
if let selectedImage = selectedImage {
|
||||||
Image(uiImage: selectedImage)
|
Image(uiImage: selectedImage)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.scaledToFill()
|
||||||
.frame(width: isKeyboardVisible ? 125 : 225,
|
.frame(width: avatarSize, height: avatarSize)
|
||||||
height: isKeyboardVisible ? 125 : 225)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 20))
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 20)
|
RoundedRectangle(cornerRadius: 20)
|
||||||
.stroke(Color.themePrimary, lineWidth: 4)
|
.stroke(Color.themePrimary, lineWidth: borderWidth)
|
||||||
)
|
)
|
||||||
.animation(.spring(response: 0.4, dampingFraction: 1), value: isKeyboardVisible)
|
|
||||||
} else {
|
} else {
|
||||||
// Default SVG avatar
|
// Default SVG avatar with animated dashed border
|
||||||
SVGImage(svgName: "IP")
|
SVGImage(svgName: "IP")
|
||||||
.frame(width: isKeyboardVisible ? 125 : 225,
|
.frame(width: avatarSize, height: avatarSize)
|
||||||
height: isKeyboardVisible ? 125 : 225)
|
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 20)
|
RoundedRectangle(cornerRadius: 20)
|
||||||
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, dash: [12, 8]))
|
.stroke(style: StrokeStyle(
|
||||||
|
lineWidth: borderWidth,
|
||||||
|
lineCap: .round,
|
||||||
|
dash: [12, 8],
|
||||||
|
dashPhase: isAnimating ? 40 : 0
|
||||||
|
))
|
||||||
.foregroundColor(Color.themePrimary)
|
.foregroundColor(Color.themePrimary)
|
||||||
)
|
)
|
||||||
.animation(.spring(response: 0.4, dampingFraction: 1), value: isKeyboardVisible)
|
.onAppear {
|
||||||
|
withAnimation(Animation.linear(duration: 1.5).repeatForever(autoreverses: false)) {
|
||||||
|
isAnimating = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Upload indicator
|
||||||
if isUploading {
|
if isUploading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle())
|
.progressViewStyle(CircularProgressViewStyle(tint: .themePrimary))
|
||||||
.scaleEffect(1.5)
|
.scaleEffect(1.5)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color.black.opacity(0.3))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 20))
|
||||||
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(width: avatarSize, height: avatarSize)
|
||||||
|
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: isKeyboardVisible)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(ScaleButtonStyle())
|
||||||
|
|
||||||
|
// Upload Button (only shown when username is not shown)
|
||||||
if !showUsername {
|
if !showUsername {
|
||||||
// Upload button
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
withAnimation {
|
||||||
showMediaPicker = true
|
showMediaPicker = true
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Text("Upload from Gallery")
|
Text("Upload from Gallery")
|
||||||
.font(Typography.font(for: .subtitle, family: .inter))
|
.font(Typography.font(for: .subtitle, family: .inter))
|
||||||
@ -71,8 +103,8 @@ public struct AvatarPicker: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showMediaPicker) {
|
.sheet(isPresented: $showMediaPicker) {
|
||||||
MediaPicker(
|
MediaPicker(
|
||||||
@ -84,7 +116,9 @@ public struct AvatarPicker: View {
|
|||||||
onDismiss: {
|
onDismiss: {
|
||||||
showMediaPicker = false
|
showMediaPicker = false
|
||||||
if !uploadManager.selectedMedia.isEmpty {
|
if !uploadManager.selectedMedia.isEmpty {
|
||||||
|
withAnimation {
|
||||||
isUploading = true
|
isUploading = true
|
||||||
|
}
|
||||||
uploadManager.startUpload()
|
uploadManager.startUpload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,10 +128,40 @@ public struct AvatarPicker: View {
|
|||||||
if let firstMedia = uploadManager.selectedMedia.first,
|
if let firstMedia = uploadManager.selectedMedia.first,
|
||||||
case .image(let image) = firstMedia,
|
case .image(let image) = firstMedia,
|
||||||
uploadManager.isAllUploaded {
|
uploadManager.isAllUploaded {
|
||||||
|
withAnimation(.spring()) {
|
||||||
selectedImage = image
|
selectedImage = image
|
||||||
isUploading = false
|
isUploading = false
|
||||||
|
if let status = uploadManager.uploadStatus["0"],
|
||||||
|
case .completed(let fileId) = status {
|
||||||
|
uploadedFileId = fileId
|
||||||
|
}
|
||||||
uploadManager.clearAllMedia()
|
uploadManager.clearAllMedia()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button style for scale effect
|
||||||
|
private struct ScaleButtonStyle: ButtonStyle {
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: configuration.isPressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
struct AvatarPicker_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
AvatarPicker(
|
||||||
|
selectedImage: .constant(nil),
|
||||||
|
showUsername: .constant(false),
|
||||||
|
isKeyboardVisible: .constant(false),
|
||||||
|
uploadedFileId: .constant(nil)
|
||||||
|
)
|
||||||
|
.padding()
|
||||||
|
.background(Color.gray.opacity(0.1))
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -11,9 +11,19 @@ struct UserInfo: View {
|
|||||||
@State private var showLogoutAlert = false
|
@State private var showLogoutAlert = false
|
||||||
@State private var avatarImage: UIImage?
|
@State private var avatarImage: UIImage?
|
||||||
@State private var showUsername: Bool = false
|
@State private var showUsername: Bool = false
|
||||||
|
@State private var uploadedFileId: String? // Add state for file ID
|
||||||
|
@State private var errorMessage: String = ""
|
||||||
|
@State private var showError: Bool = false
|
||||||
|
|
||||||
// 添加一个状态来跟踪键盘是否显示
|
// 添加一个状态来跟踪键盘是否显示
|
||||||
@State private var isKeyboardVisible = false
|
@State private var isKeyboardVisible = false
|
||||||
|
@FocusState private var isTextFieldFocused: Bool
|
||||||
|
|
||||||
|
private let keyboardPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
|
||||||
|
.map { _ in true }
|
||||||
|
.merge(with: NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
|
||||||
|
.map { _ in false })
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@ -49,77 +59,54 @@ struct UserInfo: View {
|
|||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.background(Color.themeTextWhiteSecondary)
|
.background(Color.themeTextWhiteSecondary)
|
||||||
.zIndex(1) // 确保导航栏在最上层
|
.zIndex(1) // 确保导航栏在最上层
|
||||||
|
// Dynamic text that changes based on keyboard state
|
||||||
|
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)
|
||||||
|
.lineLimit(isKeyboardVisible ? 1 : 2)
|
||||||
|
.padding(6)
|
||||||
|
.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)
|
||||||
|
.animation(.easeInOut(duration: 0.3), value: isKeyboardVisible)
|
||||||
|
.transition(.opacity)
|
||||||
|
|
||||||
// 可滚动的内容区域
|
// 可滚动的内容区域
|
||||||
|
GeometryReader { geometry in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// 单行文本(键盘显示时)
|
// 顶部Spacer - 只在非键盘状态下生效
|
||||||
if isKeyboardVisible {
|
if !isKeyboardVisible {
|
||||||
HStack(spacing: 20) {
|
Spacer(minLength: 0)
|
||||||
Text("Choose a photo as your avatar, and we'll generate a video mystery box for you.")
|
.frame(height: geometry.size.height * 0.1) // 10% of available height
|
||||||
.font(Typography.font(for: .caption))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(6)
|
|
||||||
.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)
|
|
||||||
.transition(AnyTransition.opacity.combined(with: .move(edge: .top)))
|
|
||||||
.animation(.easeInOut(duration: 0.4), value: isKeyboardVisible)
|
|
||||||
}
|
|
||||||
// 两行文本(键盘隐藏时)
|
|
||||||
else {
|
|
||||||
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)
|
|
||||||
.lineLimit(2)
|
|
||||||
.padding(6)
|
|
||||||
.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)
|
|
||||||
.transition(AnyTransition.opacity.combined(with: .move(edge: .top)))
|
|
||||||
.animation(.easeInOut(duration: 0.25), value: isKeyboardVisible)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isKeyboardVisible { Spacer() }
|
// Content VStack
|
||||||
|
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Title
|
// Title
|
||||||
Text(showUsername ? "Add Your Avatar" : "What‘s Your Name?")
|
Text(showUsername ? "Add Your Avatar" : "What's Your Name?")
|
||||||
.font(Typography.font(for: .body, family: .quicksandBold))
|
.font(Typography.font(for: .body, family: .quicksandBold))
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
ZStack {
|
|
||||||
AvatarPicker(
|
AvatarPicker(
|
||||||
selectedImage: $avatarImage,
|
selectedImage: $avatarImage,
|
||||||
showUsername: $showUsername,
|
showUsername: $showUsername,
|
||||||
isKeyboardVisible: $isKeyboardVisible
|
isKeyboardVisible: $isKeyboardVisible,
|
||||||
|
uploadedFileId: $uploadedFileId
|
||||||
)
|
)
|
||||||
}
|
|
||||||
.padding(.top, isKeyboardVisible ? 0 : 20)
|
.padding(.top, isKeyboardVisible ? 0 : 20)
|
||||||
|
|
||||||
if !showUsername {
|
if !showUsername {
|
||||||
@ -150,6 +137,11 @@ struct UserInfo: View {
|
|||||||
RoundedRectangle(cornerRadius: 16)
|
RoundedRectangle(cornerRadius: 16)
|
||||||
.fill(Color.themePrimaryLight)
|
.fill(Color.themePrimaryLight)
|
||||||
)
|
)
|
||||||
|
.focused($isTextFieldFocused)
|
||||||
|
.submitLabel(.done)
|
||||||
|
.onSubmit {
|
||||||
|
isTextFieldFocused = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@ -157,24 +149,76 @@ struct UserInfo: View {
|
|||||||
.cornerRadius(20)
|
.cornerRadius(20)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
if !isKeyboardVisible { Spacer() }
|
// 底部Spacer - 只在非键盘状态下生效
|
||||||
|
if !isKeyboardVisible {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
.frame(height: geometry.size.height * 0.1) // 10% of available height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue Button
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
if showUsername {
|
||||||
|
let parameters: [String: Any] = [
|
||||||
|
"username": userName,
|
||||||
|
"avatar_file_id": uploadedFileId ?? ""
|
||||||
|
]
|
||||||
|
|
||||||
|
NetworkService.shared.postWithToken(
|
||||||
|
path: "/iam/user/info",
|
||||||
|
parameters: parameters
|
||||||
|
) { (result: Result<UserInfoResponse, NetworkError>) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
print("✅ 用户信息更新成功")
|
||||||
|
// Update local state with the new user info
|
||||||
|
if let userData = response.data {
|
||||||
|
self.userName = userData.username
|
||||||
|
// You can update other user data here if needed
|
||||||
|
}
|
||||||
|
// Show success message or navigate back
|
||||||
|
self.dismiss()
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
print("❌ 用户信息更新失败: \(error.localizedDescription)")
|
||||||
|
// Show error message to user
|
||||||
|
// You can use an @State variable to show an alert or toast
|
||||||
|
self.errorMessage = "更新失败: \(error.localizedDescription)"
|
||||||
|
self.showError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withAnimation {
|
||||||
showUsername = true
|
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)
|
||||||
.padding(16)
|
.padding()
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 25)
|
RoundedRectangle(cornerRadius: 25)
|
||||||
.fill(Color.themePrimary)
|
.fill(Color.themePrimary)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.horizontal)
|
||||||
.padding(.bottom, isKeyboardVisible ? 20 : 40)
|
.padding(.bottom, isKeyboardVisible ? 20 : 40)
|
||||||
|
.disabled(showUsername && userName.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||||
|
.opacity((showUsername && userName.trimmingCharacters(in: .whitespaces).isEmpty) ? 0.6 : 1.0)
|
||||||
|
.animation(.easeInOut, value: showUsername)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
// 底部安全区域占位
|
||||||
|
if isKeyboardVisible {
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
.frame(height: 20) // 添加一些底部间距当键盘显示时
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minHeight: geometry.size.height) // 确保内容至少填满可用高度
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color.themeTextWhiteSecondary)
|
.background(Color.themeTextWhiteSecondary)
|
||||||
@ -190,19 +234,49 @@ struct UserInfo: View {
|
|||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 错误提示
|
||||||
|
if showError {
|
||||||
|
VStack {
|
||||||
|
Text(errorMessage)
|
||||||
|
.font(Typography.font(for: .body))
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.padding()
|
||||||
|
.background(Color.white)
|
||||||
|
.cornerRadius(10)
|
||||||
|
.shadow(radius: 2)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
// Set up keyboard notifications
|
||||||
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { _ in
|
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { _ in
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
isKeyboardVisible = true
|
isKeyboardVisible = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
|
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { _ in
|
||||||
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
isKeyboardVisible = false
|
isKeyboardVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
|
// Clean up observers
|
||||||
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
.onReceive(keyboardPublisher) { isVisible in
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
isKeyboardVisible = isVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: isTextFieldFocused) { newValue in
|
||||||
|
withAnimation(.easeInOut(duration: 0.2)) {
|
||||||
|
isKeyboardVisible = newValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user