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
}
}
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.