3

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.

theoadahl
  • 454
  • 4
  • 16
  • https://stackoverflow.com/questions/37674139/place-uibutton-at-the-end-of-text-in-uitextview-in-ios-app-in-xcode-written-in-s this may help you out.... I am not sure ... – Abu Ul Hassan Aug 11 '18 at 20:28
  • I think you have to make some custom work on it like add 5 spaces to your textView initially add a button to textView and then check for text in textView shouldChangeCharacter in range. check if remaining text is equals to 5 spaces and backspace tapped uncheck the box. – Abu Ul Hassan Aug 11 '18 at 20:57

1 Answers1

2

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 
Community
  • 1
  • 1
silicon_valley
  • 2,219
  • 2
  • 14
  • 20
  • 2
    I would word this answer differently. Something like "No, it's not possible to put a button or other control in the middle of a `UITextView`. You COULD use a custom font with characters for checked and unchecked boxes in it and use a tap gesture recognizer attached to the text view to simulate a checkbox..." (I would make it clear that you can't add a control as editable content in a text view.) – Duncan C Aug 11 '18 at 21:39
  • Thanks for your suggestion. I have plunged into Xcode and thrown some code at it based on your answer. Although I have ran into some issues I cant seem to find solution to check if the tap is on my attributed string through the UITapGestureRecognizer. Please see the edit in my question. – theoadahl Aug 12 '18 at 14:53
  • I've added answers to your other three questions. Hope this helps you tackling the problems you were having. – silicon_valley Aug 13 '18 at 19:43
  • Thanks for your answers. Beautiful methology in them also. Although I've run into a little problem: _Which method can I use to replace and insert the checkbox characters using an `NSRange`? They seem to want `UITextRanges` only._ Would really appreciate if you could provide that method also to solve this question. I've edited my question and updated and trimmed the code. – theoadahl Aug 15 '18 at 11:39
  • I've expanded my answer to explain how to replace the characters using an `NSRange`. Hope this helps :) – silicon_valley Aug 15 '18 at 20:17
  • Thank you for your patience in me! And for the detailed, concise, clear and helping answer. I will declare my question as solved here only having a small bug to fix: (if the cursor is at the last index of a line the checkbox gets placed 2 indexes to the left instead of inserting at the first index of the line). If you have a short simple solution to that I would appreciate it, otherwise i’ll continue fiddle with 'til i get it right, i believe I simply implemented your method slightly wrong in my`toolbarCheckbox` method. Thanks for your help! – theoadahl Aug 16 '18 at 23:27
  • You probably want to debug what is going on here. Just walk through it step by step and check that the values that are being calculated are correct. Maybe it's related to the inset you are setting? – silicon_valley Aug 18 '18 at 12:57
  • Removing the inset and adding an if statement for the checkbox method did it. Thank you :) – theoadahl Aug 21 '18 at 18:28