@Claude31
Here is what I get in console:
Put print statements before check on how much math symbols string has:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
//all possible math operation symbols user can add
let symbolsSet = Set(["+","-","x","/"])
var amountOfSymbols = 0
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
//current math symbol user add
let symbol = symbolsSet.filter(completeString.contains).last ?? ""
//if user add math symbol to an empty string - do not insert
if string == symbol, numberString.count == 0 { return false }
print("symbol", symbol)
print("string", string)
print("updatedString", updatedString)
print("correctDecimalString", correctDecimalString)
print("completeString", completeString)
//count how much math symbols string has. If more that one - do not insert, string can have only one
completeString.forEach { character in
if symbolsSet.contains(String(character)) {
amountOfSymbols += 1
}
}
if amountOfSymbols > 1 { return false }
}
Here is the console output after each character I type:
Press 2
symbol
string 2
updatedString 2
correctDecimalString 2
completeString 2
Press +
symbol +
string +
updatedString 2+
correctDecimalString 2+
completeString 2+
Press -
symbol +
string -
updatedString 2+-
correctDecimalString 2+-
completeString 2+-
2.Put print statements after check on how much math symbols string has:
if amountOfSymbols > 1 { return false }
print("symbol", symbol)
print("string", string)
print("updatedString", updatedString)
print("correctDecimalString", correctDecimalString)
print("completeString", completeString)
Here is the console output after each character I type:
Press 2
symbol
string 2
updatedString 2
correctDecimalString 2
completeString 2
Press +
symbol +
string +
updatedString 2+
correctDecimalString 2+
completeString 2+
Press -
Nothing is printed
Post
Replies
Boosts
Views
Activity
@Claude31
The closer I can get is to use the code:
var completeString = correctDecimalString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
if amountOfSymbols > 1 {
let newSymbol = completeString.last?.description ?? ""
completeString.removeAll { symbolsSet.contains(String($0))}
textField.text = completeString+newSymbol
return false
}
But then If I'll add math symbol again it will change the first number which I want to avoid: https://share.cleanshot.com/tmFiAr
Here is how I solved my problem:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 2
formatter.locale = .current
formatter.roundingMode = .down
//set of possible math operations
let symbolsSet = Set(["+","-","x","/"])
let numberString = textField.text ?? ""
guard let range = Range(range, in: numberString) else { return false }
let updatedString = numberString.replacingCharacters(in: range, with: string)
let correctDecimalString = updatedString.replacingOccurrences(of: formatter.decimalSeparator, with: ".")
let completeString = correctDecimalString.replacingOccurrences(of: formatter.groupingSeparator, with: "")
//receive math symbol user typed
let symbol = symbolsSet.filter(completeString.contains).first ?? ""
//receive number of symbols in a String. If user wants to type more than one math symbol - do not insert
let amountOfSymbols = completeString.filter({String($0) == symbol}).count
if amountOfSymbols > 1 { return false }
//receive numbers typed by user
let numbersArray = completeString.components(separatedBy: symbol)
//check for each number - if user wants to type more than one decimal sign - do not insert
for number in numbersArray {
let amountOfDecimalSigns = number.filter({$0 == "."}).count
if amountOfDecimalSigns > 1 { return false }
}
guard let firstNumber = Double(String(numbersArray.first ?? "0")) else { return true }
guard let secondNumber = Double(String(numbersArray.last ?? "0")) else { return true }
let firstFormattedNumber = formatter.string(for: firstNumber) ?? ""
let secondFormattedNumber = formatter.string(for: secondNumber) ?? ""
// if user typed math symbol - show 2 numbers and math symbol, if not - show just first typed number
textField.text = completeString.contains(symbol) ? "\(firstFormattedNumber)\(symbol)\(secondFormattedNumber)" : "\(firstFormattedNumber)"
return string == formatter.decimalSeparator
}
Here is how I was able to achieve my needs:
Switch popupView.isUserInteractionEnabled = false to start swipe recognises not only outside the view
change my didSwipe method onto this:
@objc private func didSwipe(_ sender:UISwipeGestureRecognizer) {
guard let window = UIApplication.shared.windows.first(where: {$0.isKeyWindow}) else { return }
let tappedArea = sender.location(in: popupView)
let popupFrame = CGRect(x: tappedArea.x, y: tappedArea.y, width: popupView.frame.width, height: popupView.frame.height)
if sender.direction == .up, window.frame.contains(popupFrame) {
UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveLinear) {
self.popupView.center.y -= 50
} completion: { _ in
self.popupView.removeFromSuperview()
}
self.isRemovedBySwipe = true
}
}
Now I am able to swipe out the popup only when swipe inside its borders and ignore swipes outside of it.
I call it in needed VC as Popup.instance.showPopup
Hei, MobileTen. Thank you for the kind reply.
I found a way to reduce a space between UITableView header and a UISearchController as:
tableView.contentInset = UIEdgeInsets(top: -10, left: 0, bottom: 0, right: 0)
What is the UILabel Here on GIF: https ://cln.sh/j45g83 ?
It's a grey text just below the searchBar and before the first cell. Take a look I made a new gif for you: https://cln.sh/ajHYYt
When do you want the label to move ? When you scroll the tableView ?
Yes, when I drag tableView, label should follow
If so, why not just put this label in HeaderView of the table ?
Can I put it just like it shows now? (right aligned, the same color and size) Can you suggest how can I move it correctly?
Having a tableView (which is a scrollView) inside another scrollView is complex to handle.
Basically I can't scroll my tableView. It has a fixed size since I have the exact amount of cells and it's not possible to delete or add to it. Just reorder. The only reason I put it all inside a scrollView is to be able to interact with a Navigation Bar Large Title: if I put a label before TableView without scrollView, then when I drag tableView up - the NavBar won't animate (going up), it will be like fixed since label will interfere...
Here is how I tried to achieve my needs:
func textFieldDidChangeSelection(_ textField: UITextField) {
let tapLocation = textField.convert(textField.bounds.origin, to: tableView)
guard let pickedCurrencyIndexPath = tableView.indexPathForRow(at: tapLocation) else { return }
//Append only if array don't have the IndexPath already
if !indexPathsArray.contains(where: {$0 == pickedCurrencyIndexPath}) {
indexPathsArray.append(pickedCurrencyIndexPath)
}
//I need to compare only 2 last IndexPaths, so delete the most old at position 0 in array
if indexPathsArray.count > 2 {
indexPathsArray.remove(at: 0)
}
for indexPath in indexPathsArray {
if indexPath.last != indexPath.first {
//user clicked on a new textfield,clear all so he can type a new number
numberFromTextField = 0
textField.placeholder = "0"
textField.text = ""
}
}
print(indexPathsArray)
}
With this code I almost achieved what I need except when I click on a new textField I can't type anything...
GIF: cln.sh/sGNlBE
Also I have one more related problem I can't fix there.
My prototype cell has a numberTextField (0 by default). When I entering the edit mode I want it to be hidden and back to visible when edit mode is off.
Here is the code for that behaviour in cellForRowAt():
cell.numberTextField.isHidden = tableView.isEditing ? true : false
return cell
But the textField is still visible when I entering the edit mode: https://cln.sh/0hXOKg
Any solutions comes in mind?
I tried to initialise cell as:
let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell") as! ConverterTableViewCell
and assess its textfield property within turnEditing() but no luck.
Claude, it seems I found out what the reason of that crash was. This is what my print() tryings gave: https://cln.sh/jpB5tG
What is a pickedCurrency: this is a global variable of custom type Currency which I created to receive its attribute currentValue (Double, 87.88). I need that value only from the picked to edit cell. After I use that value for calculation in cellForRowAt() and result of the calculation fills all other cells which is not in edit now.
I define pickedCurrency in textFieldDidBeginEditing() because there I receive the exact row of Currency I picked to edit:
func textFieldDidBeginEditing(_ textField: UITextField) {
pickedCurrency = fetchedResultsController.object(at: IndexPath(row: textField.tag, section: 0))
numberFromTextField = 0
textField.textColor = UIColor(named: "BlueColor")
textField.placeholder = "0"
textField.text = ""
}
And then use it's value in cellForRowAt to calculate all other cells values based on pickedCell value:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
let currency = fetchedResultsController.object(at: indexPath)
cell.flag.image = currencyManager.showCurrencyFlag(currency.shortName ?? "notFound")
cell.shortName.text = currency.shortName
cell.fullName.text = currency.fullName
cell.numberTextField.tag = indexPath.row
cell.numberTextField.delegate = self
if let number = numberFromTextField, let pickedCurrency = pickedCurrency {
cell.numberTextField.text = currencyManager.performCalculation(with: number, pickedCurrency, currency)
}
return cell
}
It seems when I delete a lot of cells and then click on random cell to edit it's not updates its IndexPath(row: textField.tag, section: 0)...
And this is why when I call reloadData() it refreshes pickedCurrency.
Maybe there is a way to receive Currency object I picked for editing in cellForRowAt()?
Could you show code with reloadData() in completion handler, in case we can find a way to improve smoothness.
Sure. I just add reloadData() to .delete case. Check the GIF with animation in that case: https://cln.sh/F551dX
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
tableView.reloadData()
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
Then version with asyncAfter .now + 0.5. Check the GIF with animation: https://cln.sh/WK6F8r
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .update:
if let indexPath = indexPath {
tableView.reloadRows(at: [indexPath], with: .none)
}
case .move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.moveRow(at: indexPath, to: newIndexPath)
}
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.tableView.reloadData()
}
}
case .insert:
if let newIndexPath = newIndexPath {
tableView.insertRows(at: [newIndexPath], with: .none)
}
default:
tableView.reloadData()
}
}
Can't you set a var fetchedCompleted that would be set true after fetch
But where is the fetch ends? Where to call that var?
But difficult to say what to write exactly without the complete code of the class.
Attached is the complete class code:
Complete Code
Usually, this is handled by doing the call (to self.tableView.reloadData()) in a completion handler of the calling
and I can call just tableView.reloadData() in the completion hander, everything works perfect too, except the cell deletion animation lost its smoothness, it became fast and ugly. And if I use +0.2 seconds the animation saves the smoothness like it was.
Also this error happens only when I delete a lot of cells one after another (> 10 cells) like on the GIF from initial post. If I will delete 1-2 cells - there won't be any error and no need to call tableView.reloadData().
Sure. It's just:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fetchedResultsController.sections![section].numberOfObjects
}
Where do you do it ?
Nowhere except at FRC delegate method didChange I showed in the initial post...
Could you also show cellForRowAt delegate func ?
Sure, there it is:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "converterCell", for: indexPath) as! ConverterTableViewCell
let currency = fetchedResultsController.object(at: indexPath)
cell.flag.image = currencyManager.showCurrencyFlag(currency.shortName ?? "notFound")
cell.shortName.text = currency.shortName
cell.fullName.text = currency.fullName
cell.numberTextField.tag = indexPath.row
cell.numberTextField.delegate = self
if let number = numberFromTextField, let pickedCurrency = pickedCurrency {
cell.numberTextField.text = currencyManager.performCalculation(with: number, pickedCurrency, currency)
}
return cell
}
Also show you the method where I reload all rows except one in editing mode:
func textFieldDidChangeSelection(_ textField: UITextField) {
let activeTextFieldIndexPath = IndexPath(row: textField.tag, section: 0)
pickedCurrency = fetchedResultsController.object(at: activeTextFieldIndexPath)
guard let currencyObjects = fetchedResultsController.fetchedObjects?.count else {return}
var nonActiveIndexPaths = [IndexPath]()
for object in 0..<currencyObjects where object != textField.tag {
nonActiveIndexPaths.append(IndexPath(row: object, section: 0))
}
tableView.reloadRows(at: nonActiveIndexPaths, with: .none)
}
Then, after, you may call reloadData() once datasource is updated
I found out that if I use reloadData() + 0.2 seconds after deletion, everything works good. But FRC should call reloadData automatically, I shouldn't use it manually...
case .delete:
if let indexPath = indexPath {
tableView.deleteRows(at: [indexPath], with: .none)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.tableView.reloadData()
}
}
Thank you very much, Claude!
After playing with the code I found that this version is good for me:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let formatter = NumberFormatter()
formatter.usesGroupingSeparator = true
formatter.numberStyle = .decimal
formatter.decimalSeparator = "."
formatter.groupingSeparator = " "
let completeString = textField.text!.replacingOccurrences(of: formatter.groupingSeparator, with: "") + string
guard let value = Double(completeString) else { return false }
numberFromTextField = value
let formattedNumber = formatter.string(from: NSNumber(value: value)) ?? ""
textField.text = formattedNumber
if string.isEmpty {
return true
}
return string == formatter.decimalSeparator
}
But I have one problem:
while pressing backspace on keyboard the first press doesn't remove the last character in string, the second and the next presses do. I think the problem is in my if string.isEmpty { return true } line as I found out when you press the backspace it returns an empty string... Short gif with problem: https://cln.sh/OZOslI
Any thoughts what's the problem might be?