Why does injections binding into UIViewRepresentable causes memory leak? - swiftui

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.

Related

Setting up .onCommit{} in UITextFieldViewRepresentable SwiftUI

I bridge UIKit with SwiftUI as follows:
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var language: String
#Binding var text: String
init(language: Binding<String>, text: Binding<String>) {
self._language = language
self._text = text
}
func makeUIView(context: Context) -> UITextField {
let textField = getTextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
// Change the language in the wordTextField here.
if let wordTextField = uiView as? WordTextField {
wordTextField.language = self.language
}
}
private func getTextField() -> UITextField {
let textField = WordTextField(frame: .zero)
textField.language = self.language
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
class WordTextField: UITextField {
var language: String? {
didSet {
if self.isFirstResponder{
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
override var textInputMode: UITextInputMode? {
if let language = self.language {
print("text input mode: \(language)")
for inputMode in UITextInputMode.activeInputModes {
if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
return inputMode
}
}
}
return super.textInputMode
}
}
}
And call it as follows:
UITextFieldViewRepresentable(language: $keyboardLanguage, text: $foreignThing)
This works fine in some parts of my app. In other parts, I need a text field which calls a method when the user taps the enter key after entering text. It's written like this:
TextField("", text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
I tried implementing the above with UITextFieldViewRepresenatable as follows:
UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0
self.enableHint()
}), onCommit: {
if self.userAnswer.isEmpty {
answerPlaceholder = NSMutableAttributedString(string: "Tap here to answer...")
} else {
answerDisabled = true
checkAnswer()
}
})
I'm getting 'compiler couldn't type check this expression in reasonable time' error. I think I've narrowed it down to not implementing .onCommit:{} in my UITextFieldViewRepresentable()
If this is the problem, then I'd like to know how .onCommit:{} can be implemented in UITextFieldViewRepresentable().
There are a few mistakes in the UIViewRepresentable implementation:
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
This text binding will be out of date when the View is recreated, so change it to:
func makeCoordinator() -> Coordinator {
return Coordinator()
}
You can store the textField inside the coordinator, make it a lazy, set delegate self, then return it from the make func, eg..
func makeUIView(context: Context) -> UITextField {
context.coordinator.textField // lazy property that sets delegate self.
}
In update, you need to make use of the new value of the binding, e.g.
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
// Change the language in the wordTextField here.
if let wordTextField = uiView as? WordTextField {
wordTextField.language = self.language
}
// use the new binding
context.coordinator.textDidChange = { newText in
text = newText
}
}
class Coordinator: NSObject, UITextFieldDelegate {
lazy var textField: UITextField = {
let textField = UITextField()
textField.delegate = self
return textField
}()
var textDidChange: ((String) -> Void)?
func textFieldDidChangeSelection(_ textField: UITextField) {
textDidChange?(textField.text)
}
}
You'll see something similar in PaymentButton.swift in Apple's Fruta sample.
Perhaps a simpler way would be this (but I've not tested it yet):
// update
context.coordinator.textBinding = _text
// Coordinator
var textBinding: Binding<String>?
// textViewDidChange
textBinding?.wrappedValue = textField.text

How do I make a UITextView inside of a UIViewRepresentable update when I add an attribute to an NSMutableAttributedString?

I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable. I am primarily using SwiftUI but am using UIKit here as it seems SwiftUI does not currently support the functionality needed.
My problem is, when I set the NSMutableAttributedString to be already containing a string with attributes, if I then select that text in the UIViewRepresentable before typing any new text and press the underline button in the UIToolBar to add the attribute, the attribute is added to the NSMutableAttributedString but the UIView does not update to show the updated NSMutableAttributedString. However, if I type a single character and then select the text and add the underline attribute, the UIView updates.
Could someone explain why this is and maybe point me towards a solution? Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the string before typing anything new",
attributes: [.foregroundColor: UIColor.blue])
var body: some View {
EditorExample(outerMutableString: $mutableAttributedString)
}
}
struct EditorExample: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
#State private var outerSelectedRange: NSRange = NSRange()
func makeUIView(context: Context) -> some UITextView {
// make UITextView
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = context.coordinator
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: context.coordinator,
action: #selector(context.coordinator.underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
uiView.attributedText = outerMutableString
}
func makeCoordinator() -> Coordinator {
Coordinator(innerMutableString: $outerMutableString, selectedRange: $outerSelectedRange)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var innerMutableString: NSMutableAttributedString
#Binding var selectedRange: NSRange
init(innerMutableString: Binding<NSMutableAttributedString>, selectedRange: Binding<NSRange>) {
self._innerMutableString = innerMutableString
self._selectedRange = selectedRange
}
func textViewDidChange(_ textView: UITextView) {
innerMutableString = textView.textStorage
}
func textViewDidChangeSelection(_ textView: UITextView) {
selectedRange = textView.selectedRange
}
#objc func underline() {
if (selectedRange.length > 0) {
innerMutableString.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: selectedRange)
}
}
}
}
It's not working because NSAttributedString is a class and #State is for value types like structs. This means the dependency tracking is broken and things won't update correctly.
Also your UIViewRepresentable and Coordinator design is non-standard so I thought I would share an example of the correct way to do it. The binding is change to a string, which is a value type so it's working (minus the underline feature obviously).
struct ContentView: View {
//#State private var mutableAttributedString: NSMutableAttributedString = NSMutableAttributedString(
// string: "this is the string before typing anything new",
// attributes: [.foregroundColor: UIColor.blue])
#State var string = "this is the string before typing anything new"
var body: some View {
VStack {
// EditorExample(outerMutableString: $mutableAttributedString)
// EditorExample(outerMutableString: $mutableAttributedString) // a second to test bindings are working\
//Text(mutableAttributedString.string)
EditorExample(outerMutableString2: $string)
EditorExample(outerMutableString2: $string)
}
}
}
struct EditorExample: UIViewRepresentable {
//#Binding var outerMutableString: NSMutableAttributedString
#Binding var outerMutableString2: String
// this is called first
func makeCoordinator() -> Coordinator {
// we can't pass in any values to the Coordinator because they will be out of date when update is called the second time.
Coordinator()
}
// this is called second
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
// this is called third and then repeatedly every time a let or `#Binding var` that is passed to this struct's init has changed from last time.
func updateUIView(_ uiView: UITextView, context: Context) {
//uiView.attributedText = outerMutableString
uiView.text = outerMutableString2
// we don't usually pass bindings in to the coordinator and instead use closures.
// we have to set a new closure because the binding might be different.
context.coordinator.stringDidChange2 = { string in
outerMutableString2 = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: 30.0)
textView.delegate = self
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: self,
action: #selector(underline))
toolBar.items = [underlineButton]
textView.inputAccessoryView = toolBar
return textView
}()
//var stringDidChange: ((NSMutableAttributedString) -> ())?
var stringDidChange2: ((String) -> ())?
func textViewDidChange(_ textView: UITextView) {
//innerMutableString = textView.textStorage
//stringDidChange?(textView.textStorage)
stringDidChange2?(textView.text)
}
func textViewDidChangeSelection(_ textView: UITextView) {
// selectedRange = textView.selectedRange
}
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
textView.textStorage.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: range)
// stringDidChange?(textView.textStorage)
}
}
}
}

