mirror of
https://github.com/zhigang1992/GitHawk.git
synced 2026-04-24 04:05:16 +08:00
Update StyledText with new highlight API (#1844)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
2
Pods/Manifest.lock
generated
@@ -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:
|
||||
|
||||
8436
Pods/Pods.xcodeproj/project.pbxproj
generated
8436
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
137
Pods/StyledText/README.md
generated
137
Pods/StyledText/README.md
generated
@@ -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)
|
||||
15
Pods/StyledText/StyledText/NSAttributedStringKey+StyledText.swift
generated
Normal file
15
Pods/StyledText/StyledText/NSAttributedStringKey+StyledText.swift
generated
Normal 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")
|
||||
|
||||
}
|
||||
26
Pods/StyledText/StyledText/StyledTextRenderer.swift
generated
26
Pods/StyledText/StyledText/StyledTextRenderer.swift
generated
@@ -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
|
||||
}
|
||||
|
||||
97
Pods/StyledText/StyledText/StyledTextView.swift
generated
97
Pods/StyledText/StyledText/StyledTextView.swift
generated
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user