How to undo UITextField edits in Swift - swift

I'm sure there's a simple answer to this, but I'm pretty noob to programming and have searched extensively here and on the few Undo tutorials I could find.
I have two buttons and a UITextField with a number in it. The buttons increment the number in the textfield up or down by 1 respectively. I have several more sets of these two buttons and a textfield, each representing something different. Then I have undo and redo buttons that undo the changes to the text fields caused by pressing the buttons. This all works great.
So now I'm trying to add the ability to undo changes from editing the text field directly with the number pad, so that it goes into the same undo stack and can be undone and redone from the same undo and redo buttons.
Here's a snippet of the code that controls the undo and redo of the button presses. Then, like I said, I have several more sets of these that all go into the same undo stack. Thanks.
The UITextField
#IBOutlet weak var Money: UITextField!
The Undo and Redo Buttons
#IBAction func Undo(_ sender: UIButton)
{
undoManager?.undo()
}
#IBAction func Redo(_ sender: UIButton)
{
undoManager?.redo()
}
The Up One and Down One Buttons
#IBAction func MonDown(_ sender: UIButton)
{
subtractOneMon(Mon: Money)
}
#IBAction func MonUp(_ sender: UIButton)
{
addOneMon(Mon: Money)
}
The Functions Registering the Undo/Redo
func subtractOneMon(Mon: UITextField)
{
undoManager?.registerUndo(withTarget: self, handler:
{(targetSelf) in
targetSelf.addOneMon(Mon: self.Money)
})
let Mon = Double(Money.text!)
let NewMon = Double(Mon! - 1)
Money.text = Int(NewMon).description
}
func addOneMon(Mon: UITextField)
{
undoManager?.registerUndo(withTarget: self, handler:
{(targetSelf) in
targetSelf.subtractOneMon(Mon: self.Money)
})
let Mon = Double(Money.text!) ?? 0
let NewMon = Double(Mon + 1)
Money.text = Int(NewMon).description
}

You might want to add an "Editing Changed" IBAction for your text field.
This function will be called each time any content of the field is changed.
You'd want to store the previous value of the text field somewhere.
Inside the "Editing Changed" IBAction you'd register an undo operation in your undoManager using the previous value and then update the previous value with the current value.
P.S. There's a potential memory leak in the way your handlers are implemented: currently they are holding a strong reference to your UIViewController. Consider adding [weak self] in the capture list. Check out the Memory Management article by John Sundell to learn more.

Related

UICollectionView Cell Button Long Press Gesture return self keep changing?