SwiftUI: Always active textfield keyboard doesn't dismiss

I made a custom Textfield an always active textfield with the help of some tutorials, meaning that user can tap "next on keyboard" and commits the message and continue doing so without hiding the keyboard, but now the keyboard is stuck, doesn't close when tapped outside, I tried everything I saw online, some of them were basically triggering UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
Note: my minimum iOS requirements is iOS 14 which restricts some of the new functions that helps with my issue in iOS15.
that didn't work with that Custom Textfield, its like its overriding those functions.
here is the textfield code:
import SwiftUI
struct AlwaysActiveTextField: UIViewRepresentable {
let placeholder: String
#Binding var text: String
var focusable: Binding<[Bool]>?
var returnKeyType: UIReturnKeyType = .next
var autocapitalizationType: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var isSecureTextEntry: Bool
var tag: Int
var onCommit: () -> Void
func makeUIView(context: Context) -> UITextField {
let activeTextField = UITextField(frame: .zero)
activeTextField.delegate = context.coordinator
activeTextField.placeholder = placeholder
activeTextField.font = .systemFont(ofSize: 14)
activeTextField.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [NSAttributedString.Key.foregroundColor: UIColor(contentTertiary)]
)
activeTextField.returnKeyType = returnKeyType
activeTextField.autocapitalizationType = autocapitalizationType
activeTextField.keyboardType = keyboardType
activeTextField.isSecureTextEntry = isSecureTextEntry
activeTextField.textAlignment = .left
activeTextField.tag = tag
// toolbar
if keyboardType == .numberPad { // keyboard does not have next so add next button in the toolbar
var items = [UIBarButtonItem]()
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
let nextButton = UIBarButtonItem(title: "Next", style: .plain, target: context.coordinator, action: #selector(Coordinator.showNextTextField))
items.append(contentsOf: [spacer, nextButton])
toolbar.setItems(items, animated: false)
activeTextField.inputAccessoryView = toolbar
}
// Editin listener
activeTextField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
return activeTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if let focusable = focusable?.wrappedValue {
if focusable[uiView.tag] { // set focused
uiView.becomeFirstResponder()
} else { // remove keyboard
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let activeTextField: AlwaysActiveTextField
var hasEndedViaReturn = false
weak var textField: UITextField?
init(_ activeTextField: AlwaysActiveTextField) {
self.activeTextField = activeTextField
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.textField = textField
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
var focusable: [Bool] = Array(repeating: false, count: textFieldCount) // remove focus from all text field
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
}
// work around for number pad
#objc
func showNextTextField() {
if let textField = self.textField {
_ = textFieldShouldReturn(textField)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
hasEndedViaReturn = true
guard var focusable = activeTextField.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
activeTextField.onCommit()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if !hasEndedViaReturn {// user dismisses keyboard
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
// reset all text field, so that makeUIView cannot trigger keyboard
activeTextField.focusable?.wrappedValue = Array(repeating: false, count: textFieldCount)
} else {
hasEndedViaReturn = false
}
}
#objc
func textFieldDidChange(_ textField: UITextField) {
activeTextField.text = textField.text ?? ""
}
}
}
used it on sample SwiftUI Sheet view and inside the .sheet view:
AlwaysActiveTextField(
placeholder: "Add...",
text: $newItemName,
focusable: $fieldFocus,
returnKeyType: .next,
isSecureTextEntry: false,
tag: 0,
onCommit: {
if !newItemName.isEmpty {
let newChecklistItem = ChecklistItem(
shotId: shotlistViewModel.shot.id,
name: self.newItemName,
isChecked: false,
description: ""
)
self.checklistItems.append(newChecklistItem)
if !offlineMode {
self.viewModel.addChecklistItem(newChecklistItem)
}
self.newItemName = ""
}
}
)

How to use function from other struct/view in SwiftUI?

Newbie SwiftUI Dev here.
I want to create a scheduling app in SwiftUI and I would like to create a button in navigation bar which change calendar's scope.
From .week to month and return.
struct HomeVC: View {
init() {
navbarcolor.configureWithOpaqueBackground()
navbarcolor.backgroundColor = .systemGreen
navbarcolor.titleTextAttributes = [.foregroundColor: UIColor.white]
navbarcolor.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
UINavigationBar.appearance().standardAppearance = navbarcolor
UINavigationBar.appearance().scrollEdgeAppearance = navbarcolor
}
#State private var selectedDate = Date()
var body: some View {
NavigationView{
VStack{
CalendarRepresentable(selectedDate: $selectedDate)
.frame(height: 300)
.padding(.top, 15)
Spacer()
ListView()
}
.navigationBarTitle("Calendar")
.toolbar {
Button(action: {
switchCalendarScope()
}) {
Text("Toggle")
}
}
}
}
}
This is my calendar struct, and I would like to take from here the switchCalendarScope function, and use it into button's action, but doesn't work.
struct CalendarRepresentable: UIViewRepresentable{
typealias UIViewType = FSCalendar
#Binding var selectedDate: Date
var calendar = FSCalendar()
func switchCalendarScope(){
if calendar.scope == FSCalendarScope.month {
calendar.scope = FSCalendarScope.week
} else {
calendar.scope = FSCalendarScope.month
}
}
func updateUIView(_ uiView: FSCalendar, context: Context) { }
func makeUIView(context: Context) -> FSCalendar {
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
calendar.allowsMultipleSelection = true
calendar.scrollDirection = .vertical
calendar.scope = .week
//:Customization
calendar.appearance.headerTitleFont = UIFont.systemFont(ofSize: 25, weight: UIFont.Weight.heavy)
calendar.appearance.weekdayFont = .boldSystemFont(ofSize: 15)
calendar.appearance.weekdayTextColor = .black
calendar.appearance.selectionColor = .systemGreen
calendar.appearance.todayColor = .systemGreen
calendar.appearance.caseOptions = [.headerUsesUpperCase, .weekdayUsesUpperCase]
calendar.appearance.headerTitleColor = .black
return calendar
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, FSCalendarDelegate, FSCalendarDataSource {
var parent: CalendarRepresentable
var formatter = DateFormatter()
init(_ parent: CalendarRepresentable) {
self.parent = parent
}
func calendar(_ calendar: FSCalendar, numberOfEventsFor date: Date) -> Int {
return 0
}
func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) {
formatter.dateFormat = "dd-MM-YYYY"
print("Did select == \(formatter.string(from: date))")
}
func calendar(_ calendar: FSCalendar, didDeselect date: Date, at monthPosition: FSCalendarMonthPosition) {
formatter.dateFormat = "dd-MM-YYYY"
print("Did de-select == \(formatter.string(from: date))")
}
}
}
Can anybody help?
You don't need to trigger the function in your UIViewRepresentable. You simply need to declare a variable in there that is the representation of the selected scope, and pass that in with your initializer. I am going to assume that your scope variable is of Type Scope for this:
struct CalendarRepresentable: UIViewRepresentable {
typealias UIViewType = FSCalendar
#Binding var selectedDate: Date
var calendar = FSCalendar()
var scope: Scope
func updateUIView(_ uiView: FSCalendar, context: Context) { }
func makeUIView(context: Context) -> FSCalendar {
calendar.delegate = context.coordinator
calendar.dataSource = context.coordinator
calendar.allowsMultipleSelection = true
calendar.scrollDirection = .vertical
// Set scope here
calendar.scope = scope
//:Customization
...
return calendar
}
...
}
Then from the HomeVC view you would call it like this:
CalendarRepresentable(selectedDate: $selectedDate, scope: scope)
The view will get recreated as needed. Also, one last thing, in SwiftUI there are no ViewControllers. Your HomeVC should just be named Home. It is the view, not a view controller, and they work differently and take a different mental model. This is why you were struggling in solving this. Even the UIViewRepresentable is a view in the end, and it just wraps a ViewController and instantiates the view. And they are all structs; you don't mutate a struct, you simply recreate it when you need to change it.

Cannot update value when wrapping SwiftUI binding in another binding

I want to make a login code screen. This consists of 4 separate UITextField elements, each accepting one character. What I did is implement a system whereby every time one of the UITextField's changes it will verify if all the values are filled out, and if they are update a boolean binding to tell the parent object that the code is correct.
In order to do this I wrap the #State variables inside a custom binding that does a callback on the setter, like this:
#State private var chars:[String] = ["","","",""]
...
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
and those bindings are passed to the components. Every time my text value changes validateCode is called. This works perfectly.
However now I want to add an extra behavior: If the user types 4 characters and the code is wrong I want to move the first responder back to the first textfield and clear its contents. The first responder part works fine (I also manage that using #State variables, but I do not use a binding wrapper for those), however I can't change the text inside my code. I think it's because my components use that wrapped binding, and not the variable containing the text.
This is what my validateCode looks like:
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.chars = ["","","",""]
}
}
hasFocus does its thing correctly and the cursor is being moved to the first UITextField. The text however remains in the text fields. I tried creating those bindings in the init so I could also use them in my validateCode function but that gives all kinds of compile errors because I am using self inside the getter and the setter.
Any idea how to solve this? Should I work with Observables? I'm just starting out with SwiftUI so it's possible I am missing some tools that I can use for this.
For completeness, here is the code of the entire file:
import SwiftUI
struct CWCodeView: View {
var value:String
#Binding var isValid:Bool
#State private var chars:[String] = ["","","",""]
#State private var hasFocus = [true,false,false,false]
#State private var nothingHasFocus:Bool = false
init(value:String,isValid:Binding<Bool>) {
self.value = value
self._isValid = isValid
}
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.nothingHasFocus = false
self.chars = ["","","",""]
}
}
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
return GeometryReader { geometry in
ScrollView (.vertical){
VStack{
HStack {
CWNumberField(letter: bindings[0],hasFocus: self.$hasFocus[0], previousHasFocus: self.$nothingHasFocus, nextHasFocus: self.$hasFocus[1])
CWNumberField(letter: bindings[1],hasFocus: self.$hasFocus[1], previousHasFocus: self.$hasFocus[0], nextHasFocus: self.$hasFocus[2])
CWNumberField(letter: bindings[2],hasFocus: self.$hasFocus[2], previousHasFocus: self.$hasFocus[1], nextHasFocus: self.$hasFocus[3])
CWNumberField(letter: bindings[3],hasFocus: self.$hasFocus[3], previousHasFocus: self.$hasFocus[2], nextHasFocus: self.$nothingHasFocus)
}
}
.frame(width: geometry.size.width)
.frame(height: geometry.size.height)
.modifier(AdaptsToSoftwareKeyboard())
}
}
}
}
struct CWCodeView_Previews: PreviewProvider {
static var previews: some View {
CWCodeView(value: "1000", isValid: .constant(false))
}
}
struct CWNumberField : View {
#Binding var letter:String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
var body: some View {
CWSingleCharacterTextField(character:$letter,hasFocus: $hasFocus, previousHasFocus: $previousHasFocus, nextHasFocus: $nextHasFocus)
.frame(width: 46,height:56)
.keyboardType(.numberPad)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.init("codeBorder"), lineWidth: 1)
)
}
}
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField.init()
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}
Thanks!
you just make a little mistake, but i cannot believe you just "started" SwiftUI ;)
1.) just build textfield one time, so i took it as a member variable instead of building always a new one
2.) update the text in updateuiview -> that's it
3.) ...nearly: there is still a focus/update problem...the last of the four textfields won't update correctly ...i assume this is a focus problem....
try this:
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
let textField = UITextField.init()
func makeUIView(context: Context) -> UITextField {
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = character
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}