Files
e-party-iOS/yana/MVVM/CommonComponents.swift
edwinQQQ d97de8455a feat: 优化底部导航栏组件及初始化逻辑
- 在CommonComponents中为BottomTabBar添加了便捷初始化和最简初始化方法,简化了外部使用。
- 新增内部默认items方法,确保底部导航栏的图标资源一致性。
- 在MainPage中更新BottomTabBar的使用方式,直接传入viewModel,提升代码可读性和维护性。
2025-09-26 15:23:33 +08:00

431 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
// MARK: - App Image Source Enum
enum AppImageSource: Equatable {
case camera
case photoLibrary
}
// MARK: - Tab
public struct TabBarItem: Identifiable, Equatable {
public let id: String
public let title: String
public let systemIconName: String
public init(id: String, title: String, systemIconName: String) {
self.id = id
self.title = title
self.systemIconName = systemIconName
}
}
struct BottomTabBar: View {
let items: [TabBarItem]
@Binding var selectedId: String
let onSelect: (String) -> Void
var contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0)
var horizontalPadding: CGFloat = 0
// 便 tabs
init(
selectedId: Binding<String>,
onSelect: @escaping (String) -> Void,
contentPadding: EdgeInsets = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0),
horizontalPadding: CGFloat = 0
) {
self.items = BottomTabBar.defaultItems()
self._selectedId = selectedId
self.onSelect = onSelect
self.contentPadding = contentPadding
self.horizontalPadding = horizontalPadding
}
// viewModel
init(viewModel: MainViewModel) {
self.items = BottomTabBar.defaultItems()
self._selectedId = Binding(
get: { viewModel.selectedTab.rawValue },
set: { raw in
if let tab = MainViewModel.Tab(rawValue: raw) {
viewModel.onTabChanged(tab)
}
}
)
self.onSelect = { _ in } // 使
self.contentPadding = EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 0)
self.horizontalPadding = 0
}
// 使 BottomTabView.swift
private func assetIconName(for item: TabBarItem, isSelected: Bool) -> String? {
switch item.id {
case "feed":
return isSelected ? "feed selected" : "feed unselected"
case "me":
return isSelected ? "me selected" : "me unselected"
default:
return nil
}
}
// items
private static func defaultItems() -> [TabBarItem] {
return [
TabBarItem(id: "feed", title: "Feed", systemIconName: "list.bullet"),
TabBarItem(id: "me", title: "Me", systemIconName: "person.circle")
]
}
var body: some View {
HStack(spacing: 8) {
ForEach(items) { item in
Button(action: {
selectedId = item.id
onSelect(item.id)
}) {
Group {
if let name = assetIconName(for: item, isSelected: selectedId == item.id) {
Image(name)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 30, height: 30)
} else {
Image(systemName: item.systemIconName)
.font(.system(size: 24))
.foregroundColor(selectedId == item.id ? .white : .white.opacity(0.6))
}
}
}
.frame(maxWidth: .infinity)
.padding(contentPadding)
.contentShape(Rectangle())
}
}
.padding(.horizontal, 8) // 8
.padding(.horizontal, horizontalPadding)
.background(LiquidGlassBackground())
.clipShape(Capsule())
.contentShape(Capsule())
.onTapGesture { /* 穿 */ }
.overlay(
Capsule()
.stroke(Color.white.opacity(0.12), lineWidth: 0.5)
)
.shadow(color: Color.black.opacity(0.2), radius: 12, x: 0, y: 6)
.safeAreaInset(edge: .bottom) {
Color.clear.frame(height: 0)
}
}
}
// MARK: - Liquid Glass Background (iOS 26 )
struct LiquidGlassBackground: View {
var body: some View {
Group {
if #available(iOS 26.0, *) {
// iOS 26+使
Rectangle()
.fill(Color.clear)
.glassEffect()
} else
if #available(iOS 17.0, *) {
// iOS 17-25使 +
ZStack {
Rectangle().fill(.ultraThinMaterial)
LinearGradient(
colors: [Color.white.opacity(0.06), Color.clear, Color.white.opacity(0.04)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.blendMode(.softLight)
}
} else {
//
Rectangle()
.fill(Color.black.opacity(0.2))
}
}
}
}
// MARK: -
struct LoginBackgroundView: View {
var body: some View {
Color.blue
// Image("bg")
// .resizable()
// .aspectRatio(contentMode: .fill)
// .ignoresSafeArea(.all)
}
}
// MARK: -
struct LoginHeaderView: View {
let onBack: () -> Void
var body: some View {
HStack {
Button(action: onBack) {
Image(systemName: "chevron.left")
.font(.system(size: 24, weight: .medium))
.foregroundColor(.white)
.frame(width: 44, height: 44)
}
Spacer()
}
.padding(.horizontal, 16)
.padding(.top, 8)
}
}
// MARK: -
enum InputFieldType {
case text
case number
case password
case verificationCode
}
struct CustomInputField: View {
let type: InputFieldType
let placeholder: String
let text: Binding<String>
let isPasswordVisible: Binding<Bool>?
let onGetCode: (() -> Void)?
let isCodeButtonEnabled: Bool
let isCodeLoading: Bool
let getCodeButtonText: String
init(
type: InputFieldType,
placeholder: String,
text: Binding<String>,
isPasswordVisible: Binding<Bool>? = nil,
onGetCode: (() -> Void)? = nil,
isCodeButtonEnabled: Bool = false,
isCodeLoading: Bool = false,
getCodeButtonText: String = ""
) {
self.type = type
self.placeholder = placeholder
self.text = text
self.isPasswordVisible = isPasswordVisible
self.onGetCode = onGetCode
self.isCodeButtonEnabled = isCodeButtonEnabled
self.isCodeLoading = isCodeLoading
self.getCodeButtonText = getCodeButtonText
}
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25)
.fill(Color.white.opacity(0.1))
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.white.opacity(0.3), lineWidth: 1)
)
.frame(height: 56)
HStack {
//
Group {
switch type {
case .text, .number:
TextField("", text: text)
.placeholder(when: text.wrappedValue.isEmpty) {
Text(placeholder)
.foregroundColor(.white.opacity(0.6))
}
.keyboardType(type == .number ? .numberPad : .default)
case .password:
if let isPasswordVisible = isPasswordVisible {
if isPasswordVisible.wrappedValue {
TextField("", text: text)
.placeholder(when: text.wrappedValue.isEmpty) {
Text(placeholder)
.foregroundColor(.white.opacity(0.6))
}
} else {
SecureField("", text: text)
.placeholder(when: text.wrappedValue.isEmpty) {
Text(placeholder)
.foregroundColor(.white.opacity(0.6))
}
}
}
case .verificationCode:
TextField("", text: text)
.placeholder(when: text.wrappedValue.isEmpty) {
Text(placeholder)
.foregroundColor(.white.opacity(0.6))
}
.keyboardType(.numberPad)
}
}
.foregroundColor(.white)
.font(.system(size: 16))
//
if type == .password, let isPasswordVisible = isPasswordVisible {
Button(action: {
isPasswordVisible.wrappedValue.toggle()
}) {
Image(systemName: isPasswordVisible.wrappedValue ? "eye.slash" : "eye")
.foregroundColor(.white.opacity(0.7))
.font(.system(size: 18))
}
} else if type == .verificationCode, let onGetCode = onGetCode {
Button(action: onGetCode) {
ZStack {
if isCodeLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(0.7)
} else {
Text(getCodeButtonText)
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
}
}
.frame(width: 60, height: 36)
.background(
RoundedRectangle(cornerRadius: 15)
.fill(Color.white.opacity(isCodeButtonEnabled ? 0.2 : 0.1))
)
}
.disabled(!isCodeButtonEnabled || isCodeLoading)
}
}
.padding(.horizontal, 24)
}
}
}
// MARK: -
struct LoginButtonView: View {
let isLoading: Bool
let isEnabled: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
Group {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.2)
} else {
Text("Login")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.white)
}
}
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
.background(isEnabled ? Color(red: 0.5, green: 0.3, blue: 0.8) : Color.gray)
.cornerRadius(8)
.disabled(!isEnabled)
}
}
// MARK: -
struct SettingRow: View {
let title: String
let subtitle: String
let action: (() -> Void)?
var body: some View {
Button(action: {
action?()
}) {
HStack(spacing: 16) {
HStack {
Text(title)
.font(.system(size: 16))
.foregroundColor(.white)
.multilineTextAlignment(.leading)
Spacer()
if !subtitle.isEmpty {
Text(subtitle)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.7))
}
}
Spacer()
if action != nil {
Image(systemName: "chevron.right")
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.5))
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
.disabled(action == nil)
}
}
// MARK: - Camera Picker
struct CameraPicker: UIViewControllerRepresentable {
let onImagePicked: (UIImage?) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = .camera
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(onImagePicked: onImagePicked)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let onImagePicked: (UIImage?) -> Void
init(onImagePicked: @escaping (UIImage?) -> Void) {
self.onImagePicked = onImagePicked
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
onImagePicked(image)
} else {
onImagePicked(nil)
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
onImagePicked(nil)
picker.dismiss(animated: true)
}
}
}
#Preview {
VStack(spacing: 20) {
LoginBackgroundView()
LoginHeaderView(onBack: {})
CustomInputField(
type: .text,
placeholder: "Test Input",
text: .constant("")
)
LoginButtonView(
isLoading: false,
isEnabled: true,
onTap: {}
)
}
}