Swift: Checkbox insertion in UITextView of a UITableViewCell and using UITapGestureRecognizer to recognize taps on the checkbox

混江龙づ霸主 提交于 2020-01-13 18:04:53

问题


Introduction

Context:

I am working on one of my first own apps, a sort of note-taking app.

The basic framework is simple: I have a UITableView with cells in them with a few design elements and a large UITextView which the user can write their notes into.

Issue:

  • I would like to implement the checkbox feature, similarly to what Apple has in their app "Notes". I want it to be a "part" of the text, so its erasable by hitting the erase on the keyboard.

  • I have checked posts on SO about inserting a character to a UITextView, which works (see code below), but they differ in their goal when it comes to the actual tap recognition. But I cant figure out how to check if the tap is on my NSAttributedString or not.

Question:

1. How do I swap out the characters that make up the checkbox when a user taps on them? 2. How do I get the UITapGestureRecognizer to work properly on my TextView? See edit also

Edit:

Old edited issues: (solved through silicon_valleys answer)

  • My UITapGestureRecognizer doesn't work as intended, it doesn't seem to respond to the taps.
  • How can I check if my checkbox is tapped on and replace the characters with the checkmark?
  • How do I insert the checkbox only at the beginning of the line which the user's cursor is on?

Smaller new issues:

  • The tap recognizer now works perfectly. But I cant find a way to 1. Convert the NSRange to a UITextRange so I can replace the characters or 2. use the NSRange to insert a character in the TextView.

Code:

  • checkbox insertion method toolbarCheckbox(sender: UIBarButtonItem)

    //This method is in my ViewController and adds a checkbox
    @objc func toolbarCheckbox(sender: UIBarButtonItem) {
    
           let checkboxCharacter: Character = "\u{25EF}"
           let emptyCheckbox = " \(checkboxCharacter)  "
    
           guard case let cell as EventsCell = tableView.cellForRow(at: sender.indexPathID!) else {return}
    
           var beginningOfLineForSelection = cell.eventText.selectedTextRange.flatMap {
               cell.eventText.selectionRects(for: $0).first as? UITextSelectionRect
               }?.rect.origin ?? .zero
           beginningOfLineForSelection.x = 0
    
           let layoutManager = cell.eventText.layoutManager
           let firstGlyphOnLine = layoutManager.glyphIndex(
               for: beginningOfLineForSelection,
               in: cell.eventText.textContainer,
               fractionOfDistanceThroughGlyph: nil)
    
           let newText = NSMutableAttributedString(attributedString: cell.eventText.attributedText)
           newText.insert(NSAttributedString(string: emptyCheckbox, attributes: [NSAttributedStringKey.font : UIFont(name: "Didot", size: 16)!]), at: firstGlyphOnLine)
    
           cell.eventText.attributedText = newText
    
    } 
    
  • UITextView creation (in my cell class)

    let eventText : GrowingTextView = {
         let tv = GrowingTextView(frame: .zero)
    
         let uncheckedBox = NSMutableAttributedString(string: checkboxCharacter, attributes: [NSAttributedStringKey.font : UIFont(name: "Didot", size: 16)!])
         uncheckedBox.append(NSMutableAttributedString(string: checkedBoxCharacter, attributes: [NSAttributedStringKey.font : UIFont(name: "Didot", size: 16)!]))
         tv.attributedText = uncheckedBox
    
         tv.allowsEditingTextAttributes = true
         tv.isScrollEnabled = false
         tv.textContainerInset = UIEdgeInsetsMake(1, 1, 0, 1)
         tv.translatesAutoresizingMaskIntoConstraints = false
         tv.backgroundColor = .clear
         return tv
    }()
    
  • UITapGestureRecognizer action:

        @objc func checkboxTapDone(sender: UITapGestureRecognizer) {
    
              guard let cell = tableView.cellForRow(at: sender.indexPathID!) as? EventsCell else { return }
              let layoutManager = cell.eventText.layoutManager
    
              var location = sender.location(in: cell.eventText)
              location.x -= cell.eventText.textContainerInset.left
              location.y -= cell.eventText.textContainerInset.top
    
              let textTapped = layoutManager.glyphIndex(for: location, in: cell.eventText.textContainer, fractionOfDistanceThroughGlyph: nil)
              let substring = (cell.eventText.attributedText.string as NSString).substring(with: NSMakeRange(textTapped, 1))
    
                  if substring == uncheckedBox {
    
                   }
                   else if substring == checkedBox {
    
                   }
        }
    

Thanks for reading my post.


回答1:


It is not possible to add a UIButton or other UIControl in the middle of the text of a UITextView. What you could do though, is using the attributedString property of the UITextView to render a checkbox character. You can do this by using a custom font with a character that matches the checkbox (checked and unchecked). You will need to add a UITapGestureRecognizer to the UITextView and then convert the point of the touch to detect if a checkbox was tapped and change the checkbox character from a selected checkbox to unselected and vice versa. You also need to bear in mind that you will have to set a delegate on the UITapGestureRecognizer to make sure it allows simultaneous touches with the normal touches for moving the cursor and only intercept the tap when it is on a checkbox symbol.

You can add a button in a toolbar above the keyboard which will add the checkbox symbol to the text (see this solution: How can I add a toolbar above the keyboard?).

I hope this helps.

Answer to your further questions

  • My UITapGestureRecognizer doesn't work as intended, it doesn't seem to respond to the taps.

    This is because you try to use a single gesture recognizer on all of your textviews. You should create a new UITapGestureRecognizer for each UITextView. Now it will be removed from the previous views as you are adding the to the later views (so it will only detect the tap on the last cell that is dequeued).

  • How can I check if my checkbox is tapped on and replace the characters with the checkmark?

    The code you have inside checkboxTapDone(sender:) should deal with that. You are mostly there, but the range you are creating should only be 1 character, not 9. You then need to compare the substring value with the value of a checked and unchecked character. Then update the eventText.attributedText with the opposite character.

  • How do I insert the checkbox only at the beginning of the line which the user's cursor is on?

    The following code snippet should help you determine the range where you can insert the checkbox character.

var beginningOfLineForSelection = textView.selectedTextRange.flatMap {    
    textView.selectionRects(for: $0).first as? UITextSelectionRect 
}?.rect.origin ?? .zero
beginningOfLineForSelection.x = 0

let layoutManager = textView.layoutManager
let firstGlyphOnLine = layoutManager.glyphIndex(
    for: beginningOfLineForSelection,
    in: textView.textContainer,
    fractionOfDistanceThroughGlyph: nil
)

let insertCheckboxRange = NSMakeRange(firstGlyphOnLine, 0)

Answer to smaller new issues

  • The reason you weren't seeing the replaceCharacters(in:with:) method (which takes an NSRange as its first argument), is because you were using the NSAttributedString (which isn't mutable). You need to first create a mutable copy of the attributedText property. Then you can change the text and set it again on the UITextView. See code snippet below for an example.
let newText = NSMutableAttributedString(attributedString: textView.attributedText)
newText.replaceCharacters(in: rangeOfCharactersToReplace, with: newCharacters)
textView.attributedText = newText 


来源:https://stackoverflow.com/questions/51803423/swift-checkbox-insertion-in-uitextview-of-a-uitableviewcell-and-using-uitapgest

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!