From 3fbdd9ec184b906521c2f85592c87d602232d0a9 Mon Sep 17 00:00:00 2001 From: Jonathan Cole Date: Fri, 31 Jul 2020 12:38:15 -0400 Subject: [PATCH 1/4] Fix superscript when generating NSAttributedString from HTML --- Slide for Reddit/TextDisplayStackView.swift | 68 +++++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/Slide for Reddit/TextDisplayStackView.swift b/Slide for Reddit/TextDisplayStackView.swift index a6c4c473e..b3bba112d 100644 --- a/Slide for Reddit/TextDisplayStackView.swift +++ b/Slide for Reddit/TextDisplayStackView.swift @@ -7,7 +7,6 @@ // import Anchorage -import DTCoreText import Then import UIKit import YYText @@ -566,9 +565,24 @@ public class TextDisplayStackView: UIStackView { public static func createAttributedChunk(baseHTML: String, fontSize: CGFloat, submission: Bool, accentColor: UIColor, fontColor: UIColor, linksCallback: ((URL) -> Void)?, indexCallback: (() -> Int)?) -> NSAttributedString { let font = FontGenerator.fontOfSize(size: fontSize, submission: submission) - let htmlBase = TextDisplayStackView.addSpoilers(baseHTML).replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "").replacingOccurrences(of: "", with: "") - let baseHtml = DTHTMLAttributedStringBuilder.init(html: htmlBase.trimmed().data(using: .unicode)!, options: [DTUseiOS6Attributes: true, DTDefaultTextColor: fontColor, DTDefaultFontSize: font.pointSize], documentAttributes: nil).generatedAttributedString()! - let html = NSMutableAttributedString(attributedString: baseHtml) + let htmlBase = TextDisplayStackView.addSpoilers(baseHTML) + .replacingOccurrences(of: "", with: "") + .replacingOccurrences(of: "", with: "") + .replacingOccurrences(of: "", with: "") + .replacingOccurrences(of: "", with: "") +// let htmlBase = "

WOW whoaaaa yeah

" +// let htmlBase = "

Testwhooo

" +// let htmlBase = "

WOW whabcde yeah

" +// let htmlBase = "

WOW whabc yeah

" + +// let htmlBase = "\(baseHTML)" + + let htmlString = try! NSAttributedString( + data: "\(htmlBase)".data(using: .unicode, allowLossyConversion: false)!, + options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.unicode.rawValue], + documentAttributes: nil) + let html = NSMutableAttributedString(attributedString: htmlString) + while html.mutableString.contains("\t•\t") { let rangeOfStringToBeReplaced = html.mutableString.range(of: "\t•\t") html.replaceCharacters(in: rangeOfStringToBeReplaced, with: " • ") @@ -581,8 +595,11 @@ public class TextDisplayStackView: UIStackView { let rangeOfStringToBeReplaced = html.mutableString.range(of: "\t▪\t") html.replaceCharacters(in: rangeOfStringToBeReplaced, with: " ▪ ") } - - return LinkParser.parse(html, accentColor, font: font, fontColor: fontColor, linksCallback: linksCallback, indexCallback: indexCallback) + + // Repair superscript styling + let fixed = html.fixCoreTextIssues(with: font) + + return LinkParser.parse(fixed, accentColor, font: font, fontColor: fontColor, linksCallback: linksCallback, indexCallback: indexCallback) } // public func link(at: CGPoint, withTouch: UITouch) -> TTTAttributedLabelLink? { @@ -840,3 +857,42 @@ private func convertFromNSAttributedStringKey(_ input: NSAttributedString.Key) - private func convertToNSAttributedStringKeyDictionary(_ input: [String: Any]) -> [NSAttributedString.Key: Any] { return Dictionary(uniqueKeysWithValues: input.map { key, value in (NSAttributedString.Key(rawValue: key), value) }) } + +private extension NSAttributedString { + /** + Fixes the following: + - Nested superscript is rendered incorrectly + */ + func fixCoreTextIssues(with font: UIFont) -> NSAttributedString { + let mutable = NSMutableAttributedString(attributedString: self) + + var superscriptLevel: Int = 0 + + mutable.enumerateAttributes(in: NSRange(location: 0, length: mutable.length), options: .longestEffectiveRangeNotRequired) { (value, range, stop) in + let value = value as [NSAttributedString.Key: Any] + if value[NSAttributedString.Key(rawValue: "NSSuperScript")] != nil || value[NSAttributedString.Key(rawValue: "CTSuperScript")] != nil { // kCTSuperscriptAttributeName + // Extract font from attributed string; this includes bold/italic information + let fontForChunk = value[NSAttributedString.Key.font] as! UIFont + superscriptLevel += 1 + let newFontSize = max(CGFloat(font.pointSize / 2), 10) + let newFont = UIFont(name: fontForChunk.fontName, size: newFontSize) ?? fontForChunk + let newBaseline = (font.pointSize * 0.25) + (CGFloat(superscriptLevel) * (font.pointSize / 4.0)) + + mutable.setAttributes(nil, range: range) + + mutable.addAttributes([ + .font: newFont, + .baselineOffset: newBaseline + ], range: range) + +// mutable.removeAttribute(NSAttributedString.Key(rawValue: "NSSuperScript"), range: range) // Not really necessary. +// mutable.removeAttribute(NSAttributedString.Key(rawValue: "CTSuperScript"), range: range) // Not really necessary. + } else { + // Reset superscript level if we hit a chunk with no superscript attribute + superscriptLevel = 0 + } + } + + return mutable + } +} From cd173b303ca8118f3eba4d3b5b88b935cd8bc6b1 Mon Sep 17 00:00:00 2001 From: Jonathan Cole Date: Fri, 31 Jul 2020 17:49:22 -0400 Subject: [PATCH 2/4] Fix crash bug when minimizing app (NSAttributedString problem) NSAttributedString's method for instantiating from HTML seems to run in the background using a UIWebView. This has to run on the main thread. If it does not, it will eventually lead to a SIGTRAP; in our case, it's when you background the app. We're instead going to keep using DTCoreText. --- Slide for Reddit/TextDisplayStackView.swift | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Slide for Reddit/TextDisplayStackView.swift b/Slide for Reddit/TextDisplayStackView.swift index b3bba112d..4f3252c4b 100644 --- a/Slide for Reddit/TextDisplayStackView.swift +++ b/Slide for Reddit/TextDisplayStackView.swift @@ -7,6 +7,7 @@ // import Anchorage +import DTCoreText import Then import UIKit import YYText @@ -570,17 +571,9 @@ public class TextDisplayStackView: UIStackView { .replacingOccurrences(of: "", with: "") .replacingOccurrences(of: "", with: "") .replacingOccurrences(of: "", with: "") -// let htmlBase = "

