Use AttributedStringCell for IssueLabeledCell (#1071)

* Use AttributedStringCell for IssueLabeledCell

* Code review changes
This commit is contained in:
James Sherlock
2017-11-26 17:31:27 +00:00
committed by Ryan Nystrom
parent bb2828e9c8
commit a504a87033
12 changed files with 179 additions and 109 deletions

View File

@@ -16,5 +16,6 @@ enum MarkdownAttribute {
static let linkShorteningDisabled = NSAttributedStringKey(rawValue: "com.freetime.Markdown.link-shortening-disabled-name")
static let issue = NSAttributedStringKey(rawValue: "com.freetime.Markdown.issue")
static let details = NSAttributedStringKey(rawValue: "com.freetime.Markdown.details")
static let label = NSAttributedStringKey(rawValue: "com.freetime.Markdown.label")
static let commit = NSAttributedStringKey(rawValue: "com.freetime.Markdown.commit")
}

View File

@@ -348,7 +348,9 @@ extension GithubClient {
title: newLabel.name,
color: newLabel.color,
date: Date(),
type: .added
type: .added,
repoOwner: owner,
repoName: repo
))
}
}
@@ -360,7 +362,9 @@ extension GithubClient {
title: oldLabel.name,
color: oldLabel.color,
date: Date(),
type: .removed
type: .removed,
repoOwner: owner,
repoName: repo
))
}
}

View File

@@ -91,7 +91,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsIssue: Is
title: unlabeled.label.name,
color: unlabeled.label.color,
date: date,
type: .removed
type: .removed,
repoOwner: owner,
repoName: repo
)
results.append(model)
} else if let labeled = node.asLabeledEvent,
@@ -102,7 +104,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsIssue: Is
title: labeled.label.name,
color: labeled.label.color,
date: date,
type: .added
type: .added,
repoOwner: owner,
repoName: repo
)
results.append(model)
} else if let closed = node.asClosedEvent,

View File

