Skip to content
4 changes: 4 additions & 0 deletions Aztec.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
599F25541D8BC9A1002871D6 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 599F25321D8BC9A1002871D6 /* TextView.swift */; };
B50CE7321F1FA6260018CAA1 /* NSAttributedString+Strip.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50CE7311F1FA6260018CAA1 /* NSAttributedString+Strip.swift */; };
B50CE7341F1FABA00018CAA1 /* content.html in Resources */ = {isa = PBXBuildFile; fileRef = B50CE7331F1FABA00018CAA1 /* content.html */; };
B52220D31F86A05400D7E092 /* TextViewStubDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52220D21F86A05400D7E092 /* TextViewStubDelegate.swift */; };
B524228D1F30C039002E7C6C /* HTMLDiv.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524228C1F30C039002E7C6C /* HTMLDiv.swift */; };
B524228F1F30C098002E7C6C /* HTMLDivFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B524228E1F30C098002E7C6C /* HTMLDivFormatter.swift */; };
B5375F471EC2566200F5D7EC /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5375F461EC2566200F5D7EC /* String+HTML.swift */; };
Expand Down Expand Up @@ -207,6 +208,7 @@
59FEA06D1D8BDFA700D138DF /* InNodeConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InNodeConverterTests.swift; sourceTree = "<group>"; };
B50CE7311F1FA6260018CAA1 /* NSAttributedString+Strip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Strip.swift"; sourceTree = "<group>"; };
B50CE7331F1FABA00018CAA1 /* content.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = content.html; path = Example/Example/SampleContent/content.html; sourceTree = SOURCE_ROOT; };
B52220D21F86A05400D7E092 /* TextViewStubDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = TextViewStubDelegate.swift; path = TextKit/TextViewStubDelegate.swift; sourceTree = "<group>"; };
B524228C1F30C039002E7C6C /* HTMLDiv.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLDiv.swift; sourceTree = "<group>"; };
B524228E1F30C098002E7C6C /* HTMLDivFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLDivFormatter.swift; sourceTree = "<group>"; };
B5375F461EC2566200F5D7EC /* String+HTML.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -631,6 +633,7 @@
children = (
B5D575841F2288E2003A62F6 /* TextStorageTests.swift */,
B5D575851F2288E2003A62F6 /* TextViewStubAttachmentDelegate.swift */,
B52220D21F86A05400D7E092 /* TextViewStubDelegate.swift */,
B5D575861F2288E2003A62F6 /* TextViewTests.swift */,
B5D575801F226FF4003A62F6 /* UnsupportedHTMLTests.swift */,
B5D575821F22820A003A62F6 /* HTMLRepresentationTests.swift */,
Expand Down Expand Up @@ -1088,6 +1091,7 @@
B5F84B631E706B720089A76C /* NSAttributedStringAnalyzerTests.swift in Sources */,
F10BE61C1EA7B1DB002E4625 /* NSAttributedStringReplaceOcurrencesTests.swift in Sources */,
B5375F491EC2569500F5D7EC /* StringHTMLTests.swift in Sources */,
B52220D31F86A05400D7E092 /* TextViewStubDelegate.swift in Sources */,
B5AB79FA1F5F403C00DF26F5 /* StringParagraphTests.swift in Sources */,
B57534521F267D63009D4904 /* ArrayHelperTests.swift in Sources */,
F14665451EA7C230008DE2B8 /* NSMutableAttributedStringReplaceOcurrencesTests.swift in Sources */,
Expand Down
59 changes: 43 additions & 16 deletions Aztec/Classes/TextKit/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,13 +429,9 @@ open class TextView: UITextView {
// as a demonstration that this is an SDK issue. I also reported this issue to
// Apple (34546954), but this workaround should do until the problem is resolved.
//
let workaroundTypingAttributes = typingAttributes

super.insertText(text)

// WORKAROUND: this line is related to the workaround above.
//
typingAttributes = workaroundTypingAttributes
preserveTypingAttributesForInsertion {
super.insertText(text)
}

ensureRemovalOfSingleLineParagraphAttributesAfterPressingEnter(input: text)

Expand All @@ -457,12 +453,13 @@ open class TextView: UITextView {

ensureRemovalOfParagraphStylesBeforeRemovingCharacter(at: selectedRange)

preserveTypingAttributes {
preserveTypingAttributesForDeletion {
super.deleteBackward()
}

ensureRemovalOfParagraphAttributesWhenPressingBackspaceAndEmptyingTheDocument()
ensureCursorRedraw(afterEditing: deletedString.string)

delegate?.textViewDidChange?(self)
}

Expand Down Expand Up @@ -946,7 +943,6 @@ open class TextView: UITextView {
func forceRedrawCursorAfterDelay() {
let delay = 0.05
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
let beforeTypingAttributes = self.typingAttributes
let pristine = self.selectedRange
let maxLength = self.storage.length

Expand All @@ -957,25 +953,56 @@ open class TextView: UITextView {
let delta = pristine.location == maxLength ? -1 : 1
let location = min(max(pristine.location + delta, 0), maxLength)

// Shift the SelectedRange to a nearby position: *FORCE* cursor redraw
// Yes. This is a Workaround on top of another workaround.
// WARNING: The universe may fade out of existance.
//
self.selectedRange = NSMakeRange(location, 0)
self.preserveTypingAttributesForInsertion {

// Finally, restore the original SelectedRange and the typingAttributes we had before beginning
//
self.selectedRange = pristine
self.typingAttributes = beforeTypingAttributes
// Shift the SelectedRange to a nearby position: *FORCE* cursor redraw
//
self.selectedRange = NSMakeRange(location, 0)

// Finally, restore the original SelectedRange and the typingAttributes we had before beginning
//
self.selectedRange = pristine
}
}
}

/// Workaround: This method preserves the Typing Attributes, and prevents the UITextView's delegate from beign
/// called during the `block` execution.
///
/// We're implementing this because of a bug in iOS 11, in which Typing Attributes are being lost by methods such as:
///
/// - `deleteBackwards`
/// - `insertText`
/// - Autocompletion!
///
/// Reference: https://github.com/wordpress-mobile/AztecEditor-iOS/issues/748
///
private func preserveTypingAttributesForInsertion(block: () -> Void) {
let beforeTypingAttributes = typingAttributes
let beforeDelegate = delegate

delegate = nil
block()

typingAttributes = beforeTypingAttributes
delegate = beforeDelegate

// Manually notify the delegates: We're avoiding overwork!
delegate?.textViewDidChangeSelection?(self)
delegate?.textViewDidChange?(self)
}


// WORKAROUND: iOS 11 introduced an issue that's causing UITextView to lose it's typing
// attributes under certain circumstances. This method will determine the Typing Attributes based on
/// the TextStorage attributes, whenever possible.
///
/// Issue: https://github.com/wordpress-mobile/AztecEditor-iOS/issues/749
///
private func preserveTypingAttributes(beforeDeletion block: () -> Void) {
private func preserveTypingAttributesForDeletion(block: () -> Void) {
let document = textStorage.string
guard selectedRange.location == document.characters.count else {
block()
Expand Down
22 changes: 22 additions & 0 deletions AztecTests/TextKit/TextViewStubDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import Foundation
import Aztec
import UIKit


// MARK: - Wraps UITextView Delegate methods into callbacks, for Unit Testing purposes.
//
class TextViewStubDelegate: NSObject {

/// Closure to be executed whenever `textViewDidChange` is executed.
///
var onDidChange: (() -> Void)?

}


extension TextViewStubDelegate: UITextViewDelegate {

func textViewDidChange(_ textView: UITextView) {
onDidChange?()
}
}
29 changes: 29 additions & 0 deletions AztecTests/TextKit/TextViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1699,4 +1699,33 @@ class TextViewTests: XCTestCase {

XCTAssertEqual(textView.getHTML(prettyPrint: false), expectedHTML)
}

/// This test verifies that the *ACTUAL* Typing Attributes are retrieved whenever requested from within
/// UITextView's `onDidChange` delegate callback.
///
/// We're doing this because of (multiple) iOS 11 bugs in which Typing Attributes get lost.
///
/// Ref. Issue #748: Format Bar: Active Style gets de-higlighted
///
func testActiveStyleDoesNotGetLostWheneverOnDidChangeDelegateMethodIsCalled() {
let textView = createTextView(withHTML: "")

let delegate = TextViewStubDelegate()
textView.delegate = delegate

textView.toggleBoldface(self)
textView.insertText("Bold")
textView.insertText("\n")

textView.toggleItalics(self)

delegate.onDidChange = {
let identifiers = textView.formatIdentifiersForTypingAttributes()

XCTAssert(identifiers.contains(.bold))
XCTAssert(identifiers.contains(.italic))
}

textView.insertText("Italics")
}
}
5 changes: 3 additions & 2 deletions Example/Example/EditorDemoController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -361,8 +361,9 @@ class EditorDemoController: UIViewController {
guard let toolbar = richTextView.inputAccessoryView as? Aztec.FormatBar else {
return
}
var identifiers = [FormattingIdentifier]()
if (richTextView.selectedRange.length > 0) {

let identifiers: [FormattingIdentifier]
if richTextView.selectedRange.length > 0 {
identifiers = richTextView.formatIdentifiersSpanningRange(richTextView.selectedRange)
} else {
identifiers = richTextView.formatIdentifiersForTypingAttributes()
Expand Down