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..fec33a5dd 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? CoolTextView, 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? CoolTextView, 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..ffb6755b7 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? CoolTextView, 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: CoolTextView, _ 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? CoolTextView, 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: 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) { 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 a6c4c473e..731e01d98 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: CoolTextView 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 = CoolTextView(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,9 +114,8 @@ public class TextDisplayStackView: UIStackView { self.tColor = color self.delegate = delegate self.baseFontColor = baseFontColor - self.firstTextView = YYLabel(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() @@ -168,8 +170,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) { @@ -185,21 +188,19 @@ public class TextDisplayStackView: UIStackView { } firstTextView.attributedText = string - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(string) // 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 } } @@ -253,16 +254,15 @@ public class TextDisplayStackView: UIStackView { } firstTextView.attributedText = newTitle - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(newTitle) // 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 { @@ -309,20 +309,17 @@ public class TextDisplayStackView: UIStackView { // firstTextView.linkAttributes = activeLinkAttributes as NSDictionary as? [AnyHashable: Any] firstTextView.attributedText = newTitle - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(newTitle) // 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 } @@ -422,16 +419,14 @@ public class TextDisplayStackView: UIStackView { } firstTextView.attributedText = text - firstTextView.preferredMaxLayoutWidth = estimatedWidth if !ignoreHeight { // let framesetterB = CTFramesetterCreateWithAttributedString(text) // 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 +488,20 @@ public class TextDisplayStackView: UIStackView { if body.isEmpty { continue } - let label = YYLabel(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 - 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.attributedText = text baseView.addSubview(label) @@ -523,35 +514,31 @@ 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 = CoolTextView(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)) - 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 } } } @@ -566,9 +553,16 @@ 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 htmlString = DTHTMLAttributedStringBuilder.init(html: htmlBase.trimmed().data(using: .unicode)!, options: [DTUseiOS6Attributes: true, DTDefaultTextColor: fontColor, DTDefaultFontSize: font.pointSize], documentAttributes: nil).generatedAttributedString()! + + 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 +575,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? { @@ -741,9 +738,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 { @@ -762,9 +758,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 { @@ -782,11 +777,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 { @@ -840,3 +833,47 @@ 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) + } else { + // Reset superscript level if we hit a chunk with no superscript attribute + superscriptLevel = 0 + } + } + + return mutable + } + + func boundingSize(givenSize size: CGSize) -> CGSize { + return self.boundingRect( + with: size, + options: [.usesLineFragmentOrigin, .usesFontLeading], + context: nil + ).size + } +}