I'm having something like a shopping cart page, it shows the products the user selected in a collection view, with each cell having 2 button linked to the product that will increase/decrease the amount of product to a singleton cartManager.
So far everything is working, i used protocol to ensure i know which product in the collection view that i'm adding/subtracting the product from. With this code:
protocol CartProductButtonDelegate : class {
func cartProductPlus(_ sender: CartProductCell)
func cartProductMinus(_ sender: CartProductCell)
}
class CartProductCell: UICollectionViewCell{
//labels and image product details etc.
#IBOutlet weak var productMinusBtn: UIButton!
#IBOutlet weak var productPlusBtn: UIButton!
weak var delegate : CartProductButtonDelegate?
override func awakeFromNib() {
super.awakeFromNib()
let tapPlusGesture = UITapGestureRecognizer(target: self, action: #selector(productPlusBtnTapped(_:)))
tapPlusGesture.numberOfTapsRequired = 1
self.productPlusBtn.addGestureRecognizer(tapPlusGesture)
let tapMinusGesture = UITapGestureRecognizer(target: self, action: #selector(productMinusBtnTapped(_:)))
tapMinusGesture.numberOfTapsRequired = 1
self.productMinusBtn.addGestureRecognizer(tapMinusGesture)
}
#objc func productMinusBtnTapped(_ sender: UITapGestureRecognizer) {
delegate?.cartProductMinus(self)
}
#objc func productPlusBtnTapped(_ sender: UITapGestureRecognizer) {
delegate?.cartProductPlus(self)
}
}
And in my UIViewController, i add the collectionview delegate,datasource, and the custom protocol and make all the cell's delegate to the viewcontroller in cellForItem. Everytime i add or subtract the product, i reload the collectionview in order to show the correct amount on the cell label.
func cartProductPlus(_ sender: CartProductCell) {
guard let tappedIndexPath = self.cartCollectionView.indexPath(for: sender) else {
debugPrint("GUARD BROKE GETTING INDEX PATH FOR PRODUCT PLUS TAPPED")
return
}
let product = self.productList[tappedIndexPath.item]
debugPrint("cart Product Plus on product name: \(product.name), index : \(tappedIndexPath.item)")
if let maxBought = Int(product.maxBought ?? ""){
if cartManager.numberOfProductsInCart(product: product) < maxBought{
cartManager.addProduct(product: product)
}
}
self.rearrangeArray()//this is to reload the collection view as well as update UI on cart and someother stuff
}
The problem arise when i tried to add long press gesture using existing logic, for those people who want to buy in bulk.
I've tried to implement this:
let longPressPlusGesture = UILongPressGestureRecognizer(target: self, action: #selector(productPlusLongPressed(_:)))
self.productPlusBtn.addGestureRecognizer(longPressPlusGesture)
#objc func productPlusLongPressed(_ sender: UILongPressGestureRecognizer){
if sender.state == .began || sender.state == .changed{
delegate?.cartProductPlus(self)
}
}
However when i long pressed the button, the item its adding are mixed up, the debug message are showing the collection view cell's index i'm receiving is going up in ascending order 0,1,2,3 then repeat 0,1,2,3 (depending on how many products are there in the collection view cell)
So question, is there a way to fix this? should i not reload collection view when i'm long pressing, if so, how do i update the UI to inform the user. Is there other method to work around the problem or should i just give up the idea of long press and just allow the user to tap the amount and edit it?
Alright, found a work around. After i've implemented the long press delegate, i split the protocol to two extra functions, one for when long press began/still press and the other for when long press ends.
func cartProductLongPlusStarted(_ sender: CartProductCell)
func cartProductLongMinusStarted(_ sender: CartProductCell)
func cartProductLongPlusEnded(_ sender: CartProductCell)
func cartProductLongMinusEnded(_ sender: CartProductCell)
However i do not update the UI from the viewcontroller when long press is active, i just update them from the cell itself. The cell will just hardcode to update the UI and only when the long press is finished, the view controller will just update the UI again.
#objc func productPlusLongPressed(_ sender: UILongPressGestureRecognizer){
if sender.state == .began || sender.state == .changed{
delegate?.cartProductLongPlusStarted(self)
if var amount = Int(self.productCountLabel.text ?? "0"){
if self.maxAmount != nil{
if amount < self.maxAmount!{
amount += 1
}
}else{
amount += 1
}
self.productCountLabel.text = String(amount)
}
}else{
delegate?.cartProductLongPlusEnded(self)
}
}
The only minor issue is that the long press seems to update too fast, the value might be updating abit too fast for the user to react when to stop properly, any idea to slow the updating the long press function call a bit?

Swift 3 hide NSButton immediately after click

How can I have a button disappear after it's been clicked?
#IBAction func onClick(_ sender: NSButton) {
sender.isHidden = true;
//...a lot of blocking instructions below this line
}
The above works to a certain extent, as the "sender" / button is hidden only after all of the instructions in the function have been processed. I have some blocking IO in the function (socket connections etc.) and I want the button to disappear before all that happens.
I tried using both outlets and sender.
#IBAction func onClick(_ sender: NSButton) {
sender.isHidden = true;
DispatchQueue.main.async {
//...a lot of blocking instructions below this line
}
}
I managed to achieve the desired effect by putting the "blocking" piece of code in the following statement (and pushing the .isHidden setting through immediately, in a synchronous fashion):
DispatchQueue.main.async { /*code*/ }

Give Values to Buttons - Swift

I was wondering if someone can help me with this. Right now when I click on the button 0 it will automatically take me to the 2nd screen for testing purposes. Now I want to give value to the numbers accordingly and when the number clicked is equals to the number on my label than it will show the 2nd screen. If not, the Question mark label will blink or change color. Thanks
Place this variable above viewDidLoad():
var chosenNumber: Int?
Attach your buttons to an IBAction like this one and in the storyboard make each of their tags equal to their face value.
#IBAction func aNumberButtonWasPressed(_ sender: UIButton) {
chosenNumber = sender.tag
performSegue(withIdentifier: "theSegueIdentifier", sender: nil)
}
Pass the number to the following view controller like so and then use that number to populate the label accordingly.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let vc = segue.destination as? TheViewControllerYouAreGoingTo {
vc.theVariableYouWantToSaveYourNumberTo
}
}
If you want to know how to do something differently, just ask.
If you want to really learn swift well, I highly recommend taking this Paul Hegarty Course for FREE. Paul Hegarty Course
The first two lessons specifically will teach you everything you could want to know about proper swift syntax and building a calculator early on. It might seem slightly advanced, but that is why I have watched the course 3 times now. Every 3-6 months since I started learning Swift this has helped refine my skills.
EDIT
let thisString = label.text
let thisInt = Int(thisString)
thisInt will be an optional, so at some point you will need to unwrap it. You can use it however you want to and set it to another label like so:
anotherLabel.text = "\(thisInt)"