@@ -9,95 +9,19 @@
import UIKit
import SnapKit
protocol IssueLabeledCellDelegate: class {
func didTapActor(cell: IssueLabeledCell)
func didTapLabel(cell: IssueLabeledCell)
}
final class IssueLabeledCell: UICollectionViewCell {
weak var delegate: IssueLabeledCellDelegate?
private let descriptionButton = UIButton()
private let labelButton = UIButton()
private let dateLabel = ShowMoreDetailsLabel()
override init(frame: CGRect) {
super.init(frame: frame)
descriptionButton.addTarget(self, action: #selector(IssueLabeledCell.onActor), for: .touchUpInside)
contentView.addSubview(descriptionButton)
descriptionButton.snp.makeConstraints { make in
make.left.equalTo(Styles.Sizes.eventGutter)
make.centerY.equalTo(contentView)
}
labelButton.setupAsLabel(icon: false)
labelButton.addTarget(self, action: #selector(IssueLabeledCell.onLabel), for: .touchUpInside)
contentView.addSubview(labelButton)
labelButton.snp.makeConstraints { make in
make.left.equalTo(descriptionButton.snp.right).offset(Styles.Sizes.inlineSpacing)
make.centerY.equalTo(descriptionButton)
}
dateLabel.font = Styles.Fonts.secondary
dateLabel.textColor = Styles.Colors.Gray.medium.color
contentView.addSubview(dateLabel)
dateLabel.snp.makeConstraints { make in
make.left.equalTo(labelButton.snp.right).offset(Styles.Sizes.inlineSpacing)
make.centerY.equalTo(contentView)
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
layoutContentViewForSafeAreaInsets()
}
// MARK: Private API
@objc func onActor() {
delegate?.didTapActor(cell: self)
}
@objc func onLabel() {
delegate?.didTapLabel(cell: self)
}
final class IssueLabeledCell: AttributedStringCell {
static let insets = UIEdgeInsets(
top: Styles.Sizes.inlineSpacing,
left: Styles.Sizes.eventGutter,
bottom: Styles.Sizes.inlineSpacing,
right: Styles.Sizes.eventGutter
)
// MARK: Public API
func configure(_ model: IssueLabeledModel) {
let actorAttributes = [
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.dark.color,
NSAttributedStringKey.font: Styles.Fonts.secondaryBold
]
let actor = NSAttributedString(string: model.actor, attributes: actorAttributes)
let actionString: String
switch model.type {
case .added: actionString = NSLocalizedString(" added", comment: "")
case .removed: actionString = NSLocalizedString(" removed", comment: "")
}
let actionAttributes = [
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.medium.color,
NSAttributedStringKey.font: Styles.Fonts.secondary
]
let action = NSAttributedString(string: actionString, attributes: actionAttributes)
let descriptionText = NSMutableAttributedString(attributedString: actor)
descriptionText.append(action)
descriptionButton.setAttributedTitle(descriptionText, for: .normal)
let color = UIColor.fromHex(model.color)
labelButton.backgroundColor = color
labelButton.setTitle(model.title, for: .normal)
labelButton.setTitleColor(color.textOverlayColor, for: .normal)
dateLabel.setText(date: model.date)
set(attributedText: model.attributedString)
}
}

View File

@@ -22,6 +22,7 @@ final class IssueLabeledModel: ListDiffable {
let color: String
let date: Date
let type: EventType
let attributedString: NSAttributedStringSizing
init(
id: String,
@@ -29,7 +30,9 @@ final class IssueLabeledModel: ListDiffable {
title: String,
color: String,
date: Date,
type: EventType
type: EventType,
repoOwner: String,
repoName: String
) {
self.id = id
self.actor = actor
@@ -37,6 +40,66 @@ final class IssueLabeledModel: ListDiffable {
self.color = color
self.date = date
self.type = type
// Actor
let actorAttributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.dark.color,
NSAttributedStringKey.font: Styles.Fonts.secondaryBold,
MarkdownAttribute.username: actor
]
let attributedString = NSMutableAttributedString(string: actor, attributes: actorAttributes)
// Action
let actionAttributes = [
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.medium.color,
NSAttributedStringKey.font: Styles.Fonts.secondary
]
let actionString: String
switch type {
case .added: actionString = NSLocalizedString(" added ", comment: "")
case .removed: actionString = NSLocalizedString(" removed ", comment: "")
}
attributedString.append(NSAttributedString(string: actionString, attributes: actionAttributes))
// Label
let labelColor = color.color
let labelDetails = LabelDetails(owner: repoOwner, repo: repoName, label: title)
let labelAttributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.font: Styles.Fonts.secondaryBold,
NSAttributedStringKey.backgroundColor: labelColor,
NSAttributedStringKey.foregroundColor: labelColor.textOverlayColor ?? .black,
MarkdownAttribute.label: labelDetails
]
attributedString.append(NSAttributedString(string: title, attributes: labelAttributes))
// Date
let dateAttributes: [NSAttributedStringKey: Any] = [
NSAttributedStringKey.font: Styles.Fonts.secondary,
NSAttributedStringKey.foregroundColor: Styles.Colors.Gray.medium.color,
MarkdownAttribute.details: DateDetailsFormatter().string(from: date)
]
attributedString.append(NSAttributedString(string: " \(date.agoString)", attributes: dateAttributes))
// Set
self.attributedString = NSAttributedStringSizing(
containerWidth: 0,
attributedText: attributedString,
inset: IssueLabeledCell.insets,
backgroundColor: Styles.Colors.Gray.lighter.color
)
}
// MARK: ListDiffable

View File

@@ -9,10 +9,10 @@
import Foundation
import IGListKit
final class IssueLabeledSectionController: ListGenericSectionController<IssueLabeledModel>, IssueLabeledCellDelegate {
final class IssueLabeledSectionController: ListGenericSectionController<IssueLabeledModel> {
private let issueModel: IssueDetailsModel
init(issueModel: IssueDetailsModel) {
self.issueModel = issueModel
super.init()
@@ -20,7 +20,8 @@ final class IssueLabeledSectionController: ListGenericSectionController<IssueLab
override func sizeForItem(at index: Int) -> CGSize {
guard let width = collectionContext?.containerSize.width else { fatalError("Collection context must be set") }
return CGSize(width: width, height: Styles.Sizes.labelEventHeight)
let insets = IssueLabeledCell.insets
return CGSize(width: width, height: insets.top + insets.bottom + (object?.attributedString.textViewSize(width).height ?? 0))
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
@@ -28,20 +29,8 @@ final class IssueLabeledSectionController: ListGenericSectionController<IssueLab
let object = self.object
else { fatalError("Missing collection context, cell incorrect type, or object missing") }
cell.configure(object)
cell.delegate = self
cell.delegate = viewController
return cell
}
// MARK: IssueLabeledCellDelegate
func didTapActor(cell: IssueLabeledCell) {
guard let actor = object?.actor else { return }
viewController?.presentProfile(login: actor)
}
func didTapLabel(cell: IssueLabeledCell) {
guard let label = object?.title else { return }
viewController?.presentLabels(owner: issueModel.owner, repo: issueModel.repo, label: label)
}
}

View File

@@ -97,7 +97,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsPullReque
title: unlabeled.label.name,
color: unlabeled.label.color,
date: date,
type: .removed
type: .removed,
repoOwner: owner,
repoName: repo
)
results.append(model)
} else if let labeled = node.asLabeledEvent,
@@ -108,7 +110,9 @@ extension IssueOrPullRequestQuery.Data.Repository.IssueOrPullRequest.AsPullReque
title: labeled.label.name,
color: labeled.label.color,
date: date,
type: .added
type: .added,
repoOwner: owner,
repoName: repo
)
results.append(model)
} else if let closed = node.asClosedEvent,

View File

@@ -0,0 +1,23 @@
//
// LabelDetails.swift
// Freetime
//
// Created by Sherlock, James on 26/11/2017.
// Copyright © 2017 Ryan Nystrom. All rights reserved.
//
import Foundation
final class LabelDetails {
let owner: String
let repo: String
let label: String
init(owner: String, repo: String, label: String) {
self.owner = owner
self.repo = repo
self.label = label
}
}

View File

@@ -63,7 +63,7 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
textContainer.maximumNumberOfLines = maximumNumberOfLines
textContainer.lineFragmentPadding = lineFragmentPadding
layoutManager = NSLayoutManager()
layoutManager = LabelLayoutManager()
layoutManager.allowsNonContiguousLayout = allowsNonContiguousLayout
layoutManager.hyphenationFactor = hyphenationFactor
layoutManager.showsInvisibleCharacters = showsInvisibleCharacters
@@ -197,3 +197,50 @@ final class NSAttributedStringSizing: NSObject, ListDiffable {
}
}
final class LabelLayoutManager: NSLayoutManager {
override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>, count rectCount: Int, forCharacterRange charRange: NSRange, color: UIColor) {
// Get the attributes for the backgroundColor attribute
var range = charRange
let attributes = textStorage?.attributes(at: charRange.location, effectiveRange: &range)
// Ensure that this is one of our labels we're dealing with, ignore basic backgroundColor attributes
guard attributes?[MarkdownAttribute.label] != nil else {
super.fillBackgroundRectArray(rectArray, count: rectCount, forCharacterRange: charRange, color: color)
return
}
// Define label rectangle and rounded path
let cornerRadius: CGFloat = 2
let rect = rectArray[0].insetBy(dx: -2, dy: 1)
let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
// Define the CoreGraphics context
let context = UIGraphicsGetCurrentContext()
context?.setAllowsAntialiasing(true)
context?.setShouldAntialias(true)
context?.setLineWidth(cornerRadius * 2)
context?.setLineJoin(.round)
// Draw the label background
context?.addPath(path.cgPath)
context?.drawPath(using: .fillStroke)
// Add a small border around the label
let borderWidth = 1 / UIScreen.main.scale
let strokeRect = rect.insetBy(dx: -2, dy: -borderWidth)
let strokePath = UIBezierPath(roundedRect: strokeRect, cornerRadius: cornerRadius)
Styles.Colors.Gray.border.color.set()
context?.setLineWidth(borderWidth)
context?.addPath(strokePath.cgPath)
context?.drawPath(using: .stroke)
// Reset color as per Apple docs
color.set()
}
}

View File

@@ -22,6 +22,10 @@ extension UIViewController: AttributedStringViewDelegate {
guard let url = URL(string: "mailTo:\(email)") else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
func didTapLabel(view: AttributedStringView, label: LabelDetails) {
presentLabels(owner: label.owner, repo: label.repo, label: label.label)
}
func didTapCommit(view: AttributedStringView, commit: CommitDetails) {
presentCommit(owner: commit.owner, repo: commit.repo, hash: commit.hash)

View File

@@ -13,6 +13,7 @@ protocol AttributedStringViewDelegate: class {
func didTapUsername(view: AttributedStringView, username: String)
func didTapEmail(view: AttributedStringView, email: String)
func didTapCommit(view: AttributedStringView, commit: CommitDetails)
func didTapLabel(view: AttributedStringView, label: LabelDetails)
}
protocol AttributedStringViewIssueDelegate: class {
@@ -87,6 +88,8 @@ final class AttributedStringView: UIView {
delegate?.didTapEmail(view: self, email: emailString)
} else if let issue = attributes[MarkdownAttribute.issue] as? IssueDetailsModel {
issueDelegate?.didTapIssue(view: self, issue: issue)
} else if let label = attributes[MarkdownAttribute.label] as? LabelDetails {
delegate?.didTapLabel(view: self, label: label)
} else if let commit = attributes[MarkdownAttribute.commit] as? CommitDetails {
delegate?.didTapCommit(view: self, commit: commit)
}

View File

@@ -340,6 +340,7 @@
7BBFEE591F8A8A0400C68E47 /* SearchBarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBFEE561F8A8A0400C68E47 /* SearchBarCell.swift */; };
7BBFEE5B1F8A8A0400C68E47 /* SearchBarSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBFEE581F8A8A0400C68E47 /* SearchBarSectionController.swift */; };
7BF2239D1F91056C006CC9A2 /* File+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF2239C1F91056C006CC9A2 /* File+Filename.swift */; };
98003D8D1FCAD7FC00755C17 /* LabelDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */; };
98647DF31F758CCF00A4DE7A /* NewIssueTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98647DF21F758CCF00A4DE7A /* NewIssueTableViewController.swift */; };
986B87191F2B875800AAB55C /* GithubClient+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986B87181F2B875800AAB55C /* GithubClient+Search.swift */; };
986B871B1F2B87DD00AAB55C /* SearchRepoResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986B871A1F2B87DD00AAB55C /* SearchRepoResult.swift */; };
@@ -750,6 +751,7 @@
7BBFEE581F8A8A0400C68E47 /* SearchBarSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBarSectionController.swift; sourceTree = "<group>"; };
7BF2239C1F91056C006CC9A2 /* File+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Filename.swift"; sourceTree = "<group>"; };
94840B8CCA6EA8A3EA37FA91 /* Pods-Freetime.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Freetime.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Freetime/Pods-Freetime.debug.xcconfig"; sourceTree = "<group>"; };
98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelDetails.swift; sourceTree = "<group>"; };
98647DF21F758CCF00A4DE7A /* NewIssueTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewIssueTableViewController.swift; sourceTree = "<group>"; };
986B87181F2B875800AAB55C /* GithubClient+Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "GithubClient+Search.swift"; sourceTree = "<group>"; };
986B871A1F2B87DD00AAB55C /* SearchRepoResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchRepoResult.swift; sourceTree = "<group>"; };
@@ -1321,6 +1323,7 @@
291929411F3EA8CD0012067B /* File.swift */,
7BF2239C1F91056C006CC9A2 /* File+Filename.swift */,
2963A93A1EE25F6F0066509C /* LabelableFields+IssueLabelModel.swift */,
98003D8C1FCAD7FC00755C17 /* LabelDetails.swift */,
291929591F3FB1D20012067B /* Link.swift */,
29C9FDBA1EC65FEE00EE3A52 /* NotificationResponse.swift */,
29C9FDBB1EC65FEE00EE3A52 /* Organization.swift */,
@@ -2176,6 +2179,7 @@
292FCAF81EDFCC510026635E /* CollapsibleCell.swift in Sources */,
295F52AD1EF1BE83000B53CF /* CommentModelsFromMarkdown.swift in Sources */,
2919295A1F3FB1D20012067B /* Content.swift in Sources */,
98003D8D1FCAD7FC00755C17 /* LabelDetails.swift in Sources */,
297DD5E11F061BBE006E7E63 /* CreateProfileViewController.swift in Sources */,
29A195041EC74C4800C3E289 /* Date+Display.swift in Sources */,
29AAB7171FB4A2AE001D5E6A /* BoundedImageSize.swift in Sources */,