1. SwiftUI 文本输入基础与实战
在iOS应用开发中,文本输入是最基础也最常用的交互方式之一。SwiftUI作为苹果推出的声明式UI框架,提供了简单而强大的文本输入组件。TextField是SwiftUI中最基础的文本输入控件,它的基本用法看起来很简单:
swift复制@State private var username = ""
TextField("请输入用户名", text: $username)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
这段代码创建了一个带圆角边框的文本输入框,绑定到username状态变量。但实际开发中,我们需要考虑更多细节:
- 键盘类型适配:不同类型的输入需要不同的键盘
- 输入验证:实时校验输入内容的合法性
- 自动聚焦:页面加载后自动弹出键盘
- 输入限制:控制最大字符数等
1.1 键盘类型优化
SwiftUI通过keyboardType修饰符可以指定键盘类型:
swift复制TextField("请输入手机号", text: $phoneNumber)
.keyboardType(.phonePad)
常用键盘类型包括:
- .default:默认键盘
- .asciiCapable:ASCII字符键盘
- .numbersAndPunctuation:数字和标点
- .URL:URL专用键盘
- .numberPad:纯数字键盘
- .phonePad:电话键盘
- .emailAddress:电子邮件键盘
提示:在真机上测试键盘类型,模拟器可能无法完全模拟某些键盘的特殊行为。
1.2 输入验证与实时反馈
结合onChange修饰符可以实现输入验证:
swift复制@State private var username = ""
@State private var usernameValid = true
TextField("用户名", text: $username)
.onChange(of: username) { newValue in
usernameValid = newValue.count >= 4
}
.foregroundColor(usernameValid ? .primary : .red)
对于更复杂的验证,可以结合正则表达式:
swift复制func isValidEmail(_ email: String) -> Bool {
let regex = #"^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,64}$"#
return email.range(of: regex, options: .regularExpression) != nil
}
2. 安全密码输入的实现细节
SecureField是专门用于密码输入的组件,它会自动隐藏输入内容:
swift复制@State private var password = ""
SecureField("请输入密码", text: $password)
2.1 密码可见性切换
实际应用中,常需要提供"显示密码"的切换功能:
swift复制@State private var password = ""
@State private var showPassword = false
HStack {
if showPassword {
TextField("密码", text: $password)
} else {
SecureField("密码", text: $password)
}
Button(action: {
showPassword.toggle()
}) {
Image(systemName: showPassword ? "eye.slash" : "eye")
}
}
2.2 密码强度实时评估
实现密码强度指示器可以提升用户体验:
swift复制enum PasswordStrength: Int {
case weak = 1
case medium = 2
case strong = 3
}
func evaluatePassword(_ password: String) -> PasswordStrength {
var strength: Int = 0
// 长度检查
if password.count >= 8 { strength += 1 }
// 包含数字
if password.rangeOfCharacter(from: .decimalDigits) != nil { strength += 1 }
// 包含特殊字符
if password.rangeOfCharacter(from: CharacterSet(charactersIn: "!@#$%^&*()_+")) != nil { strength += 1 }
return PasswordStrength(rawValue: strength) ?? .weak
}
3. 多行文本输入的高级用法
TextEditor是SwiftUI中用于多行文本输入的组件:
swift复制@State private var bio = ""
TextEditor(text: $bio)
.frame(height: 200)
.border(Color.gray, width: 1)
3.1 自定义占位文本
TextEditor本身不支持placeholder,需要自己实现:
swift复制struct PlaceholderTextEditor: View {
@Binding var text: String
let placeholder: String
var body: some View {
ZStack(alignment: .topLeading) {
if text.isEmpty {
Text(placeholder)
.foregroundColor(Color(UIColor.placeholderText))
.padding(.vertical, 8)
.padding(.horizontal, 4)
}
TextEditor(text: $text)
}
}
}
3.2 动态高度调整
实现根据内容自动调整高度的TextEditor:
swift复制struct DynamicHeightTextEditor: View {
@Binding var text: String
@State private var height: CGFloat = 100
var body: some View {
TextEditor(text: $text)
.frame(height: height)
.background(GeometryReader { geometry in
Color.clear
.preference(key: HeightPreferenceKey.self,
value: geometry.size.height)
})
.onPreferenceChange(HeightPreferenceKey.self) { newHeight in
let lineHeight: CGFloat = 20 // 预估行高
let minHeight: CGFloat = 100
let maxHeight: CGFloat = 300
let calculatedHeight = min(max(newHeight, minHeight), maxHeight)
// 按行高取整
let lines = max(1, Int(calculatedHeight / lineHeight))
height = CGFloat(lines) * lineHeight
}
}
}
struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
4. 输入控制与格式化
4.1 输入长度限制
实现字符数限制并显示剩余字数:
swift复制@State private var tweet = ""
let maxTweetLength = 280
var body: some View {
VStack {
TextEditor(text: $tweet)
.onChange(of: tweet) { _ in
if tweet.count > maxTweetLength {
tweet = String(tweet.prefix(maxTweetLength))
}
}
HStack {
Spacer()
Text("\(maxTweetLength - tweet.count)")
.foregroundColor(tweet.count > maxTweetLength * 0.8 ? .red : .gray)
}
}
}
4.2 输入内容格式化
实现电话号码自动格式化:
swift复制@State private var phoneNumber = ""
func formatPhoneNumber(_ number: String) -> String {
let cleanNumber = number.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
let mask = "(XXX) XXX-XXXX"
var result = ""
var index = cleanNumber.startIndex
for ch in mask where index < cleanNumber.endIndex {
if ch == "X" {
result.append(cleanNumber[index])
index = cleanNumber.index(after: index)
} else {
result.append(ch)
}
}
return result
}
TextField("电话号码", text: $phoneNumber)
.onChange(of: phoneNumber) { newValue in
phoneNumber = formatPhoneNumber(newValue)
}
.keyboardType(.phonePad)
5. 输入辅助功能与无障碍支持
5.1 输入焦点管理
使用FocusState管理输入焦点:
swift复制enum Field {
case username
case password
}
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("用户名", text: $username)
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
SecureField("密码", text: $password)
.focused($focusedField, equals: .password)
.submitLabel(.done)
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
focusedField = .username
}
}
}
5.2 无障碍支持
为输入组件添加无障碍标识:
swift复制TextField("用户名", text: $username)
.accessibilityLabel("用户名输入框")
.accessibilityHint("请输入您的用户名,长度4-20个字符")
.accessibilityValue(username.isEmpty ? "空" : "已输入\(username.count)个字符")
对于密码输入,可以特别标注:
swift复制SecureField("密码", text: $password)
.accessibilityLabel("密码输入框")
.accessibilityHint("安全密码输入区域,内容不可见")
.accessibilityAddTraits(.isSecureTextField)
6. 输入样式自定义与主题适配
6.1 自定义输入框样式
创建可复用的自定义样式:
swift复制struct CustomTextFieldStyle: TextFieldStyle {
func _body(configuration: TextField<_Label>) -> some View {
configuration
.padding(10)
.background(
RoundedRectangle(cornerRadius: 8)
.strokeBorder(Color.blue, lineWidth: 1)
)
.overlay(
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.1))
)
}
}
// 使用方式
TextField("搜索...", text: $searchText)
.textFieldStyle(CustomTextFieldStyle())
6.2 暗黑模式适配
确保输入组件在暗黑模式下表现良好:
swift复制TextField("邮箱", text: $email)
.foregroundColor(.primary)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color(.systemBackground))
.shadow(color: .primary.opacity(0.1), radius: 2, x: 0, y: 1)
)
.colorScheme(.dark) // 可以强制测试暗黑模式
7. 输入性能优化与高级技巧
7.1 大量输入的性能优化
当处理大量文本输入时,需要考虑性能:
swift复制@StateObject private var textModel = LargeTextModel()
struct LargeTextView: View {
var body: some View {
TextEditor(text: $textModel.text)
.onReceive(textModel.$text.throttle(for: 0.5, scheduler: RunLoop.main, latest: true)) {
// 处理文本变化,如自动保存
saveToDatabase(text: $0)
}
}
}
class LargeTextModel: ObservableObject {
@Published var text = "" {
didSet {
// 只在文本变化较大时更新
if abs(text.count - oldValue.count) > 10 {
objectWillChange.send()
}
}
}
}
7.2 输入历史与撤销功能
实现简单的输入历史记录:
swift复制class TextHistory: ObservableObject {
@Published var currentText = ""
private var history: [String] = []
private var historyIndex = 0
func commitChange() {
history.append(currentText)
historyIndex = history.count - 1
}
func undo() {
guard historyIndex > 0 else { return }
historyIndex -= 1
currentText = history[historyIndex]
}
func redo() {
guard historyIndex < history.count - 1 else { return }
historyIndex += 1
currentText = history[historyIndex]
}
}
8. 输入测试与调试技巧
8.1 输入组件预览技巧
创建包含各种状态的预览:
swift复制struct TextField_Previews: PreviewProvider {
static var previews: some View {
Group {
// 正常状态
TextField("用户名", text: .constant(""))
// 输入状态
TextField("用户名", text: .constant("user123"))
.previewDisplayName("有输入")
// 错误状态
TextField("用户名", text: .constant("invalid"))
.foregroundColor(.red)
.previewDisplayName("错误状态")
// 暗黑模式
TextField("用户名", text: .constant(""))
.preferredColorScheme(.dark)
}
}
}
8.2 输入自动化测试
为输入组件编写UI测试:
swift复制func testLoginForm() throws {
let app = XCUIApplication()
app.launch()
let usernameField = app.textFields["username"]
XCTAssertTrue(usernameField.exists)
usernameField.tap()
usernameField.typeText("testuser")
let passwordField = app.secureTextFields["password"]
passwordField.tap()
passwordField.typeText("password123")
app.buttons["login"].tap()
// 验证登录结果
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 1))
}
9. 实际应用案例:完整登录表单实现
结合所有知识点,实现一个完整的登录表单:
swift复制struct LoginForm: View {
enum Field: Hashable {
case email, password
}
@State private var email = ""
@State private var password = ""
@State private var showPassword = false
@State private var rememberMe = false
@State private var isLoading = false
@State private var errorMessage: String?
@FocusState private var focusedField: Field?
var body: some View {
Form {
Section {
TextField("电子邮箱", text: $email)
.keyboardType(.emailAddress)
.textContentType(.emailAddress)
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .email)
.submitLabel(.next)
HStack {
if showPassword {
TextField("密码", text: $password)
} else {
SecureField("密码", text: $password)
}
Button {
showPassword.toggle()
} label: {
Image(systemName: showPassword ? "eye.slash" : "eye")
}
}
.focused($focusedField, equals: .password)
.submitLabel(.done)
Toggle("记住我", isOn: $rememberMe)
}
if let error = errorMessage {
Section {
Text(error)
.foregroundColor(.red)
}
}
Section {
Button(action: login) {
HStack {
Spacer()
if isLoading {
ProgressView()
} else {
Text("登录")
}
Spacer()
}
}
.disabled(email.isEmpty || password.isEmpty || isLoading)
}
}
.onSubmit {
switch focusedField {
case .email:
focusedField = .password
default:
login()
}
}
.navigationTitle("登录")
}
func login() {
isLoading = true
errorMessage = nil
// 模拟网络请求
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
isLoading = false
if password.count < 6 {
errorMessage = "密码长度至少6位"
} else if !email.contains("@") {
errorMessage = "请输入有效的电子邮箱"
} else {
// 登录成功处理
}
}
}
}
10. 输入国际化与本地化支持
10.1 多语言输入处理
swift复制TextField(LocalizedStringKey("username.placeholder"), text: $username)
.disableAutocorrection(true)
在Localizable.strings文件中定义:
code复制"username.placeholder" = "请输入用户名";
10.2 输入方向适配
处理从右到左(RTL)语言:
swift复制TextField("اسم المستخدم", text: $username)
.environment(\.layoutDirection, .rightToLeft)
.multilineTextAlignment(.trailing)
11. 输入安全最佳实践
11.1 敏感数据处理
安全处理内存中的密码:
swift复制final class SecureString: ObservableObject {
private(set) var value: String
init(_ value: String) {
self.value = value
}
func update(_ newValue: String) {
// 先清空原有内存
value.withMutableCharacters { buffer in
buffer.replaceSubrange(buffer.startIndex..<buffer.endIndex, with: repeatElement(" ", count: buffer.count))
}
value = newValue
}
deinit {
// 对象销毁时清空内存
update("")
}
}
11.2 自动填充与密码管理
支持密码管理工具:
swift复制TextField("用户名", text: $username)
.textContentType(.username)
SecureField("密码", text: $password)
.textContentType(.password)
12. 自定义输入视图进阶
12.1 创建标签输入框
实现类似邮件客户件的标签输入:
swift复制struct TagsInputView: View {
@State private var tags: [String] = []
@State private var currentTag = ""
var body: some View {
VStack(alignment: .leading) {
HStack {
ForEach(tags, id: \.self) { tag in
TagView(text: tag) {
tags.removeAll { $0 == tag }
}
}
TextField("添加标签", text: $currentTag, onCommit: addTag)
.autocapitalization(.none)
.disableAutocorrection(true)
.frame(minWidth: 100)
}
}
}
private func addTag() {
let tag = currentTag.trimmingCharacters(in: .whitespaces)
guard !tag.isEmpty, !tags.contains(tag) else { return }
tags.append(tag)
currentTag = ""
}
}
struct TagView: View {
let text: String
let onDelete: () -> Void
var body: some View {
HStack(spacing: 4) {
Text(text)
.font(.caption)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.2))
.cornerRadius(4)
Button(action: onDelete) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.gray)
}
}
}
}
12.2 富文本输入实现
基础富文本编辑器实现:
swift复制struct RichTextEditor: UIViewRepresentable {
@Binding var text: NSAttributedString
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.autocapitalizationType = .sentences
textView.isSelectable = true
textView.isUserInteractionEnabled = true
textView.backgroundColor = .systemBackground
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = text
}
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
class Coordinator: NSObject, UITextViewDelegate {
var text: Binding<NSAttributedString>
init(_ text: Binding<NSAttributedString>) {
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
self.text.wrappedValue = textView.attributedText ?? NSAttributedString()
}
}
}