diff --git a/Aztec.xcodeproj/project.pbxproj b/Aztec.xcodeproj/project.pbxproj index 80414a78c..1e4ceee74 100644 --- a/Aztec.xcodeproj/project.pbxproj +++ b/Aztec.xcodeproj/project.pbxproj @@ -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 */; }; @@ -207,6 +208,7 @@ 59FEA06D1D8BDFA700D138DF /* InNodeConverterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InNodeConverterTests.swift; sourceTree = ""; }; B50CE7311F1FA6260018CAA1 /* NSAttributedString+Strip.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Strip.swift"; sourceTree = ""; }; 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 = ""; }; B524228C1F30C039002E7C6C /* HTMLDiv.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLDiv.swift; sourceTree = ""; }; B524228E1F30C098002E7C6C /* HTMLDivFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLDivFormatter.swift; sourceTree = ""; }; B5375F461EC2566200F5D7EC /* String+HTML.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; @@ -631,6 +633,7 @@ children = ( B5D575841F2288E2003A62F6 /* TextStorageTests.swift */, B5D575851F2288E2003A62F6 /* TextViewStubAttachmentDelegate.swift */, + B52220D21F86A05400D7E092 /* TextViewStubDelegate.swift */, B5D575861F2288E2003A62F6 /* TextViewTests.swift */, B5D575801F226FF4003A62F6 /* UnsupportedHTMLTests.swift */, B5D575821F22820A003A62F6 /* HTMLRepresentationTests.swift */, @@ -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 */, diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index 60a532355..518696be1 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -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) @@ -457,12 +453,13 @@ open class TextView: UITextView { ensureRemovalOfParagraphStylesBeforeRemovingCharacter(at: selectedRange) - preserveTypingAttributes { + preserveTypingAttributesForDeletion { super.deleteBackward() } ensureRemovalOfParagraphAttributesWhenPressingBackspaceAndEmptyingTheDocument() ensureCursorRedraw(afterEditing: deletedString.string) + delegate?.textViewDidChange?(self) } @@ -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 @@ -957,17 +953,48 @@ 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 @@ -975,7 +1002,7 @@ open class TextView: UITextView { /// /// 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() diff --git a/AztecTests/TextKit/TextViewStubDelegate.swift b/AztecTests/TextKit/TextViewStubDelegate.swift new file mode 100644 index 000000000..e0c7d5575 --- /dev/null +++ b/AztecTests/TextKit/TextViewStubDelegate.swift @@ -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?() + } +} diff --git a/AztecTests/TextKit/TextViewTests.swift b/AztecTests/TextKit/TextViewTests.swift index 5270e6deb..4e89d0853 100644 --- a/AztecTests/TextKit/TextViewTests.swift +++ b/AztecTests/TextKit/TextViewTests.swift @@ -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") + } } diff --git a/Example/Example/EditorDemoController.swift b/Example/Example/EditorDemoController.swift index c5a51e555..ede79bcc5 100644 --- a/Example/Example/EditorDemoController.swift +++ b/Example/Example/EditorDemoController.swift @@ -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()