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

SwiftUI与UIKit混合开发:List拖动数据UI同步异常及日期错位

问题描述

实现了一个包含可拖动单元格的SwiftUI List,每个单元格包含自定义的PickerTextField(基于UIKit的UIDatePicker封装)。拖动单元格调整顺序后,编辑日期时发现修改的是拖动前对应位置的数据,而非当前选中的单元格数据,导致数组options与List显示顺序不同步,编辑操作错位。

现有代码

MyView 实现

struct MyView: View {
    @State var options: [DatePollOption] = [DatePollOption(date: ""),DatePollOption(date: "")]
    var body: some View{
        List{
            Section{
                ForEach($options) { $option in
                    HStack{
                        PickerTextField(
                            placeholder: "GG", date: $option.date)
                    }
                }
                .onMove(perform: move)
            }
        }
    }
    
    func move(from source: IndexSet, to destination: Int) {
        options.move(fromOffsets: source, toOffset: destination)
    }
}

struct DatePollOption : Identifiable {
    var id: String = UUID().uuidString
    var date : String?
}

PickerTextField 实现

class Helper{
    public var dateChanged : (()-> Void)?
    public var doneButtonTapped: (()-> Void)?
    
    @objc func dateValueChanged(){
        self.dateChanged?()
    }
}

struct PickerTextField : UIViewRepresentable{
    var textField = UITextField()
    @State var datePickerView = UIDatePicker()
    let toolbar = UIToolbar()
    let helper = Helper()
    
    public var placeholder: String
    @Binding public var date: String?
    
    @Binding var dates : [DatePollOption]
    var dateFormatterDate : DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyy"
        return dateFormatter
    }()
    
    func makeUIView(context: Context) -> UITextField {
        self.datePickerView.preferredDatePickerStyle = .wheels
        self.datePickerView.addTarget(self.helper, action: #selector(self.helper.dateValueChanged), for: .valueChanged)
        
        self.textField.placeholder = self.placeholder
        self.textField.inputView = self.datePickerView
        textField.delegate = context.coordinator
        
        self.textField.inputAccessoryView = toolbar
        
        self.helper.dateChanged = {
            date = dateFormatterDate.string(from: self.datePickerView.date)
        }
        
        return self.textField
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(datePicker: $datePickerView, date: $date, textField: textField, toolBar: toolbar)
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        if let selectedDate = self.date {
            uiView.text = selectedDate
        }
    }
    
    class Coordinator: NSObject,UITextFieldDelegate,UIToolbarDelegate{
        @Binding var datePicker : UIDatePicker
        var textField : UITextField
        @Binding public var date: String?
        let toolBar : UIToolbar
        var dateFormatter : DateFormatter = {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "HH:mm"
            return dateFormatter
        }()
        let helper = Helper()
        
        init(datePicker: Binding<UIDatePicker>,date: Binding<String?>,textField: UITextField,toolBar: UIToolbar) {
            self._datePicker = datePicker
            self._date = date
            self.textField = textField
            self.toolBar = toolBar
            super.init()
            
            let doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(doneButtonaction))
            toolBar.setItems([doneButton], animated: false)
        }
        
        @objc func doneButtonaction(){
            if self.date == nil {
                self.date = dateFormatter.string(from: self.datePicker.date)
                self.textField.resignFirstResponder()
            }
            self.textField.resignFirstResponder()
        }
    }
}

问题原因分析

核心问题出在PickerTextField的实现违反了UIViewRepresentable的设计规范:

  • 结构体存储UIKit视图实例:将textField、datePickerView直接作为结构体属性,而非在makeUIView内创建,SwiftUI复用结构体实例时会导致旧视图与新绑定关联错位。
  • 用@State存储UIKit视图:datePickerView使用@State修饰,该状态属于视图内部状态,单元格拖动复用后无法与新绑定同步,导致编辑操作指向旧数据。
  • 冗余绑定参数:dates绑定未被使用,增加代码复杂度。
  • 日期格式不一致:dateChanged与doneButtonaction使用不同的日期格式,可能导致显示与编辑逻辑冲突。

解决方案

修改PickerTextField实现,遵循UIViewRepresentable的正确用法,确保视图与绑定的同步性:

修正后的PickerTextField代码

class Helper{
    public var dateChanged : ((Date)-> Void)?
    public var doneButtonTapped: (()-> Void)?
    
    @objc func dateValueChanged(_ sender: UIDatePicker){
        self.dateChanged?(sender.date)
    }
}

struct PickerTextField : UIViewRepresentable{
    public var placeholder: String
    @Binding public var date: String?
    private let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "dd MMM yyyy"
        return formatter
    }()
    private let timeFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter
    }()
    
    func makeUIView(context: Context) -> UITextField {
        // 在makeUIView内创建所有UIKit视图,避免复用问题
        let textField = UITextField()
        textField.placeholder = placeholder
        
        let datePickerView = UIDatePicker()
        datePickerView.preferredDatePickerStyle = .wheels
        datePickerView.addTarget(context.coordinator.helper, action: #selector(context.coordinator.helper.dateValueChanged(_:)), for: .valueChanged)
        textField.inputView = datePickerView
        
        // 创建工具栏
        let toolbar = UIToolbar()
        toolbar.sizeToFit()
        let doneButton = UIBarButtonItem(title: "Done", style: .done, target: context.coordinator, action: #selector(context.coordinator.doneButtonaction))
        toolbar.setItems([doneButton], animated: false)
        textField.inputAccessoryView = toolbar
        
        textField.delegate = context.coordinator
        return textField
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(date: $date, dateFormatter: dateFormatter, timeFormatter: timeFormatter)
    }
    
    func updateUIView(_ uiView: UITextField, context: Context) {
        // 更新TextField内容,同步DatePicker选中日期
        uiView.text = date
        if let dateStr = date, let selectedDate = dateFormatter.date(from: dateStr) ?? timeFormatter.date(from: dateStr) {
            (uiView.inputView as? UIDatePicker)?.date = selectedDate
        }
    }
    
    class Coordinator: NSObject, UITextFieldDelegate{
        @Binding var date: String?
        private let dateFormatter: DateFormatter
        private let timeFormatter: DateFormatter
        let helper = Helper()
        
        init(date: Binding<String?>, dateFormatter: DateFormatter, timeFormatter: DateFormatter) {
            self._date = date
            self.dateFormatter = dateFormatter
            self.timeFormatter = timeFormatter
            super.init()
            
            helper.dateChanged = { [weak self] selectedDate in
                guard let self = self else { return }
                self.date = self.dateFormatter.string(from: selectedDate)
            }
        }
        
        @objc func doneButtonaction(){
            guard let datePicker = UIApplication.shared.keyWindow?.rootViewController?.view.subviews.first(where: { $0 is UIDatePicker }) as? UIDatePicker else { return }
            if date == nil {
                date = timeFormatter.string(from: datePicker.date)
            }
            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
        }
        
        // 点击TextField时同步DatePicker日期
        func textFieldDidBeginEditing(_ textField: UITextField) {
            if let dateStr = date, let selectedDate = dateFormatter.date(from: dateStr) ?? timeFormatter.date(from: dateStr) {
                (textField.inputView as? UIDatePicker)?.date = selectedDate
            }
        }
    }
}

关键修改点说明

  1. 不在结构体存储UIKit视图:所有UIKit视图均在makeUIView内创建,确保每个PickerTextField对应独立的视图实例,避免复用导致的绑定错位。
  2. 移除@State修饰的UIKit视图:删除@State var datePickerView,通过Coordinator处理回调,保证绑定与视图状态同步。
  3. 优化回调逻辑:Helper的dateChanged改为接收Date参数,通过Coordinator持有绑定,确保数据更新指向正确的数组元素。
  4. 同步视图与绑定数据:在updateUIView和textFieldDidBeginEditing中,根据绑定的日期字符串同步DatePicker的选中状态,保证编辑时显示正确的日期。
  5. 移除冗余参数:删除未使用的dates绑定,简化代码结构。
  6. 统一日期格式处理:将格式器传入Coordinator,避免重复创建,同时兼容两种日期格式的解析。

验证效果

修改后,拖动单元格调整顺序后,编辑任意单元格的日期都会正确修改当前显示位置对应的options数组元素,数据与UI显示顺序完全同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 00:35:15