Connect 2 different buttons to the same outlet (Swift)

I want to connect a second button to a label #IBOutlet var ourScore: UILabel! .
The first button is connected as
#IBAction func buttonPressed(sender: AnyObject) {
ourScore.text = "\(++score)"
}
How is it possible that I can add another button to the label, so when you click on the second button it works together. The first one counts it up and the second one needs to reset it, back to 0.
Your button isn't linked to your label outlet, the code of your IBAction makes reference to it. You posted this method:
#IBAction func buttonPressed(sender: AnyObject)
{
ourScore.text = "\(++score)"
}
so create a new method:
#IBAction func resetButtonPressed(sender: AnyObject)
{
score = 0;
ourScore.text = "\(score)"
}
Link that second IBAction method to your second button. Done.
You create the button in the same exact way you created the first one but instead of ++ it'd be -- (or whatever you want it to be).
The label is available to anything inside of your class so you can manipulate it and read from it anywhere.

UITextView paste method override

I really want to be able to detect a paste event in a UITextView, however it appears this cannot be done.
I originally tried subclassing a UITextView and overriding the paste: method, but it never gets called on a paste event.
Has anyone been able to do this? A previous question on the same ilk didn't have an answer back in August...
The text view doesn't catch the paste: event because it wasn't the actual responder is not the text view, but the private web view (UIWebDocumentView) that powers the text view.
However, on paste, the web view will call the text view's (private) -[UITextView keyboardInput:shouldInsertText:isMarkedText:], and in turn, the text view's delegate's -textView:shouldChangeTextInRange:replacementText:.
Therefore, you just need to implement -textView:shouldChangeTextInRange:replacementText: in the text view's delegate.
(Of course, normal keyboard input will trigger this method too. There's no perfect way to distinguish them.)
#KennyTM what I did for one of my applications was keep up with the current text length and the previous text length. If the (currentTextLength - previousTextLength) was greater than 1, then the user must have pasted something
With iOS 14 you have to do this in two parts to avoid showing the user notification that you are checking the UIPasteboard. In my case I did not want to do anything bad with the user data but I did want to do some special formating when the user did paste into the UITextView.
Step 1: Create a custom UITextView and override paste()
import UIKit
protocol TouchableTextViewDelegate : class{
func touchesDidBegin()
func pasting()
}
class TouchableTextView: UITextView {
weak var touchableDelegate : TouchableTextViewDelegate?
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.isFirstResponder{
return true
}
touchableDelegate?.touchesDidBegin()
return false
}
override func paste(_ sender: Any?) {
touchableDelegate?.pasting()
super.paste(sender)
}
}
Step 2: In the file location where you handle the shouldChangeTextIn create a variable and be sure to set the delegate for the TouchableTextView. In my case
//top of the view
var isPasting : Bool = false
//also when creating UITextView use both delegates
textView.touchableDelegate = self
//add the normal delegate
textView.delegate = self
extension SliderTextView : TouchableTextViewDelegate{
func pasting() {
self.isPaste = true
}
func touchesDidBegin() {
sliderEditingDelegate?.touchesDidBegin(sliderTextView: self)
}
}
Step 3: Inside shouldChangeTextIn I handle the action like this
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let isPaste = self.isPaste
//be sure to set this to false
self.isPaste = false
if isPaste,
let pt = UIPasteboard.general.string,
text.contains(pt){
//you will see the paste notification and that is good for the user
// but only when the user pastes
// do whatever special thing or formatting you want to do
}
return true
}
The good is that you will not trigger the notification unless the user is pasting in the UITextView.
To detect if a user is parsing a text in a textView, compare the replacementText in the shouldChangeTextInRange delegate with the text the user is currently holding in the UIPasteboard. Then take action depending on requirements.
for code, see my answer in the following question:
how to know when text is pasted into UITextView