WOW whoaaaa yeah

" -// let htmlBase = "

Testwhooo

" -// let htmlBase = "

WOW whabcde yeah

" -// let htmlBase = "

WOW whabc yeah

" -// let htmlBase = "\(baseHTML)" + let htmlString = DTHTMLAttributedStringBuilder.init(html: htmlBase.trimmed().data(using: .unicode)!, options: [DTUseiOS6Attributes: true, DTDefaultTextColor: fontColor, DTDefaultFontSize: font.pointSize], documentAttributes: nil).generatedAttributedString()! - let htmlString = try! NSAttributedString( - data: "\(htmlBase)".data(using: .unicode, allowLossyConversion: false)!, - options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.unicode.rawValue], - documentAttributes: nil) let html = NSMutableAttributedString(attributedString: htmlString) while html.mutableString.contains("\t•\t") { @@ -870,7 +863,7 @@ private extension NSAttributedString { mutable.enumerateAttributes(in: NSRange(location: 0, length: mutable.length), options: .longestEffectiveRangeNotRequired) { (value, range, stop) in let value = value as [NSAttributedString.Key: Any] - if value[NSAttributedString.Key(rawValue: "NSSuperScript")] != nil || value[NSAttributedString.Key(rawValue: "CTSuperScript")] != nil { // kCTSuperscriptAttributeName + if value[NSAttributedString.Key(rawValue: "NSSuperScript")] != nil || value[NSAttributedString.Key(rawValue: "CTSuperscript")] != nil { // kCTSuperscriptAttributeName // Extract font from attributed string; this includes bold/italic information let fontForChunk = value[NSAttributedString.Key.font] as! UIFont superscriptLevel += 1 @@ -884,9 +877,6 @@ private extension NSAttributedString { .font: newFont, .baselineOffset: newBaseline ], range: range) - -// mutable.removeAttribute(NSAttributedString.Key(rawValue: "NSSuperScript"), range: range) // Not really necessary. -// mutable.removeAttribute(NSAttributedString.Key(rawValue: "CTSuperScript"), range: range) // Not really necessary. } else { // Reset superscript level if we hit a chunk with no superscript attribute superscriptLevel = 0 From e4c614bb616033690e0b50877f715ae03a4da7da Mon Sep 17 00:00:00 2001 From: Jonathan Cole Date: Mon, 10 Aug 2020 22:27:33 -0400 Subject: [PATCH 3/4] Remove YYText and totally break the app --- Slide for Reddit.xcodeproj/project.pbxproj | 31 ++++++ Slide for Reddit/CommentDepthCell.swift | 87 ++++++++++------ Slide for Reddit/CoolTextView.swift | 21 ++++ Slide for Reddit/LinkCellView.swift | 14 +-- Slide for Reddit/TextDisplayStackView.swift | 108 ++++++++++---------- 5 files changed, 171 insertions(+), 90 deletions(-) create mode 100644 Slide for Reddit/CoolTextView.swift diff --git a/Slide for Reddit.xcodeproj/project.pbxproj b/Slide for Reddit.xcodeproj/project.pbxproj index 29b93d43e..09fc1dece 100644 --- a/Slide for Reddit.xcodeproj/project.pbxproj +++ b/Slide for Reddit.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 09F48EBC20F652CF00BAC8AC /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F48EBB20F652CF00BAC8AC /* VideoView.swift */; }; 2D3972CE071AEB2AC2811512 /* Pods_Slide_for_RedditTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 733616027CB406046CABF77A /* Pods_Slide_for_RedditTests.framework */; }; 323EFB2821434781005157FA /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323EFB2721434781005157FA /* ProgressBarView.swift */; }; + B4BF517224D4EA23000000D9 /* CoolTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4BF517124D4EA23000000D9 /* CoolTextView.swift */; }; B776B17D5CA92860424F37C0 /* ModQueueContributionLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B776BA7C2B7217C3875F3BEC /* ModQueueContributionLoader.swift */; }; B776B1DF80711B443ED76B61 /* SettingsPro.swift in Sources */ = {isa = PBXBuildFile; fileRef = B776B2AEE4A2438F5803329C /* SettingsPro.swift */; }; B776B1E23F4DEC18069F1579 /* MediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B776BC65995FE8D1EE75E2AB /* MediaViewController.swift */; }; @@ -455,6 +456,7 @@ 5A3C22D1F67B7A388DDF34E6 /* Pods-Slide for Reddit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Slide for Reddit.release.xcconfig"; path = "Pods/Target Support Files/Pods-Slide for Reddit/Pods-Slide for Reddit.release.xcconfig"; sourceTree = ""; }; 733616027CB406046CABF77A /* Pods_Slide_for_RedditTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Slide_for_RedditTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B12412FF705B2BEB9D3B245F /* Pods_Slide_for_Reddit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Slide_for_Reddit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B4BF517124D4EA23000000D9 /* CoolTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoolTextView.swift; sourceTree = ""; }; B776B21CFB79FA354C296037 /* ColorMuxPagingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorMuxPagingViewController.swift; sourceTree = ""; }; B776B2AEE4A2438F5803329C /* SettingsPro.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsPro.swift; sourceTree = ""; }; B776B2E54F7438959A76E42E /* LiveThreadUpdate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveThreadUpdate.swift; sourceTree = ""; }; @@ -1302,6 +1304,34 @@ BE15FE742163D03900288D86 /* TopLockViewController.swift */, BECF52432012AF7A00A310F9 /* VCPresenter.swift */, BE11351B2159390700502C5B /* WatchSessionManager.swift */, + BE17A24F215FFFAA002C6CE6 /* ReadLaterContributionLoader.swift */, + BE17A2512160127C002C6CE6 /* ReadLaterViewController.swift */, + BE15FE742163D03900288D86 /* TopLockViewController.swift */, + BE8264E82194AE0E002A540E /* OfflineOverviewViewController.swift */, + BE25402521CCC03E003BD547 /* ForceTouchGestureRecognizer.swift */, + BE8AE94221F9225000E1C0D1 /* Main.storyboard */, + BE8AE94621F922F400E1C0D1 /* ColorPickerViewController.swift */, + BE47AF70225C574700A61FB0 /* ManageMultireddit.swift */, + BE762C9422A2FF7200B29757 /* DraftFindReturnViewController.swift */, + BE602AF9229F3A5200AF0DC3 /* LinkBubble.swift */, + BE7A2D3D22AD99600005F74F /* DragDownAlertMenu.swift */, + BEC4A1A3232E98CF00EE5114 /* ProfileInfoViewController.swift */, + BEC4A1B1232F3B4F00EE5114 /* collections.plist */, + BEC4A1B3232F3F1800EE5114 /* CollectionsContributionLoader.swift */, + BEC4A1B5232F3F9A00EE5114 /* CollectionsViewController.swift */, + BEC4A1B72331533400EE5114 /* AutoplayScrollViewHandler.swift */, + BEB7BAD923A05F2500E39593 /* InsetTransitioningDelegate.swift */, + BEC814EB24BD43EE005C8E8C /* SiriShortcuts.swift */, + BEC814ED24BE8B94005C8E8C /* HistoryViewController.swift */, + BEC814EF24BE8BBD005C8E8C /* HistoryContributionLoader.swift */, + BE25CAD324C3A28300736CA5 /* SwipeForwardNavigationController.swift */, + BE25CAD524C3A65400736CA5 /* SwipeForwardAnimatedTransitioning.swift */, + BE25CAD924C4B02700736CA5 /* NavigationHomeViewController.swift */, + BE25CADB24C4E3DA00736CA5 /* MainViewController.swift */, + BE25CADD24C4E3FB00736CA5 /* SplitMainViewController.swift */, + BE93DECE24CE4D8300464B64 /* icons.plist */, + BE93DED024CE550800464B64 /* subcolors.plist */, + B4BF517124D4EA23000000D9 /* CoolTextView.swift */, ); path = "Slide for Reddit"; sourceTree = ""; @@ -2154,6 +2184,7 @@ 09526EF8214C399F005A3F6B /* UIPanGestureRecognizer+Utilities.swift in Sources */, B776B69E386227A51C8E10DF /* MediaTableViewController.swift in Sources */, B776B1E23F4DEC18069F1579 /* MediaViewController.swift in Sources */, + B4BF517224D4EA23000000D9 /* CoolTextView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Slide for Reddit/CommentDepthCell.swift b/Slide for Reddit/CommentDepthCell.swift index 22654f934..a965b6cd5 100644 --- a/Slide for Reddit/CommentDepthCell.swift +++ b/Slide for Reddit/CommentDepthCell.swift @@ -148,7 +148,8 @@ class CommentDepthCell: MarginedTableViewCell, UIViewControllerPreviewingDelegat $0.isUserInteractionEnabled = true $0.accessibilityIdentifier = "Comment body" $0.ignoreHeight = true - $0.firstTextView.textContainerInset = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0) + // TODOjon: +// $0.firstTextView.textContainerInset = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0) }) self.title = YYLabel().then({ @@ -2342,8 +2343,8 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { } else if self.commentBody.overflow.frame.contains(location) { let innerLocation = self.commentBody.convert(location, to: self.commentBody.overflow) for view in self.commentBody.overflow.subviews { - if view.frame.contains(innerLocation) && view is YYLabel { - return UITargetedPreview(view: self.commentBody, parameters: self.getLocationForPreviewedText(view as! YYLabel, innerLocation, self.previewedURL?.absoluteString, self.commentBody) ?? parameters) + if let view = view as? UILabel, view.frame.contains(innerLocation) { + return UITargetedPreview(view: self.commentBody, parameters: self.getLocationForPreviewedText(view, innerLocation, self.previewedURL?.absoluteString, self.commentBody) ?? parameters) } } } @@ -2362,8 +2363,8 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { } else if self.commentBody.overflow.frame.contains(location) { let innerLocation = self.commentBody.convert(location, to: self.commentBody.overflow) for view in self.commentBody.overflow.subviews { - if view.frame.contains(innerLocation) && view is YYLabel { - return getConfigurationForTextView(view as! YYLabel, innerLocation) + if let view = view as? UILabel, view.frame.contains(innerLocation) { + return getConfigurationForTextView(view, innerLocation) } } } else if self.commentBody.links.frame.contains(location) { @@ -2380,33 +2381,23 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { self.previewedVC = nil } - func getLocationForPreviewedText(_ label: YYLabel, _ location: CGPoint, _ inputURL: String?, _ changeRectTo: UIView? = nil) -> UIPreviewParameters? { + func getLocationForPreviewedText(_ textView: CoolTextView, _ location: CGPoint, _ inputURL: String?, _ changeRectTo: UIView? = nil) -> UIPreviewParameters? { if inputURL == nil { return nil } - let point = label.superview?.convert(location, to: label) ?? location + // TODOjon: + let point = textView.superview?.convert(location, to: textView) ?? location // Convert touch point from frame space to label space var params: UIPreviewParameters? - if let attributedText = label.attributedText, let layoutManager = YYTextLayout(containerSize: label.frame.size, text: attributedText) { - let locationFinal = layoutManager.textPosition(for: point, lineIndex: layoutManager.lineIndex(for: point)) - if locationFinal < 1000000 { - attributedText.enumerateAttribute( - .link, - in: NSRange(location: 0, length: attributedText.length) - ) { (value, range, _) in - if let url = value as? NSURL { - if url.absoluteString == inputURL! { - let baseRects = layoutManager.selectionRects(for: YYTextRange(range: range)) - var cgs = [NSValue]() - for rect in baseRects { - if changeRectTo != nil { - cgs.append(NSValue(cgRect: changeRectTo!.convert(rect.rect, from: label))) - } else { - cgs.append(NSValue(cgRect: rect.rect)) - } - } - params = UIPreviewParameters(textLineRects: cgs) - params?.backgroundColor = .clear - } + if let attributedText = textView.attributedText { + attributedText.enumerateAttribute( + .link, + in: NSRange(location: 0, length: attributedText.length) + ) { (value, range, _) in + if let url = value as? NSURL { + if url.absoluteString == inputURL! { + let rects = textView.selectionRects(for: range.toTextRange(textInput: textView)!).map {NSValue(cgRect: $0.rect)} + params = UIPreviewParameters(textLineRects: rects) + params?.backgroundColor = .clear } } } @@ -2414,10 +2405,10 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { return params } - func getConfigurationForTextView(_ label: YYLabel, _ location: CGPoint) -> UIContextMenuConfiguration? { - let point = label.superview?.convert(location, to: label) ?? location + func getConfigurationForTextView(_ textView: CoolTextView, _ location: CGPoint) -> UIContextMenuConfiguration? { +// let point = textView.superview?.convert(location, to: label) ?? location - if let attributedText = label.attributedText, let layoutManager = YYTextLayout(containerSize: label.frame.size, text: attributedText) { + if let attributedText = textView.attributedText, let layoutManager = YYTextLayout(containerSize: textView.frame.size, text: attributedText) { let locationFinal = layoutManager.textPosition(for: point, lineIndex: layoutManager.lineIndex(for: point)) if locationFinal < 1000000 { let attributes = attributedText.attributes(at: Int(locationFinal), effectiveRange: nil) @@ -2547,3 +2538,37 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: nil) } } + +extension UILabel { + func boundingRect(forCharacterRange range: NSRange) -> CGRect? { + + guard let attributedText = attributedText else { return nil } + + let textStorage = NSTextStorage(attributedString: attributedText) + let layoutManager = NSLayoutManager() + + textStorage.addLayoutManager(layoutManager) + + let textContainer = NSTextContainer(size: bounds.size) + textContainer.lineFragmentPadding = 0.0 + + layoutManager.addTextContainer(textContainer) + + var glyphRange = NSRange() + + // Convert the range for glyphs. + layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) + + return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + } +} + +extension NSRange { + func toTextRange(textInput: UITextInput) -> UITextRange? { + if let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location), + let rangeEnd = textInput.position(from: rangeStart, offset: length) { + return textInput.textRange(from: rangeStart, to: rangeEnd) + } + return nil + } +} diff --git a/Slide for Reddit/CoolTextView.swift b/Slide for Reddit/CoolTextView.swift new file mode 100644 index 000000000..098f8e9b6 --- /dev/null +++ b/Slide for Reddit/CoolTextView.swift @@ -0,0 +1,21 @@ +// +// CoolTextView.swift +// Slide for Reddit +// +// Created by Jonathan Cole on 7/31/20. +// Copyright © 2020 Haptic Apps. All rights reserved. +// + +import UIKit + +class CoolTextView: UITextView { + + /* + // Only override draw() if you perform custom drawing. + // An empty implementation adversely affects performance during animation. + override func draw(_ rect: CGRect) { + // Drawing code + } + */ + +} diff --git a/Slide for Reddit/LinkCellView.swift b/Slide for Reddit/LinkCellView.swift index 50c45aeee..7c02045f0 100644 --- a/Slide for Reddit/LinkCellView.swift +++ b/Slide for Reddit/LinkCellView.swift @@ -3186,8 +3186,8 @@ extension LinkCellView: UIContextMenuInteractionDelegate { } else if self.textView.overflow.frame.contains(location) { let innerLocation = self.textView.convert(location, to: self.textView.overflow) for view in self.textView.overflow.subviews { - if view.frame.contains(innerLocation) && view is YYLabel { - return UITargetedPreview(view: self.textView, parameters: self.getLocationForPreviewedText(view as! YYLabel, innerLocation, self.previewedURL?.absoluteString, self.textView) ?? parameters) + if let view = view as? UILabel, view.frame.contains(innerLocation) { + return UITargetedPreview(view: self.textView, parameters: self.getLocationForPreviewedText(view, innerLocation, self.previewedURL?.absoluteString, self.textView) ?? parameters) } } } @@ -3203,7 +3203,8 @@ extension LinkCellView: UIContextMenuInteractionDelegate { } } - func getLocationForPreviewedText(_ label: YYLabel, _ location: CGPoint, _ inputURL: String?, _ changeRectTo: UIView? = nil) -> UIPreviewParameters? { + func getLocationForPreviewedText(_ label: UILabel, _ location: CGPoint, _ inputURL: String?, _ changeRectTo: UIView? = nil) -> UIPreviewParameters? { + // TODOjon: if inputURL == nil { return nil } @@ -3247,8 +3248,8 @@ extension LinkCellView: UIContextMenuInteractionDelegate { let innerLocation = self.contentView.convert(innerPoint, to: self.textView.overflow) print(innerLocation) for view in self.textView.overflow.subviews { - if view.frame.contains(innerLocation) && view is YYLabel { - return getConfigurationForTextView(view as! YYLabel, innerLocation) + if let view = view as? UILabel, view.frame.contains(innerLocation) { + return getConfigurationForTextView(view, innerLocation) } } } @@ -3268,7 +3269,8 @@ extension LinkCellView: UIContextMenuInteractionDelegate { return nil } - func getConfigurationForTextView(_ label: YYLabel, _ location: CGPoint) -> UIContextMenuConfiguration? { + func getConfigurationForTextView(_ label: UILabel, _ location: CGPoint) -> UIContextMenuConfiguration? { + // TODOjon: let point = label.superview?.convert(location, to: label) ?? location if let attributedText = label.attributedText, let layoutManager = YYTextLayout(containerSize: label.frame.size, text: attributedText) { let locationFinal = layoutManager.textPosition(for: point, lineIndex: layoutManager.lineIndex(for: point)) diff --git a/Slide for Reddit/TextDisplayStackView.swift b/Slide for Reddit/TextDisplayStackView.swift index 4f3252c4b..ce41da9b8 100644 --- a/Slide for Reddit/TextDisplayStackView.swift +++ b/Slide for Reddit/TextDisplayStackView.swift @@ -12,6 +12,8 @@ import Then import UIKit import YYText +typealias TextAction = (UIView, NSAttributedString, NSRange, CGRect) -> Void + public protocol TextDisplayStackViewDelegate: class { func linkTapped(url: URL, text: String) func linkLongTapped(url: URL) @@ -28,7 +30,7 @@ public class TextDisplayStackView: UIStackView { var estimatedHeight = CGFloat(0) weak var parentLongPress: UILongPressGestureRecognizer? - let firstTextView: YYLabel + let firstTextView: UILabel let overflow: UIStackView let links: UIScrollView @@ -41,8 +43,8 @@ public class TextDisplayStackView: UIStackView { var delegate: TextDisplayStackViewDelegate var ignoreHeight = false - var touchLinkAction: YYTextAction? - var longTouchLinkAction: YYTextAction? + var touchLinkAction: TextAction? + var longTouchLinkAction: TextAction? var activeSet = false @@ -52,7 +54,7 @@ public class TextDisplayStackView: UIStackView { self.tColor = .black self.baseFontColor = .white self.delegate = delegate - self.firstTextView = YYLabel(frame: .zero) + self.firstTextView = UILabel(frame: .zero) self.overflow = UIStackView() self.overflow.isUserInteractionEnabled = true self.links = TouchUIScrollView() @@ -96,8 +98,9 @@ public class TextDisplayStackView: UIStackView { } self.isUserInteractionEnabled = true - self.firstTextView.highlightLongPressAction = longTouchLinkAction - self.firstTextView.highlightTapAction = touchLinkAction + // TODOjon: +// self.firstTextView.highlightLongPressAction = longTouchLinkAction +// self.firstTextView.highlightTapAction = touchLinkAction } func setColor(_ color: UIColor) { @@ -111,7 +114,7 @@ public class TextDisplayStackView: UIStackView { self.tColor = color self.delegate = delegate self.baseFontColor = baseFontColor - self.firstTextView = YYLabel(frame: CGRect.zero).then({ + self.firstTextView = UILabel(frame: CGRect.zero).then({ $0.accessibilityIdentifier = "Top title" $0.numberOfLines = 0 $0.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) @@ -168,8 +171,9 @@ public class TextDisplayStackView: UIStackView { } }) } - self.firstTextView.highlightLongPressAction = longTouchLinkAction - self.firstTextView.highlightTapAction = touchLinkAction + // TODOjon: +// self.firstTextView.highlightLongPressAction = longTouchLinkAction +// self.firstTextView.highlightTapAction = touchLinkAction } required public init(coder: NSCoder) { @@ -192,14 +196,13 @@ public class TextDisplayStackView: UIStackView { // let textSizeB = CTFramesetterSuggestFrameSizeWithConstraints(framesetterB, CFRange(), nil, CGSize.init(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude), nil) // estimatedHeight += textSizeB.height - let size = CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: string)! - firstTextView.textLayout = layout - estimatedHeight += layout.textBoundingSize.height + // TODOjon: + let size = string.boundingSize(givenSize: CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude)) + estimatedHeight += size.height firstTextView.horizontalAnchors == horizontalAnchors firstTextView.removeConstraints(addedConstraints) addedConstraints = batch { - firstTextView.heightAnchor == layout.textBoundingSize.height + firstTextView.heightAnchor == size.height } } @@ -260,9 +263,9 @@ public class TextDisplayStackView: UIStackView { // let textSizeB = CTFramesetterSuggestFrameSizeWithConstraints(framesetterB, CFRange(), nil, CGSize.init(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude), nil) // estimatedHeight += textSizeB.height - let size = CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: newTitle)! - estimatedHeight += layout.textBoundingSize.height + // TODOjon: + let size = newTitle.boundingSize(givenSize: CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude)) + estimatedHeight += size.height } if blocks.count > 1 { @@ -316,13 +319,11 @@ public class TextDisplayStackView: UIStackView { // let textSizeB = CTFramesetterSuggestFrameSizeWithConstraints(framesetterB, CFRange(), nil, CGSize.init(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude), nil) // estimatedHeight += textSizeB.height - let size = CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: newTitle)! - firstTextView.textLayout = layout - estimatedHeight += layout.textBoundingSize.height + let size = newTitle.boundingSize(givenSize: CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude)) + estimatedHeight += size.height firstTextView.removeConstraints(addedConstraints) addedConstraints = batch { - firstTextView.heightAnchor == layout.textBoundingSize.height + firstTextView.heightAnchor == size.height } firstTextView.horizontalAnchors == horizontalAnchors } @@ -429,9 +430,8 @@ public class TextDisplayStackView: UIStackView { // let textSizeB = CTFramesetterSuggestFrameSizeWithConstraints(framesetterB, CFRange(), nil, CGSize.init(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude), nil) // estimatedHeight += textSizeB.height - let size = CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: text)! - estimatedHeight += layout.textBoundingSize.height + let size = text.boundingSize(givenSize: CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude)) + estimatedHeight += size.height } startIndex = 1 @@ -493,24 +493,23 @@ public class TextDisplayStackView: UIStackView { if body.isEmpty { continue } - let label = YYLabel(frame: .zero) + let label = UILabel(frame: .zero) label.accessibilityIdentifier = "Quote" let text = createAttributedChunk(baseHTML: body, accent: tColor, linksCallback: linksCallback, indexCallback: indexCallback) label.alpha = 0.7 label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping - label.highlightLongPressAction = longTouchLinkAction - label.highlightTapAction = touchLinkAction + // TODOjon: +// label.highlightLongPressAction = longTouchLinkAction +// label.highlightTapAction = touchLinkAction let baseView = UIView() baseView.accessibilityIdentifier = "Quote box view" label.setBorder(border: .left, weight: 2, color: tColor) - let size = CGSize(width: estimatedWidth - 12, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: text)! - estimatedHeight += layout.textBoundingSize.height - label.textLayout = layout - label.preferredMaxLayoutWidth = layout.textBoundingSize.width + let size = text.boundingSize(givenSize: CGSize(width: estimatedWidth - 12, height: CGFloat.greatestFiniteMagnitude)) + estimatedHeight += size.height + label.preferredMaxLayoutWidth = size.width label.attributedText = text baseView.addSubview(label) @@ -523,35 +522,34 @@ public class TextDisplayStackView: UIStackView { baseView.horizontalAnchors == overflow.horizontalAnchors if !ignoreHeight { - baseView.heightAnchor == layout.textBoundingSize.height + baseView.heightAnchor == size.height } } else { if block.trimmed().isEmpty || block.trimmed() == "\n" { continue } let text = createAttributedChunk(baseHTML: block.trimmed(), accent: tColor, linksCallback: linksCallback, indexCallback: indexCallback) - let label = YYLabel(frame: CGRect.zero).then { + let label = UILabel(frame: CGRect.zero).then { $0.accessibilityIdentifier = "Paragraph" $0.numberOfLines = 0 $0.lineBreakMode = .byWordWrapping $0.attributedText = text $0.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) } - label.highlightLongPressAction = longTouchLinkAction - label.highlightTapAction = touchLinkAction + // TODOjon: +// label.highlightLongPressAction = longTouchLinkAction +// label.highlightTapAction = touchLinkAction - let size = CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: text)! - label.textLayout = layout - label.preferredMaxLayoutWidth = layout.textBoundingSize.width + let size = text.boundingSize(givenSize: CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude)) + label.preferredMaxLayoutWidth = size.width - estimatedHeight += layout.textBoundingSize.height + estimatedHeight += size.height overflow.addArrangedSubview(label) label.horizontalAnchors == overflow.horizontalAnchors if !ignoreHeight { - label.heightAnchor == layout.textBoundingSize.height + label.heightAnchor == size.height } } } @@ -751,9 +749,8 @@ public class TextDisplayStackView: UIStackView { startIndex = 1 } - let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: newTitle)! - totalHeight += layout.textBoundingSize.height + let size = newTitle.boundingSize(givenSize: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) + totalHeight += size.height if blocks.count > 1 { if startIndex == 0 { @@ -772,9 +769,8 @@ public class TextDisplayStackView: UIStackView { } } - let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: newTitle)! - totalHeight += layout.textBoundingSize.height + let size = newTitle.boundingSize(givenSize: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) + totalHeight += size.height } for block in blocks { @@ -792,11 +788,9 @@ public class TextDisplayStackView: UIStackView { totalHeight += body.globalHeight } else { let text = createAttributedChunk(baseHTML: block, fontSize: fontSize, submission: submission, accentColor: .white, fontColor: .white, linksCallback: nil, indexCallback: nil) - let size = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude) - let layout = YYTextLayout(containerSize: size, text: text)! - let textSize = layout.textBoundingSize + let size = text.boundingSize(givenSize: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)) - totalHeight += textSize.height + totalHeight += size.height } } if hasLinks && !SettingValues.disablePreviews { @@ -885,4 +879,12 @@ private extension NSAttributedString { return mutable } + + func boundingSize(givenSize size: CGSize) -> CGSize { + return self.boundingRect( + with: size, + options: [.usesLineFragmentOrigin, .usesFontLeading], + context: nil + ).size + } } From 9fdde509cc5f0687a0add17948cbc0072d0b5362 Mon Sep 17 00:00:00 2001 From: ccrama Date: Mon, 10 Aug 2020 21:53:42 -0500 Subject: [PATCH 4/4] Initial UILabel cleanup --- Slide for Reddit/CommentDepthCell.swift | 4 ++-- Slide for Reddit/LinkCellView.swift | 8 ++++---- Slide for Reddit/TextDisplayStackView.swift | 21 +++++---------------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Slide for Reddit/CommentDepthCell.swift b/Slide for Reddit/CommentDepthCell.swift index a965b6cd5..fec33a5dd 100644 --- a/Slide for Reddit/CommentDepthCell.swift +++ b/Slide for Reddit/CommentDepthCell.swift @@ -2343,7 +2343,7 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { } else if self.commentBody.overflow.frame.contains(location) { let innerLocation = self.commentBody.convert(location, to: self.commentBody.overflow) for view in self.commentBody.overflow.subviews { - if let view = view as? UILabel, view.frame.contains(innerLocation) { + if let view = view as? CoolTextView, view.frame.contains(innerLocation) { return UITargetedPreview(view: self.commentBody, parameters: self.getLocationForPreviewedText(view, innerLocation, self.previewedURL?.absoluteString, self.commentBody) ?? parameters) } } @@ -2363,7 +2363,7 @@ extension CommentDepthCell: UIContextMenuInteractionDelegate { } else if self.commentBody.overflow.frame.contains(location) { let innerLocation = self.commentBody.convert(location, to: self.commentBody.overflow) for view in self.commentBody.overflow.subviews { - if let view = view as? UILabel, view.frame.contains(innerLocation) { + if let view = view as? CoolTextView, view.frame.contains(innerLocation) { return getConfigurationForTextView(view, innerLocation) } } diff --git a/Slide for Reddit/LinkCellView.swift b/Slide for Reddit/LinkCellView.swift index 7c02045f0..ffb6755b7 100644 --- a/Slide for Reddit/LinkCellView.swift +++ b/Slide for Reddit/LinkCellView.swift @@ -3186,7 +3186,7 @@ extension LinkCellView: UIContextMenuInteractionDelegate { } else if self.textView.overflow.frame.contains(location) { let innerLocation = self.textView.convert(location, to: self.textView.overflow) for view in self.textView.overflow.subviews { - if let view = view as? UILabel, view.frame.contains(innerLocation) { + if let view = view as? CoolTextView, view.frame.contains(innerLocation) { return UITargetedPreview(view: self.textView, parameters: self.getLocationForPreviewedText(view, innerLocation, self.previewedURL?.absoluteString, self.textView) ?? parameters) } } @@ -3203,7 +3203,7 @@ extension LinkCellView: UIContextMenuInteractionDelegate { } } - func getLocationForPreviewedText(_ label: UILabel, _ location: CGPoint, _ inputURL: String?, _ changeRectTo: UIView? = nil) -> UIPreviewParameters? { + func getLocationForPreviewedText(_ label: CoolTextView, _ location: CGPoint, _ inputURL: String?, _ changeRectTo: UIView? = nil) -> UIPreviewParameters? { // TODOjon: if inputURL == nil { return nil @@ -3248,7 +3248,7 @@ extension LinkCellView: UIContextMenuInteractionDelegate { let innerLocation = self.contentView.convert(innerPoint, to: self.textView.overflow) print(innerLocation) for view in self.textView.overflow.subviews { - if let view = view as? UILabel, view.frame.contains(innerLocation) { + if let view = view as? CoolTextView, view.frame.contains(innerLocation) { return getConfigurationForTextView(view, innerLocation) } } @@ -3269,7 +3269,7 @@ extension LinkCellView: UIContextMenuInteractionDelegate { return nil } - func getConfigurationForTextView(_ label: UILabel, _ location: CGPoint) -> UIContextMenuConfiguration? { + func getConfigurationForTextView(_ label: CoolTextView, _ location: CGPoint) -> UIContextMenuConfiguration? { // TODOjon: let point = label.superview?.convert(location, to: label) ?? location if let attributedText = label.attributedText, let layoutManager = YYTextLayout(containerSize: label.frame.size, text: attributedText) { diff --git a/Slide for Reddit/TextDisplayStackView.swift b/Slide for Reddit/TextDisplayStackView.swift index ce41da9b8..731e01d98 100644 --- a/Slide for Reddit/TextDisplayStackView.swift +++ b/Slide for Reddit/TextDisplayStackView.swift @@ -30,7 +30,7 @@ public class TextDisplayStackView: UIStackView { var estimatedHeight = CGFloat(0) weak var parentLongPress: UILongPressGestureRecognizer? - let firstTextView: UILabel + let firstTextView: CoolTextView let overflow: UIStackView let links: UIScrollView @@ -54,7 +54,7 @@ public class TextDisplayStackView: UIStackView { self.tColor = .black self.baseFontColor = .white self.delegate = delegate - self.firstTextView = UILabel(frame: .zero) + self.firstTextView = CoolTextView(frame: .zero) self.overflow = UIStackView() self.overflow.isUserInteractionEnabled = true self.links = TouchUIScrollView() @@ -114,9 +114,8 @@ public class TextDisplayStackView: UIStackView { self.tColor = color self.delegate = delegate self.baseFontColor = baseFontColor - self.firstTextView = UILabel(frame: CGRect.zero).then({ + self.firstTextView = CoolTextView(frame: CGRect.zero).then({ $0.accessibilityIdentifier = "Top title" - $0.numberOfLines = 0 $0.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) }) self.links = TouchUIScrollView() @@ -189,7 +188,6 @@ public class TextDisplayStackView: UIStackView { } firstTextView.attributedText = string - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(string) @@ -256,7 +254,6 @@ public class TextDisplayStackView: UIStackView { } firstTextView.attributedText = newTitle - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(newTitle) @@ -312,7 +309,6 @@ public class TextDisplayStackView: UIStackView { // firstTextView.linkAttributes = activeLinkAttributes as NSDictionary as? [AnyHashable: Any] firstTextView.attributedText = newTitle - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(newTitle) @@ -423,7 +419,6 @@ public class TextDisplayStackView: UIStackView { } firstTextView.attributedText = text - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(text) @@ -493,12 +488,10 @@ public class TextDisplayStackView: UIStackView { if body.isEmpty { continue } - let label = UILabel(frame: .zero) + let label = CoolTextView(frame: .zero) label.accessibilityIdentifier = "Quote" let text = createAttributedChunk(baseHTML: body, accent: tColor, linksCallback: linksCallback, indexCallback: indexCallback) label.alpha = 0.7 - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping // TODOjon: // label.highlightLongPressAction = longTouchLinkAction // label.highlightTapAction = touchLinkAction @@ -509,7 +502,6 @@ public class TextDisplayStackView: UIStackView { let size = text.boundingSize(givenSize: CGSize(width: estimatedWidth - 12, height: CGFloat.greatestFiniteMagnitude)) estimatedHeight += size.height - label.preferredMaxLayoutWidth = size.width label.attributedText = text baseView.addSubview(label) @@ -529,10 +521,8 @@ public class TextDisplayStackView: UIStackView { continue } let text = createAttributedChunk(baseHTML: block.trimmed(), accent: tColor, linksCallback: linksCallback, indexCallback: indexCallback) - let label = UILabel(frame: CGRect.zero).then { + let label = CoolTextView(frame: CGRect.zero).then { $0.accessibilityIdentifier = "Paragraph" - $0.numberOfLines = 0 - $0.lineBreakMode = .byWordWrapping $0.attributedText = text $0.setContentCompressionResistancePriority(UILayoutPriority.required, for: .vertical) } @@ -541,7 +531,6 @@ public class TextDisplayStackView: UIStackView { // label.highlightTapAction = touchLinkAction let size = text.boundingSize(givenSize: CGSize(width: estimatedWidth, height: CGFloat.greatestFiniteMagnitude)) - label.preferredMaxLayoutWidth = size.width estimatedHeight += size.height