Update StyledText with new highlight API (#1844)

This commit is contained in:
Ryan Nystrom
2018-05-30 22:28:32 -04:00
committed by GitHub
parent f90eab44fe
commit f8b8a977d6
33 changed files with 4518 additions and 4274 deletions

View File

@@ -55,7 +55,7 @@ final class BookmarkCell: SwipeSelectableCell {
layoutContentViewForSafeAreaInsets()
let bounds = contentView.bounds
textView.reposition(width: bounds.width)
textView.reposition(for: bounds.width)
let textViewFrame = textView.frame
@@ -75,7 +75,7 @@ final class BookmarkCell: SwipeSelectableCell {
func configure(viewModel: BookmarkViewModel, height: CGFloat) {
imageView.image = viewModel.bookmark.type.icon.withRenderingMode(.alwaysTemplate)
textView.configure(renderer: viewModel.text, width: contentView.bounds.width)
textView.configure(with: viewModel.text, width: contentView.bounds.width)
// set "Owner/Repo #123" on the detail label if issue/PR, otherwise clear and collapse it
switch viewModel.bookmark.type {

View File

@@ -32,7 +32,7 @@ final class BookmarkSectionController: ListGenericSectionController<BookmarkView
return CGSize(
width: width,
height: max(object.text.viewSize(width: width).height, Styles.Sizes.tableCellHeightLarge)
height: max(object.text.viewSize(in: width).height, Styles.Sizes.tableCellHeightLarge)
)
}

View File

@@ -21,7 +21,7 @@ final class IssueTargetBranchSectionController: ListSectionController {
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.insetContainerSize.width else { fatalError("Collection context must be set") }
return CGSize(width: width, height: self.object?.targetBranchText.viewSize(width: width).height ?? 0)
return CGSize(width: width, height: self.object?.targetBranchText.viewSize(in: width).height ?? 0)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {

View File

@@ -68,7 +68,7 @@ final class IssueCommentCodeBlockCell: IssueCommentBaseCell, ListBindable {
let contentSize = viewModel.contentSize
scrollView.contentSize = contentSize
textView.configure(renderer: viewModel.code, width: 0)
textView.configure(with: viewModel.code, width: 0)
}
}

View File

@@ -26,7 +26,7 @@ final class IssueCommentCodeBlockModel: NSObject, ListDiffable {
// MARK: Public API
var contentSize: CGSize {
return code.viewSize(width: 0)
return code.viewSize(in: 0)
}
// MARK: ListDiffable

View File

@@ -74,7 +74,10 @@ private extension TextElement {
])
children.build(builder, options: options, context: context)
case .link(let children, _, let url):
var attributes: [NSAttributedStringKey: Any] = [.foregroundColor: Styles.Colors.Blue.medium.color]
var attributes: [NSAttributedStringKey: Any] = [
.foregroundColor: Styles.Colors.Blue.medium.color,
.highlight: true
]
if let shortlink = url?.shortlinkInfo {
attributes[MarkdownAttribute.issue] = IssueDetailsModel(
owner: shortlink.owner,

View File

@@ -34,7 +34,7 @@ func fillBuckets(
bucket.rows.append(IssueCommentTableModel.Row(string: cell, fill: fill))
// adjust the max width of each column using whatever is the largest so all cells are the same width
let size = cell.viewSize(width: 0)
let size = cell.viewSize(in: 0)
bucket.maxWidth = max(bucket.maxWidth, size.width)
maxHeight = max(maxHeight, size.height)
}

View File

@@ -40,7 +40,7 @@ final class IssueCommentQuoteCell: IssueCommentBaseCell, ListBindable {
override func layoutSubviews() {
super.layoutSubviews()
textView.reposition(width: contentView.bounds.width)
textView.reposition(for: contentView.bounds.width)
for (i, border) in borders.enumerated() {
border.frame = CGRect(
x: Styles.Sizes.commentGutter + (IssueCommentQuoteCell.borderWidth + Styles.Sizes.columnSpacing) * CGFloat(i),
@@ -72,7 +72,7 @@ final class IssueCommentQuoteCell: IssueCommentBaseCell, ListBindable {
contentView.addSubview(border)
borders.append(border)
}
textView.configure(renderer: viewModel.string, width: contentView.bounds.width)
textView.configure(with: viewModel.string, width: contentView.bounds.width)
setNeedsLayout()
}

View File

@@ -40,7 +40,7 @@ final class IssueCommentTableCollectionCell: UICollectionViewCell {
// MARK: Public API
func configure(_ model: StyledTextRenderer) {
textView.configure(renderer: model, width: 0)
textView.configure(with: model, width: 0)
}
func setRightBorder(visible: Bool) {

View File

@@ -13,7 +13,7 @@ import StyledText
final class IssueCommentTextCell: IssueCommentBaseCell, ListBindable {
static let inset = UIEdgeInsets(
top: 0,
top: 2,
left: Styles.Sizes.commentGutter,
bottom: Styles.Sizes.rowSpacing,
right: Styles.Sizes.commentGutter
@@ -33,7 +33,7 @@ final class IssueCommentTextCell: IssueCommentBaseCell, ListBindable {
override func layoutSubviews() {
super.layoutSubviews()
textView.reposition(width: contentView.bounds.width)
textView.reposition(for: contentView.bounds.width)
}
// MARK: Accessibility
@@ -47,7 +47,7 @@ final class IssueCommentTextCell: IssueCommentBaseCell, ListBindable {
func bindViewModel(_ viewModel: Any) {
guard let viewModel = viewModel as? StyledTextRenderer else { return }
textView.configure(renderer: viewModel, width: contentView.bounds.width)
textView.configure(with: viewModel, width: contentView.bounds.width)
}
}

View File

@@ -49,8 +49,8 @@ final class IssueDiffHunkPreviewCell: IssueCommentBaseCell, ListBindable {
guard let viewModel = viewModel as? StyledTextRenderer else { return }
let width: CGFloat = 0
scrollView.contentSize = viewModel.viewSize(width: width)
textView.configure(renderer: viewModel, width: width)
scrollView.contentSize = viewModel.viewSize(in: width)
textView.configure(with: viewModel, width: width)
}
}

View File

@@ -39,7 +39,7 @@ final class IssueDiffHunkSectionController: ListBindingSectionController<IssueDi
let width = (collectionContext?.insetContainerSize.width ?? 0) - inset.left - inset.right
let height: CGFloat
if let viewModel = viewModel as? StyledTextRenderer {
height = viewModel.viewSize(width: 0).height
height = viewModel.viewSize(in: 0).height
} else {
height = Styles.Sizes.labelEventHeight
}

View File

@@ -16,14 +16,14 @@ func BodyHeightForComment(
imageCache: ImageCellHeightCache?
) -> CGFloat {
if let viewModel = viewModel as? StyledTextRenderer {
return viewModel.viewSize(width: width).height
return viewModel.viewSize(in: width).height
} else if let viewModel = viewModel as? IssueCommentCodeBlockModel {
let inset = IssueCommentCodeBlockCell.scrollViewInset
return viewModel.contentSize.height + inset.top + inset.bottom
} else if let viewModel = viewModel as? IssueCommentImageModel {
return imageCache?.height(model: viewModel, width: width) ?? 200
} else if let viewModel = viewModel as? IssueCommentQuoteModel {
return viewModel.string.viewSize(width: width).height
return viewModel.string.viewSize(in: width).height
} else if viewModel is IssueCommentHrModel {
return 3.0 + IssueCommentHrCell.inset.top + IssueCommentHrCell.inset.bottom
} else if let cache = webviewCache, let viewModel = viewModel as? IssueCommentHtmlModel {

View File

@@ -20,7 +20,7 @@ final class IssueLabeledSectionController: ListGenericSectionController<IssueLab
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.insetContainerSize.width else { fatalError("Collection context must be set") }
return CGSize(width: width, height: object?.string.viewSize(width: width).height ?? 0)
return CGSize(width: width, height: object?.string.viewSize(in: width).height ?? 0)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {

View File

@@ -17,7 +17,7 @@ final class IssueMilestoneEventSectionController: ListGenericSectionController<I
else { fatalError("Collection context must be set") }
return CGSize(
width: width,
height: object.string.viewSize(width: width).height
height: object.string.viewSize(in: width).height
)
}

View File

@@ -24,7 +24,7 @@ final class IssueReferencedSectionController: ListGenericSectionController<Issue
else { fatalError("Missing context") }
return CGSize(
width: width,
height: object.string.viewSize(width: width).height
height: object.string.viewSize(in: width).height
)
}

View File

@@ -17,7 +17,7 @@ final class IssueReferencedCommitSectionController: ListGenericSectionController
else { fatalError("Missing context") }
return CGSize(
width: width,
height: object.string.viewSize(width: width).height
height: object.string.viewSize(in: width).height
)
}

View File

@@ -58,7 +58,7 @@ final class IssueRenamedCell: UICollectionViewCell {
override func layoutSubviews() {
super.layoutSubviews()
layoutContentViewForSafeAreaInsets()
titleView.reposition(width: contentView.bounds.width)
titleView.reposition(for: contentView.bounds.width)
}
// MARK: Private API
@@ -87,7 +87,7 @@ final class IssueRenamedCell: UICollectionViewCell {
dateLabel.setText(date: model.date)
titleView.configure(renderer: model.titleChangeString, width: contentView.bounds.width)
titleView.configure(with: model.titleChangeString, width: contentView.bounds.width)
}
}

View File

@@ -15,7 +15,7 @@ final class IssueRenamedSectionController: ListGenericSectionController<IssueRen
guard let width = collectionContext?.insetContainerSize.width,
let object = self.object
else { fatalError("Missing context") }
return CGSize(width: width, height: object.titleChangeString.viewSize(width: width).height)
return CGSize(width: width, height: object.titleChangeString.viewSize(in: width).height)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {

View File

@@ -17,7 +17,7 @@ final class IssueRequestSectionController: ListGenericSectionController<IssueReq
else { fatalError("Collection context must be set") }
return CGSize(
width: width,
height: object.string.viewSize(width: width).height
height: object.string.viewSize(in: width).height
)
}

View File

@@ -28,7 +28,7 @@ final class IssueTitleSectionController: ListSectionController {
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.insetContainerSize.width
else { fatalError("Collection context must be set") }
return CGSize(width: width, height: self.object?.string.viewSize(width: width).height ?? 0)
return CGSize(width: width, height: self.object?.string.viewSize(in: width).height ?? 0)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {

View File

@@ -104,7 +104,7 @@ final class NotificationCell: SwipeSelectableCell {
override func layoutSubviews() {
super.layoutSubviews()
layoutContentViewForSafeAreaInsets()
textView.reposition(width: contentView.bounds.width)
textView.reposition(for: contentView.bounds.width)
}
// MARK: Public API
@@ -131,7 +131,7 @@ final class NotificationCell: SwipeSelectableCell {
}
titleLabel.attributedText = title
textView.configure(renderer: viewModel.title, width: contentView.bounds.width)
textView.configure(with: viewModel.title, width: contentView.bounds.width)
dateLabel.setText(date: viewModel.date)
reasonImageView.image = viewModel.type.icon.withRenderingMode(.alwaysTemplate)

View File

@@ -26,7 +26,7 @@ SwipeCollectionViewCellDelegate {
else { fatalError("Collection context must be set") }
return CGSize(
width: width,
height: max(ceil(object?.title.viewSize(width: width).height ?? 0), NotificationCell.minHeight)
height: max(ceil(object?.title.viewSize(in: width).height ?? 0), NotificationCell.minHeight)
)
}

View File

@@ -76,7 +76,7 @@ final class RepositorySummaryCell: SelectableCell {
override func layoutSubviews() {
super.layoutSubviews()
titleView.reposition(width: contentView.bounds.width)
titleView.reposition(for: contentView.bounds.width)
resizeLabelListView(labels: labelListView.labels, cacheKey: labelListView.labels.reduce("", {$0 + $1.name}))
}
@@ -94,7 +94,7 @@ final class RepositorySummaryCell: SelectableCell {
// MARK: Public API
func configure(_ model: RepositoryIssueSummaryModel) {
titleView.configure(renderer: model.title, width: contentView.bounds.width)
titleView.configure(with: model.title, width: contentView.bounds.width)
let format = NSLocalizedString("#%d opened %@ by %@", comment: "")
secondaryLabel.text = String(format: format, arguments: [model.number, model.created.agoString(.long), model.author])

View File

@@ -34,7 +34,7 @@ final class RepositorySummarySectionController: ListGenericSectionController<Rep
return labelListViewHeight + Styles.Sizes.rowSpacing
}()
let height = object.title.viewSize(width: width).height
let height = object.title.viewSize(in: width).height
+ Styles.Text.secondary.preferredFont.lineHeight
+ Styles.Sizes.rowSpacing
+ labelListViewHeightAndSpacing

View File

@@ -18,6 +18,8 @@ class StyledTextViewCell: UICollectionViewCell {
textView.gesturableAttributes = MarkdownAttribute.all
contentView.addSubview(textView)
isAccessibilityElement = true
// clipsToBounds = false
// textView.clipsToBounds = false
}
required init?(coder aDecoder: NSCoder) {
@@ -27,7 +29,7 @@ class StyledTextViewCell: UICollectionViewCell {
override func layoutSubviews() {
super.layoutSubviews()
layoutContentViewForSafeAreaInsets()
textView.reposition(width: contentView.bounds.width)
textView.reposition(for: contentView.bounds.width)
}
override var accessibilityLabel: String? {
@@ -47,7 +49,7 @@ class StyledTextViewCell: UICollectionViewCell {
// MARK: Public API
final func set(renderer: StyledTextRenderer) {
textView.configure(renderer: renderer, width: contentView.bounds.width)
textView.configure(with: renderer, width: contentView.bounds.width)
}
}

View File

@@ -191,7 +191,7 @@ CHECKOUT OPTIONS:
:commit: b39c89ea688b79cc8daeb29c214a925c9a1c4396
:git: https://github.com/GitHawkApp/MessageViewController.git
StyledText:
:commit: c287ae538f3d233e1196c9b56dd67a841f944b30
:commit: 5a1597f7513610b59663b3be29d6352732968b82
:git: https://github.com/GitHawkApp/StyledText.git
SPEC CHECKSUMS:

2
Pods/Manifest.lock generated
View File

@@ -191,7 +191,7 @@ CHECKOUT OPTIONS:
:commit: b39c89ea688b79cc8daeb29c214a925c9a1c4396
:git: https://github.com/GitHawkApp/MessageViewController.git
StyledText:
:commit: c287ae538f3d233e1196c9b56dd67a841f944b30
:commit: 5a1597f7513610b59663b3be29d6352732968b82
:git: https://github.com/GitHawkApp/StyledText.git
SPEC CHECKSUMS:

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,136 @@
COMING SOON
<p align="center">
<img src="/images/styledtext.png" />
</p>
`StyledText` is a declarative building and fast rendering attributed string library. It serves as a replacement to `NSAttributedString` and `UILabel` for background-thread size and bitmap caching.
## Features
- Declarative attributed string building API
- Find text sizes on a background thread without sanitizer warnings
- Cache rendered text bitmaps for improved performance
- Custom attribute interaction handling (link taps, etc)
## Installation
Just add `StyledText` to your Podfile and install. Done!
```ruby
pod 'StyledText'
```
## Usage
### Building `NSAttributedString`s
`StyledText` lets you build complex `NSAttributedString`s by combining:
- Add `NSAttributedString`s or `String`s and continue using the previous attributes, saving you from repetitive `.font` and `.foregroundColor` styling
- Intermix complex font traits like **bold** and _italics_ to get _**bold italics**_.
- Handle dynamic text size at string render time. Let's you build the string once and re-render it on device text-size changes.
- Call `save()` and `restore()` to push/pop style settings, letting you build complex test styles without complex code.
```swift
StyledTextBuilder(text: "Foo ")
.save()
.add(text: "bar", traits: [.traitBold])
.restore()
.add(text: " baz!")
.build()
.render(contentSizeCategory: .large)
```
> Foo **bar** baz!
The basic steps are:
- Create a `StyledTextBuilder`
- Add `StyledText` objects
- Call `build()` when finished to generate a `StyledTextString` object
- Call `render(contentSizeCategory:)` to create an `NSAttributedString`
### Rendering Text Bitmaps
Create a `StyledTextRenderer` for sizing and rendering text by initialize it with a `StyledTextString` and a `UIContentSizeCategory`.
```swift
let renderer = StyledTextRenderer(
string: string,
contentSizeCategory: .large
)
```
Once created, you can easily get the size of the text constrained to a width:
```swift
let size = renderer.size(in: 320)
```
You can also get a bitmap of the text:
```swift
let result = renderer.render(for: 320)
view.layer.contents = result.image
```
### StyledTextView
To make rendering and layout of text in a `UIView` simpler, use `StyledTextView` to manage display as well as interactions. All you need is a `StyledTextRenderer` and a width and you're set!
```swift
let view = StyledTextView()
view.configure(with: renderer, width: 320)
```
Set a delegate on the view to handle tap and long presses:
```swift
view.delegate = self
// StyledTextViewDelegate
func didTap(view: StyledTextView, attributes: [NSAttributedStringKey: Any], point: CGPoint) {
guard let link = attributes[.link] else { return }
show(SFSafariViewController(url: link))
}
```
## Background Rendering
`StyledText` exists to do background sizing and rendering of text content so that scrolling large amounts of text is buttery smooth. The typical pipeline to do this is:
1. Get the current width and `UIContentSizeCategory`
2. Go to a background queue
3. Build text
4. Warm caches
5. Return to the main queue
6. Configure your views
```swift
// ViewController.swift
let width = view.bounds.width
let contentSizeCategory = UIApplication.shared.preferredContentSizeCategory
DispatchQueue.global().async {
let builder = StyledTextBuilder(...)
let renderer = StyledTextRenderer(text: builder.build(), contentSizeCategory: contentSizeCategory)
.warm() // warms the size cache
DispatchQueue.main.async {
self.textView.configure(with: renderer, width: width)
}
}
```
## FAQ
> Why not use `UITextView`?
Prior to iOS 7, `UITextView` just used WebKit under the hood and was terribly slow. Now that it uses TextKit, it's significantly faster but still requires all sizing and rendering be done on the main thread.
For apps with lots of text embedded in `UITableViewCell`s or `UICollectionViewCell`s, `UITextView` bring scrolling to a grinding halt.
## Acknowledgements
- [@ocrickard](https://github.com/ocrickard) who built [ComponentTextKit](https://github.com/facebook/componentkit/tree/master/ComponentTextKit) and taught me the basics.
- Created with ❤️ by [Ryan Nystrom](https://twitter.com/_ryannystrom)

View File

@@ -0,0 +1,15 @@
//
// NSAttributedStringKey+StyledText.swift
// StyledText
//
// Created by Ryan Nystrom on 5/29/18.
// Copyright © 2018 Ryan Nystrom. All rights reserved.
//
import Foundation
public extension NSAttributedStringKey {
static let highlight = NSAttributedStringKey("com.whoisryannystrom.styledtext.highlight")
}

View File

@@ -10,8 +10,8 @@ import UIKit
public final class StyledTextRenderer {
private let layoutManager: NSLayoutManager
private let textContainer: NSTextContainer
internal let layoutManager: NSLayoutManager
internal let textContainer: NSTextContainer
public let scale: CGFloat
public let inset: UIEdgeInsets
@@ -50,7 +50,7 @@ public final class StyledTextRenderer {
layoutManager.addTextContainer(textContainer)
}
private var storage: NSTextStorage {
internal var storage: NSTextStorage {
if let storage = map[contentSizeCategory] {
return storage
}
@@ -78,14 +78,14 @@ public final class StyledTextRenderer {
return size
}
public func size(width: CGFloat) -> CGSize {
public func size(in width: CGFloat = .greatestFiniteMagnitude) -> CGSize {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
return _size(StyledTextRenderCacheKey(width: width, attributedText: storage, backgroundColor: backgroundColor))
}
public func viewSize(width: CGFloat) -> CGSize {
return size(width: width).resized(inset: inset)
public func viewSize(in width: CGFloat = .greatestFiniteMagnitude) -> CGSize {
return size(in: width).resized(inset: inset)
}
private static let globalBitmapCache = LRUCache<StyledTextRenderCacheKey, CGImage>(
@@ -94,7 +94,7 @@ public final class StyledTextRenderer {
clearOnWarning: true
)
public func render(width: CGFloat) -> (image: CGImage?, size: CGSize) {
public func render(for width: CGFloat) -> (image: CGImage?, size: CGSize) {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
@@ -115,7 +115,7 @@ public final class StyledTextRenderer {
return (contents, size)
}
public func attributes(at point: CGPoint) -> [NSAttributedStringKey: Any]? {
public func attributes(at point: CGPoint) -> (attributes: [NSAttributedStringKey: Any], index: Int)? {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
var fractionDistance: CGFloat = 1.0
@@ -124,8 +124,10 @@ public final class StyledTextRenderer {
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: &fractionDistance
)
if index != NSNotFound, fractionDistance < 1.0 {
return layoutManager.textStorage?.attributes(at: index, effectiveRange: nil)
if index != NSNotFound,
fractionDistance < 1.0,
let attributes = layoutManager.textStorage?.attributes(at: index, effectiveRange: nil) {
return (attributes, index)
}
return nil
}
@@ -140,8 +142,8 @@ public final class StyledTextRenderer {
width: CGFloat
) -> StyledTextRenderer {
switch option {
case .size: let _ = size(width: width)
case .bitmap: let _ = render(width: width)
case .size: let _ = size(in: width)
case .bitmap: let _ = render(for: width)
}
return self
}

View File

@@ -21,6 +21,7 @@ open class StyledTextView: UIView {
private var renderer: StyledTextRenderer?
private var tapGesture: UITapGestureRecognizer?
private var longPressGesture: UILongPressGestureRecognizer?
private var highlightLayer = CAShapeLayer()
override public init(frame: CGRect) {
super.init(frame: frame)
@@ -45,6 +46,35 @@ open class StyledTextView: UIView {
let long = UILongPressGestureRecognizer(target: self, action: #selector(onLong(recognizer:)))
addGestureRecognizer(long)
self.longPressGesture = long
self.highlightColor = UIColor.black.withAlphaComponent(0.1)
layer.addSublayer(highlightLayer)
}
public var highlightColor: UIColor? {
get {
guard let color = highlightLayer.fillColor else { return nil }
return UIColor(cgColor: color)
}
set { highlightLayer.fillColor = newValue?.cgColor }
}
// MARK: Overries
open override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else { return }
highlight(at: touch.location(in: self))
}
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
clearHighlight()
}
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
clearHighlight()
}
// MARK: UIGestureRecognizerDelegate
@@ -54,7 +84,7 @@ open class StyledTextView: UIView {
let attributes = renderer?.attributes(at: gestureRecognizer.location(in: self)) else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
for attribute in attributes {
for attribute in attributes.attributes {
if gesturableAttributes.contains(attribute.key) {
return true
}
@@ -67,7 +97,7 @@ open class StyledTextView: UIView {
@objc func onTap(recognizer: UITapGestureRecognizer) {
let point = recognizer.location(in: self)
guard let attributes = renderer?.attributes(at: point) else { return }
delegate?.didTap(view: self, attributes: attributes, point: point)
delegate?.didTap(view: self, attributes: attributes.attributes, point: point)
}
@objc func onLong(recognizer: UILongPressGestureRecognizer) {
@@ -75,21 +105,74 @@ open class StyledTextView: UIView {
guard recognizer.state == .began,
let attributes = renderer?.attributes(at: point)
else { return }
delegate?.didLongPress(view: self, attributes: attributes, point: point)
delegate?.didLongPress(view: self, attributes: attributes.attributes, point: point)
}
private func highlight(at point: CGPoint) {
guard let renderer = renderer,
let attributes = renderer.attributes(at: point),
attributes.attributes[.highlight] != nil
else { return }
let storage = renderer.storage
let maxLen = storage.length
var min = attributes.index
var max = attributes.index
storage.enumerateAttributes(
in: NSRange(location: 0, length: attributes.index),
options: .reverse
) { (attrs, range, stop) in
if attrs[.highlight] != nil && min > 0 {
min = range.location
} else {
stop.pointee = true
}
}
storage.enumerateAttributes(
in: NSRange(location: attributes.index, length: maxLen - attributes.index),
options: []
){ (attrs, range, stop) in
if attrs[.highlight] != nil && max < maxLen {
max = range.location + range.length
} else {
stop.pointee = true
}
}
let range = NSRange(location: min, length: max - min)
let path = UIBezierPath()
renderer.layoutManager.enumerateEnclosingRects(
forGlyphRange: range,
withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 0),
in: renderer.textContainer
) { (rect, stop) in
path.append(UIBezierPath(roundedRect: rect.insetBy(dx: -2, dy: -2), cornerRadius: 3))
}
highlightLayer.frame = bounds
highlightLayer.path = path.cgPath
}
private func clearHighlight() {
highlightLayer.path = nil
}
// MARK: Public API
open func configure(renderer: StyledTextRenderer, width: CGFloat) {
open func configure(with renderer: StyledTextRenderer, width: CGFloat) {
self.renderer = renderer
layer.contentsScale = renderer.scale
reposition(width: width)
reposition(for: width)
accessibilityLabel = renderer.string.allText
}
open func reposition(width: CGFloat) {
open func reposition(for width: CGFloat) {
guard let renderer = self.renderer else { return }
let result = renderer.render(width: width)
let result = renderer.render(for: width)
layer.contents = result.image
frame = CGRect(origin: CGPoint(x: renderer.inset.left, y: renderer.inset.top), size: result.size)
}