You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在UIKit中使用Combine实现表单验证与提交的问题

UIKit表单验证与Combine API调用问题解决方案

问题根源分析

  1. collect()导致无响应:你用的CombineLatest4基于@Published实现,这类publisher是无限期发布的(只要字段变化就会发送新值),而collect()需要上游publisher发送finished事件才会输出结果,因此永远等不到响应。
  2. 移除collect()后重复触发:去掉collect()后,每次调用isValidForm给@Published字段赋值时,CombineLatest4都会发布新的验证结果,加上@Published本身的特性,会多次触发sink回调,进而导致重复API调用。

重构验证逻辑

先调整ViewModel设计,把「一次性按钮点击验证」和「实时字段验证」分开,按钮点击场景用同步验证更直接:

修改ViewModel代码

@Published public var firstName = ""
@Published public var lastName = ""
@Published public var phoneNumber = ""
@Published public var emailAddress = ""    

// 实时验证单个字段的Publisher(用于输入时即时提示用户,比如输入框标红)
var isFirstNameValidPublisher: AnyPublisher<Bool, Never> {
    $firstName
        .map { $0.count >= 3 }
        .eraseToAnyPublisher()
}

var isLastNameValidPublisher: AnyPublisher<Bool, Never> {
    $lastName
        .map { $0.count >= 3 }
        .eraseToAnyPublisher()
}

var isUserEmailValidPublisher: AnyPublisher<Bool, Never> {
    $emailAddress
        .map { email in
            let emailPredicate = NSPredicate(format:"SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}")
            return emailPredicate.evaluate(with: email)
        }
        .eraseToAnyPublisher()
}

var isPhoneNumberValidPublisher: AnyPublisher<Bool, Never> {
    $phoneNumber
        .map { $0.count >= 8 }
        .eraseToAnyPublisher()
}

// 按钮点击时用的一次性验证方法(直接返回Bool,无需Publisher)
public func isFormValid(firstName: String, lastName: String, phoneNum: String, emailAddress: String, isConsent: Bool) -> Bool {
    let firstNameValid = firstName.count >= 3
    let lastNameValid = lastName.count >= 3
    let phoneValid = phoneNum.count >= 8
    let emailValid = NSPredicate(format:"SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}").evaluate(with: emailAddress)
    return firstNameValid && lastNameValid && phoneValid && emailValid && isConsent
}

// 可选:实时监听整体表单是否可用(用于自动启用/禁用提交按钮)
var isFormValidOverallPublisher: AnyPublisher<Bool, Never> {
    Publishers.CombineLatest4(isFirstNameValidPublisher, isLastNameValidPublisher, isUserEmailValidPublisher, isPhoneNumberValidPublisher)
        .map { $0 && $1 && $2 && $3 }
        .eraseToAnyPublisher()
}

按钮点击事件处理(ViewController)

点击按钮时先做一次性验证,通过后调用API,同时管理AnyCancellable避免重复提交:

修改ViewController代码

// 类内定义全局变量,用于管理API请求的生命周期
private var apiCancellable: AnyCancellable?

// 提交按钮点击事件
@IBAction func submitButtonTapped(_ sender: UIButton) {
    // 1. 获取表单输入值
    guard let firstName = firstNameTextView.text?.trimmingCharacters(in: .whitespaces),
          let lastName = lastNameTextView.text?.trimmingCharacters(in: .whitespaces),
          let phoneNum = phoneNumberTextView.text?.trimmingCharacters(in: .whitespaces),
          let email = emailTextView.text?.trimmingCharacters(in: .whitespaces),
          !firstName.isEmpty, !lastName.isEmpty, !phoneNum.isEmpty, !email.isEmpty else {
        showAlert(message: "请填写所有必填字段")
        return
    }
    
    // 2. 执行表单验证
    let isConsent = true // 根据你的同意开关状态修改
    guard dependencies.leadConsumptionUseCase.isFormValid(firstName: firstName, lastName: lastName, phoneNum: phoneNum, emailAddress: email, isConsent: isConsent) else {
        showAlert(message: "表单填写不符合要求,请检查:姓名至少3位、手机号至少8位、邮箱格式正确")
        return
    }
    
    // 3. 取消之前未完成的请求,避免重复提交
    apiCancellable?.cancel()
    
    // 4. 调用封装好的API
    apiCancellable = dependencies.leadConsumptionUseCase.submitForm(firstName: firstName, lastName: lastName, phoneNum: phoneNum, email: email)
        .receive(on: DispatchQueue.main) // 切回主线程更新UI
        .sink(receiveCompletion: { [weak self] completion in
            guard let self = self else { return }
            switch completion {
            case .finished: break
            case .failure(let error):
                self.showAlert(message: "提交失败:\(error.localizedDescription)")
            }
        }, receiveValue: { [weak self] response in
            guard let self = self else { return }
            // 处理API响应,更新界面
            self.showAlert(message: "提交成功!")
            // 清空表单
            self.firstNameTextView.text = ""
            self.lastNameTextView.text = ""
            self.phoneNumberTextView.text = ""
            self.emailTextView.text = ""
        })
}

// 辅助方法:显示提示弹窗
private func showAlert(message: String) {
    let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "确定", style: .default))
    present(alert, animated: true)
}

关键注意事项

  • 避免重复请求:每次点击按钮先取消apiCancellable,确保同一时间只有一个API请求在执行。
  • 主线程更新UI:API调用通常在后台线程执行,必须用receive(on: DispatchQueue.main)切回主线程操作界面,避免崩溃。
  • 职责分离:一次性验证用于按钮点击场景,实时验证用于输入过程中的即时反馈,逻辑更清晰。

内容的提问来源于stack exchange,提问作者pankaj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 12:45:44