Denis
Denis

Reputation: 795

UITextView undo manager do not work with replacement attributed string (iOS 6)

iOS 6 has been updated to use UITextView for rich text editing (a UITextView now earns an attributedText property —which is stupidly non mutable—). Here is a question asked on iOS 6 Apple forum under NDA, that can be made public since iOS 6 is now public...

In a UITextView, I can undo any font change but cannot undo a replacement in a copy of the view's attributed string. When using this code...

- (void) replace: (NSAttributedString*) old with: (NSAttributedString*) new

{
1.    [[myView.undoManager prepareWithInvocationTarget:self] replace:new with:old];
2.    old=new;

}

... undoing is working well.

But if I add a line to get the result visible in my view, the undoManager do not fire the "replace:with:" method as it should...

- (void) replace: (NSAttributedString*) old with: (NSAttributedString*) new

{
1.    [[myView.undoManager prepareWithInvocationTarget:self] replace:new with:old];
2.    old=new;
3.    myView.attributedText=[[NSAttributedString alloc] initWithAttributedString:old];

}

Any idea? I have the same problem with any of the replacement methods, using a range or not, for MutableAttributedString I tried to use on line "2"...

Upvotes: 5

Views: 2272

Answers (3)

user8637708
user8637708

Reputation: 3773

UITextView has undoManager that will manage undo and redo for free without requiring any additional code.

Replacing its attributedText will reset the undoManager (Updating text and its attributes in textStorage not work for me either). However, I discovered that undo and redo functionality works normally when formatting text without replacing attributedText but by standard edit actions (Right click on highlighting text > Font > Bold (Mac Catalyst)).

To ensure that undoManager works properly, you need to use only certain specific methods of UITextView. Using other methods may break the undo functionality of UITextView.

Editing Text

  1. You need to set the allowsEditingTextAttributes of UITextView to be true, this will make UITextView support undo and redo of attributedText.
self.textView.allowsEditingTextAttributes = true
  1. If you want to change the text of attributedText, use replace(_:withText:) of UITextInput, or insertText(_:) and deleteBackward() of UIKeyInput that UITextView conforming to.
self.textView.replace(self.textView.selectedTextRange!, withText: "test")

Updating Attributes

If you want to change attributes of text, use updateTextAttributes(conversionHandler:) of UITextView instead.

self.textView.updateTextAttributes { _ in
                
    let font = UIFont.boldSystemFont(ofSize: 17)
    let attributes: [NSAttributedString.Key: Any] = [
        .font: font,
    ]
    
    return attributes

}

or

self.textView.updateTextAttributes { attributes in
    
    let newAttributes = attributes
    let font = UIFont.boldSystemFont(ofSize: 17)
    let newAttributes: [.font] = font
    
    return newAttributes

}

Inserting Attachments

According to the documentation for init(attachment:) in NSAttributedString.

This is a convenience method for creating an attributed string containing an attachment using character (NSTextAttachment.character) as the base character.

If you want to add an attachment using updateTextAttributes, you should insert a special character (The attachment will not show up if you are not using this character.) for the attachment first (NSTextAttachment.character).

For example,

let attachment = NSTextAttachment(image: image)
let specialChar = String(Character(UnicodeScalar(NSTextAttachment.character)!))

textView.insertText(specialChar)
textView.selectedRange = NSRange(location: textView.selectedRange.lowerBound-specialChar.count, length: specialChar.count)
textView.updateTextAttributes { attributes in
           
    var newAttributes = attributes
    newAttributes[.attachment] = attachment
    return newAttributes
            
}

For changing text and its attributes in specific range, modify the selectedRange or selectedTextRange of UITextView.

To implement undo and redo buttons, check this answer : https://stackoverflow.com/a/50530040/8637708

I have tested with Mac Catalyst, it should work on iOS and iPadOS too.

Upvotes: 2

Brad G
Brad G

Reputation: 2593

Umm, wow I really didn't expect this to work! I couldn't find a solution so I just started trying anything and everything...

- (void)applyAttributesToSelection:(NSDictionary*)attributes {
    UITextView *textView = self.contentCell.textView;

    NSRange selectedRange = textView.selectedRange;
    UITextRange *selectedTextRange = textView.selectedTextRange;
    NSAttributedString *selectedText = [textView.textStorage attributedSubstringFromRange:selectedRange];

    [textView.undoManager beginUndoGrouping];
    [textView replaceRange:selectedTextRange withText:selectedText.string];
    [textView.textStorage addAttributes:attributes range:selectedRange];
    [textView.undoManager endUndoGrouping];

    [textView setTypingAttributes:attributes];
}

Upvotes: 4

Joan Lluch
Joan Lluch

Reputation: 1

The Undo Manager is reset after setting its 'text' or 'attributedText' property, this is why it does not work. Whether this behavior is a bug or by design I don't know.

However, you can use the UITextInput protocol method instead.

  • (void)replaceRange:(UITextRange *)range withText:(NSString *)text

This works.

Upvotes: 0

Related Questions