swiftuimemory-managementmemory-leaksbindinguiviewrepresentable

Why does injections binding into UIViewRepresentable causes memory leak?


I've just started learning SwiftUI and I use Date Picker via UIViewRepresentable and I'm injection binding into it because I want to update my date property in View Model. When I open memory graph I see there is few instances of that view model so it causes memory leak. But when I comment Binding property in Date picker there is no memory leak. Does anyone know how to inject binding without memory issues? Here is the code:

Date Picker class (UIViewRepresentable)

struct DatePickerTextField: UIViewRepresentable {
    private let textField = BaseTextField()
    private let datePicker = UIDatePicker()
    private let helper = Helper()
    public var typeOfDatePicker: TypeOfDatePicker
    public var placeholder: String
    @Binding public var date: String
    @Binding var dateLimit: String

func setDate() {
    let dateVar = DateHelper.getFullDate.string(from: datePicker.date)
    
    switch typeOfDatePicker {
    case .startDate:
        if let endDate = DateHelper.createDateFromString(dateLimit) {
            if datePicker.date <= endDate {
                date = dateVar
            }
        } else {
            date = dateVar
        }
    case .endDate:
        if let startDate = DateHelper.createDateFromString(dateLimit) {
            if datePicker.date >= startDate {
                date = dateVar
            }
        } else {
            date = dateVar
        }
    }
    
    setDatePickerLimits()
}

func setDatePickerLimits() {
    switch typeOfDatePicker {
    case .startDate:
        datePicker.minimumDate = Calendar.current.date(byAdding: .year, value: -1, to: Date())
        if dateLimit != "" {
            datePicker.maximumDate = DateHelper.createDateFromString(dateLimit)
        } else {
            datePicker.maximumDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())
        }
    case .endDate:
        if dateLimit != "" {
            datePicker.minimumDate = DateHelper.createDateFromString(dateLimit)
        } else {
            datePicker.minimumDate = Calendar.current.date(byAdding: .year, value: -1, to: Date())
        }
        datePicker.maximumDate = Calendar.current.date(byAdding: .year, value: 1, to: Date())
    }
}

func makeUIView(context: Context) -> UITextField {
    
    setDatePickerLimits()
    datePicker.locale = Locale(identifier: L10n.calendarLocaleIdentifier)
    datePicker.datePickerMode = .date
    datePicker.preferredDatePickerStyle = .wheels
    datePicker.addTarget(self.helper, action: #selector(self.helper.dateValueChanged), for: .valueChanged)
    
    textField.placeholder = placeholder
    textField.backgroundColor = Asset.backgroundTextFiledViewColor.color
    textField.layer.cornerRadius = 10
    textField.inputView = datePicker
    
    let toolbar = UIToolbar()
    toolbar.sizeToFit()
    let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    let doneButton = UIBarButtonItem(title: L10n.titleChoose, style: .plain, target: helper, action: #selector(helper.doneButtonTapped))
    let cancelButton = UIBarButtonItem(title: L10n.buttonTitleCancel, style: .plain, target: helper, action: #selector(helper.cancelButtonTapped))
    
    toolbar.setItems([cancelButton, flexibleSpace, doneButton], animated: true)
    textField.inputAccessoryView = toolbar
    
    helper.onDateValueChanged = {
        setDate()
    }
 
    helper.onDoneButtonTapped = {
        setDate()
        textField.resignFirstResponder()
    }
    
    helper.onCancelButtonTapped = {
        textField.resignFirstResponder()
    }
    
    return textField
}

func updateUIView(_ uiView: UITextField, context: Context) {

        uiView.text = date

}

func makeCoordinator() -> Coordinator {
    Coordinator()
}

class Helper {
    public var onDateValueChanged: (() -> Void)?
    public var onDoneButtonTapped: (() -> Void)?
    public var onCancelButtonTapped: (() -> Void)?
    
    @objc func dateValueChanged() {
        onDateValueChanged?()
        
    }
    
    @objc func doneButtonTapped() {
        onDoneButtonTapped?()
    }
    
    @objc func cancelButtonTapped() {
        onCancelButtonTapped?()
    }
}

class Coordinator {}

}

And here is its usage in View:

DatePickerTextField(typeOfDatePicker: .startDate,
 placeholder: L10n.textAvailableFrom, date: $createNewOffersViewModel.offersDTO.seasonStart,
 dateLimit: $createNewOffersViewModel.offersDTO.seasonEnd)
                            .frame(height: 50, alignment: .center)
                            .onChange(of: $createNewOffersViewModel.offersDTO.seasonStart.wrappedValue) { newValue in
                                if !newValue.isEmpty {
                                    self.createNewOffersViewModel.createOfferValidation[CreateNewOfferValidationEnum.seasonStart] = true
                                }
                            }

Solution

  • Your DatePickerTextField is a struct and it is init and thrown away every time SwiftUI updates. Thus you have to be careful not to init any objects when the struct inits because thats a major memory leak and performance hit. Looks to me like you are initing UIDatePicker objects and a few other things in the struct. You need to move these object inits into local vars inside makeUIView so they only happen once.

    Then use updateUIView to copy all the struct's new values into the UIView object. updateUIView is called when any lets changed from the last time this struct was init, or if any @Binding var changes.

    As long as DatePickerTextField stays in the same place in the hierarchy it will find the correct UIView object.