Implement new timeline layout.
This commit is contained in:
parent
2efbd44811
commit
e37d4ddd2f
|
@ -12,13 +12,13 @@ extension String {
|
||||||
|
|
||||||
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
|
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
|
||||||
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
|
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
|
||||||
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
|
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||||
return ceil(boundingBox.height)
|
return ceil(boundingBox.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
|
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
|
||||||
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
|
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
|
||||||
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
|
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||||
return ceil(boundingBox.width)
|
return ceil(boundingBox.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import Articles
|
||||||
struct MasterTimelineCellData {
|
struct MasterTimelineCellData {
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let text: String
|
let summary: String
|
||||||
let dateString: String
|
let dateString: String
|
||||||
let feedName: String
|
let feedName: String
|
||||||
let showFeedName: Bool
|
let showFeedName: Bool
|
||||||
|
@ -25,7 +25,7 @@ struct MasterTimelineCellData {
|
||||||
init(article: Article, showFeedName: Bool, feedName: String?, avatar: UIImage?, showAvatar: Bool, featuredImage: UIImage?) {
|
init(article: Article, showFeedName: Bool, feedName: String?, avatar: UIImage?, showAvatar: Bool, featuredImage: UIImage?) {
|
||||||
|
|
||||||
self.title = TimelineStringFormatter.truncatedTitle(article)
|
self.title = TimelineStringFormatter.truncatedTitle(article)
|
||||||
self.text = TimelineStringFormatter.truncatedSummary(article)
|
self.summary = TimelineStringFormatter.truncatedSummary(article)
|
||||||
|
|
||||||
self.dateString = TimelineStringFormatter.dateString(article.logicalDatePublished)
|
self.dateString = TimelineStringFormatter.dateString(article.logicalDatePublished)
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ struct MasterTimelineCellData {
|
||||||
|
|
||||||
init() { //Empty
|
init() { //Empty
|
||||||
self.title = ""
|
self.title = ""
|
||||||
self.text = ""
|
self.summary = ""
|
||||||
self.dateString = ""
|
self.dateString = ""
|
||||||
self.feedName = ""
|
self.feedName = ""
|
||||||
self.showFeedName = false
|
self.showFeedName = false
|
||||||
|
|
|
@ -11,114 +11,100 @@ import RSCore
|
||||||
|
|
||||||
struct MasterTimelineCellLayout {
|
struct MasterTimelineCellLayout {
|
||||||
|
|
||||||
|
static let maxNumberOfLines = 2
|
||||||
static let cellPadding = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
|
static let cellPadding = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
|
||||||
|
|
||||||
static let feedColor = AppAssets.timelineTextSecondaryColor
|
static let unreadCircleMarginLeft = CGFloat(integerLiteral: 8)
|
||||||
static let feedNameFont = UIFont.systemFont(ofSize: UIFont.systemFontSize)
|
|
||||||
|
|
||||||
static let dateColor = AppAssets.timelineTextSecondaryColor
|
|
||||||
static let dateFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: UIFont.Weight.bold)
|
|
||||||
static let dateMarginBottom = CGFloat(integerLiteral: 1)
|
|
||||||
|
|
||||||
static let titleColor = AppAssets.timelineTextPrimaryColor
|
|
||||||
static let titleFont = UIFont.systemFont(ofSize: UIFont.systemFontSize, weight: .semibold)
|
|
||||||
static let titleBottomMargin = CGFloat(integerLiteral: 1)
|
|
||||||
static let titleNumberOfLines = 2
|
|
||||||
|
|
||||||
static let textColor = AppAssets.timelineTextPrimaryColor
|
|
||||||
static let textFont = UIFont.systemFont(ofSize: UIFont.systemFontSize)
|
|
||||||
|
|
||||||
static let textOnlyFont = UIFont.systemFont(ofSize: UIFont.systemFontSize)
|
|
||||||
|
|
||||||
static let unreadCircleDimension = CGFloat(integerLiteral: 8)
|
static let unreadCircleDimension = CGFloat(integerLiteral: 8)
|
||||||
static let unreadCircleMarginRight = CGFloat(integerLiteral: 8)
|
static let unreadCircleMarginRight = CGFloat(integerLiteral: 8)
|
||||||
|
|
||||||
static let starDimension = CGFloat(integerLiteral: 13)
|
static let starDimension = CGFloat(integerLiteral: 13)
|
||||||
|
|
||||||
static let avatarSize = CGSize(width: 48.0, height: 48.0)
|
static let avatarSize = CGSize(width: 48.0, height: 48.0)
|
||||||
static let avatarMarginLeft = CGFloat(integerLiteral: 8)
|
static let avatarMarginRight = CGFloat(integerLiteral: 8)
|
||||||
static let avatarCornerRadius = CGFloat(integerLiteral: 4)
|
static let avatarCornerRadius = CGFloat(integerLiteral: 4)
|
||||||
|
|
||||||
|
static let titleColor = AppAssets.timelineTextPrimaryColor
|
||||||
|
static let titleFont = UIFont.preferredFont(forTextStyle: .headline)
|
||||||
|
static let titleBottomMargin = CGFloat(integerLiteral: 1)
|
||||||
|
|
||||||
|
|
||||||
|
static let feedColor = AppAssets.timelineTextSecondaryColor
|
||||||
|
static let feedNameFont = UIFont.preferredFont(forTextStyle: .footnote)
|
||||||
|
static let feedRightMargin = CGFloat(integerLiteral: 8)
|
||||||
|
|
||||||
|
static let dateColor = AppAssets.timelineTextSecondaryColor
|
||||||
|
static let dateFont = UIFont.preferredFont(forTextStyle: .footnote)
|
||||||
|
static let dateMarginBottom = CGFloat(integerLiteral: 1)
|
||||||
|
|
||||||
|
static let summaryColor = AppAssets.timelineTextPrimaryColor
|
||||||
|
static let summaryFont = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
|
||||||
static let chevronWidth = CGFloat(integerLiteral: 28)
|
static let chevronWidth = CGFloat(integerLiteral: 28)
|
||||||
|
|
||||||
let width: CGFloat
|
let width: CGFloat
|
||||||
|
let insets: UIEdgeInsets
|
||||||
|
|
||||||
let height: CGFloat
|
let height: CGFloat
|
||||||
let feedNameRect: CGRect
|
|
||||||
let dateRect: CGRect
|
|
||||||
let titleRect: CGRect
|
|
||||||
let numberOfLinesForTitle: Int
|
|
||||||
let summaryRect: CGRect
|
|
||||||
let textRect: CGRect
|
|
||||||
let unreadIndicatorRect: CGRect
|
let unreadIndicatorRect: CGRect
|
||||||
let starRect: CGRect
|
let starRect: CGRect
|
||||||
let avatarImageRect: CGRect
|
let avatarImageRect: CGRect
|
||||||
let paddingBottom: CGFloat
|
let titleRect: CGRect
|
||||||
|
let summaryRect: CGRect
|
||||||
|
let feedNameRect: CGRect
|
||||||
|
let dateRect: CGRect
|
||||||
|
|
||||||
let separatorInsets: UIEdgeInsets
|
let separatorInsets: UIEdgeInsets
|
||||||
|
|
||||||
init(width: CGFloat, height: CGFloat, feedNameRect: CGRect, dateRect: CGRect, titleRect: CGRect, numberOfLinesForTitle: Int, summaryRect: CGRect, textRect: CGRect, unreadIndicatorRect: CGRect, starRect: CGRect, avatarImageRect: CGRect, paddingBottom: CGFloat, separatorInsets: UIEdgeInsets) {
|
init(width: CGFloat, insets: UIEdgeInsets, cellData: MasterTimelineCellData, showAvatar: Bool) {
|
||||||
|
|
||||||
self.width = width - MasterTimelineCellLayout.chevronWidth
|
self.width = width
|
||||||
self.feedNameRect = feedNameRect
|
self.insets = insets
|
||||||
self.dateRect = dateRect
|
|
||||||
self.titleRect = titleRect
|
|
||||||
self.numberOfLinesForTitle = numberOfLinesForTitle
|
|
||||||
self.summaryRect = summaryRect
|
|
||||||
self.textRect = textRect
|
|
||||||
self.unreadIndicatorRect = unreadIndicatorRect
|
|
||||||
self.starRect = starRect
|
|
||||||
self.avatarImageRect = avatarImageRect
|
|
||||||
self.paddingBottom = paddingBottom
|
|
||||||
self.separatorInsets = separatorInsets
|
|
||||||
|
|
||||||
if height > 0.1 {
|
var currentPoint = CGPoint.zero
|
||||||
self.height = height
|
currentPoint.x = MasterTimelineCellLayout.cellPadding.left + insets.left + MasterTimelineCellLayout.unreadCircleMarginLeft
|
||||||
|
currentPoint.y = MasterTimelineCellLayout.cellPadding.top
|
||||||
|
|
||||||
|
// Unread Indicator and Star
|
||||||
|
self.unreadIndicatorRect = MasterTimelineCellLayout.rectForUnreadIndicator(currentPoint)
|
||||||
|
self.starRect = MasterTimelineCellLayout.rectForStar(currentPoint)
|
||||||
|
|
||||||
|
// Start the point at the beginning position of the main block
|
||||||
|
currentPoint.x += MasterTimelineCellLayout.unreadCircleDimension + MasterTimelineCellLayout.unreadCircleMarginRight
|
||||||
|
|
||||||
|
// Separator Insets
|
||||||
|
self.separatorInsets = UIEdgeInsets(top: 0, left: currentPoint.x, bottom: 0, right: 0)
|
||||||
|
|
||||||
|
// Avatar
|
||||||
|
if showAvatar {
|
||||||
|
self.avatarImageRect = MasterTimelineCellLayout.rectForAvatar(currentPoint)
|
||||||
|
currentPoint.x = self.avatarImageRect.maxX + MasterTimelineCellLayout.avatarMarginRight
|
||||||
} else {
|
} else {
|
||||||
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
|
self.avatarImageRect = CGRect.zero
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
let textAreaWidth = width - (currentPoint.x + MasterTimelineCellLayout.chevronWidth + MasterTimelineCellLayout.cellPadding.right + insets.right)
|
||||||
|
|
||||||
init(width: CGFloat, height: CGFloat, cellData: MasterTimelineCellData, hasAvatar: Bool) {
|
// Title Text Block
|
||||||
|
let (titleRect, numberOfLinesForTitle) = MasterTimelineCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
|
||||||
|
self.titleRect = titleRect
|
||||||
|
|
||||||
let width = width - MasterTimelineCellLayout.chevronWidth
|
// Summary Text Block
|
||||||
|
if self.titleRect != CGRect.zero {
|
||||||
// If height == 0.0, then height is calculated.
|
currentPoint.y = self.titleRect.maxY + MasterTimelineCellLayout.titleBottomMargin
|
||||||
|
|
||||||
let showAvatar = hasAvatar && cellData.showAvatar
|
|
||||||
var textBoxRect = MasterTimelineCellLayout.rectForTextBox(cellData, showAvatar, width)
|
|
||||||
|
|
||||||
let (titleRect, numberOfLinesForTitle) = MasterTimelineCellLayout.rectForTitle(textBoxRect, cellData)
|
|
||||||
let summaryRect = numberOfLinesForTitle > 0 ? MasterTimelineCellLayout.rectForSummary(textBoxRect, titleRect, numberOfLinesForTitle, cellData) : CGRect.zero
|
|
||||||
let textRect = numberOfLinesForTitle > 0 ? CGRect.zero : MasterTimelineCellLayout.rectForText(textBoxRect, cellData)
|
|
||||||
|
|
||||||
var lastTextRect = titleRect
|
|
||||||
if numberOfLinesForTitle == 0 {
|
|
||||||
lastTextRect = textRect
|
|
||||||
} else if numberOfLinesForTitle == 1 {
|
|
||||||
if summaryRect.height > 0.1 {
|
|
||||||
lastTextRect = summaryRect
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self.summaryRect = MasterTimelineCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
|
||||||
|
|
||||||
let dateRect = MasterTimelineCellLayout.rectForDate(textBoxRect, lastTextRect, cellData)
|
currentPoint.y = [self.titleRect, self.summaryRect].maxY()
|
||||||
let feedNameRect = MasterTimelineCellLayout.rectForFeedName(textBoxRect, dateRect, cellData)
|
|
||||||
|
|
||||||
textBoxRect.size.height = ceil([titleRect, summaryRect, textRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
|
// Feed Name and Pub Date
|
||||||
let avatarImageRect = MasterTimelineCellLayout.rectForAvatar(cellData, showAvatar, textBoxRect, width, height)
|
self.dateRect = MasterTimelineCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
|
||||||
let unreadIndicatorRect = MasterTimelineCellLayout.rectForUnreadIndicator(textBoxRect)
|
|
||||||
let starRect = MasterTimelineCellLayout.rectForStar(unreadIndicatorRect)
|
|
||||||
|
|
||||||
let paddingBottom = MasterTimelineCellLayout.cellPadding.bottom
|
let feedNameWidth = textAreaWidth - (MasterTimelineCellLayout.feedRightMargin + self.dateRect.size.width)
|
||||||
|
self.feedNameRect = MasterTimelineCellLayout.rectForFeedName(cellData, currentPoint, feedNameWidth)
|
||||||
|
|
||||||
let separatorInsets = UIEdgeInsets(top: 0, left: unreadIndicatorRect.maxX + MasterTimelineCellLayout.unreadCircleMarginRight, bottom: 0, right: 0)
|
self.height = [self.avatarImageRect, self.feedNameRect].maxY() + MasterTimelineCellLayout.cellPadding.bottom
|
||||||
|
|
||||||
self.init(width: width, height: height, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom, separatorInsets: separatorInsets)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static func height(for width: CGFloat, cellData: MasterTimelineCellData) -> CGFloat {
|
|
||||||
let layout = MasterTimelineCellLayout(width: width, height: 0.0, cellData: cellData, hasAvatar: true)
|
|
||||||
return layout.height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -127,123 +113,102 @@ struct MasterTimelineCellLayout {
|
||||||
|
|
||||||
private extension MasterTimelineCellLayout {
|
private extension MasterTimelineCellLayout {
|
||||||
|
|
||||||
static func rectForTextBox(_ cellData: MasterTimelineCellData, _ showAvatar: Bool, _ width: CGFloat) -> CGRect {
|
static func rectForUnreadIndicator(_ point: CGPoint) -> CGRect {
|
||||||
|
|
||||||
// Returned height is a placeholder. Not needed when this is calculated.
|
|
||||||
|
|
||||||
let textBoxOriginX = MasterTimelineCellLayout.cellPadding.left + MasterTimelineCellLayout.unreadCircleDimension + MasterTimelineCellLayout.unreadCircleMarginRight
|
|
||||||
let textBoxMaxX = floor((width - MasterTimelineCellLayout.cellPadding.right) - (showAvatar ? MasterTimelineCellLayout.avatarSize.width + MasterTimelineCellLayout.avatarMarginLeft : 0.0))
|
|
||||||
let textBoxWidth = floor(textBoxMaxX - textBoxOriginX)
|
|
||||||
let textBoxRect = CGRect(x: textBoxOriginX, y: MasterTimelineCellLayout.cellPadding.top, width: textBoxWidth, height: 1000000)
|
|
||||||
|
|
||||||
return textBoxRect
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectForTitle(_ textBoxRect: CGRect, _ cellData: MasterTimelineCellData) -> (CGRect, Int) {
|
|
||||||
|
|
||||||
var r = textBoxRect
|
|
||||||
|
|
||||||
if cellData.title.isEmpty {
|
|
||||||
r.size.height = 0
|
|
||||||
return (r, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MasterTimelineCellLayout.titleFont, numberOfLines: MasterTimelineCellLayout.titleNumberOfLines, width: Int(textBoxRect.width))
|
|
||||||
r.size.height = sizeInfo.size.height
|
|
||||||
if sizeInfo.numberOfLinesUsed < 1 {
|
|
||||||
r.size.height = 0
|
|
||||||
}
|
|
||||||
return (r, sizeInfo.numberOfLinesUsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectForSummary(_ textBoxRect: CGRect, _ titleRect: CGRect, _ titleNumberOfLines: Int, _ cellData: MasterTimelineCellData) -> CGRect {
|
|
||||||
|
|
||||||
if titleNumberOfLines >= MasterTimelineCellLayout.titleNumberOfLines || cellData.text.isEmpty {
|
|
||||||
return CGRect.zero
|
|
||||||
}
|
|
||||||
|
|
||||||
return rectOfLineBelow(titleRect, titleRect, 0, cellData.text, MasterTimelineCellLayout.textFont)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectForText(_ textBoxRect: CGRect, _ cellData: MasterTimelineCellData) -> CGRect {
|
|
||||||
|
|
||||||
var r = textBoxRect
|
|
||||||
|
|
||||||
if cellData.text.isEmpty {
|
|
||||||
r.size.height = 0
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
let sizeInfo = MultilineUILabelSizer.size(for: cellData.text, font: MasterTimelineCellLayout.textOnlyFont, numberOfLines: MasterTimelineCellLayout.titleNumberOfLines, width: Int(textBoxRect.width))
|
|
||||||
r.size.height = sizeInfo.size.height
|
|
||||||
if sizeInfo.numberOfLinesUsed < 1 {
|
|
||||||
r.size.height = 0
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectForDate(_ textBoxRect: CGRect, _ rectAbove: CGRect, _ cellData: MasterTimelineCellData) -> CGRect {
|
|
||||||
|
|
||||||
return rectOfLineBelow(textBoxRect, rectAbove, MasterTimelineCellLayout.titleBottomMargin, cellData.dateString, MasterTimelineCellLayout.dateFont)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectForFeedName(_ textBoxRect: CGRect, _ dateRect: CGRect, _ cellData: MasterTimelineCellData) -> CGRect {
|
|
||||||
|
|
||||||
if !cellData.showFeedName {
|
|
||||||
return CGRect.zero
|
|
||||||
}
|
|
||||||
|
|
||||||
return rectOfLineBelow(textBoxRect, dateRect, MasterTimelineCellLayout.dateMarginBottom, cellData.feedName, MasterTimelineCellLayout.feedNameFont)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectOfLineBelow(_ textBoxRect: CGRect, _ rectAbove: CGRect, _ topMargin: CGFloat, _ value: String, _ font: UIFont) -> CGRect {
|
|
||||||
|
|
||||||
let textFieldSize = SingleLineUILabelSizer.size(for: value, font: font)
|
|
||||||
var r = CGRect.zero
|
|
||||||
r.size = textFieldSize
|
|
||||||
r.origin.y = rectAbove.maxY + topMargin
|
|
||||||
r.origin.x = textBoxRect.origin.x
|
|
||||||
|
|
||||||
var width = textFieldSize.width
|
|
||||||
width = min(width, textBoxRect.size.width)
|
|
||||||
width = max(width, 0.0)
|
|
||||||
r.size.width = width
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rectForUnreadIndicator(_ titleRect: CGRect) -> CGRect {
|
|
||||||
|
|
||||||
var r = CGRect.zero
|
var r = CGRect.zero
|
||||||
r.size = CGSize(width: MasterTimelineCellLayout.unreadCircleDimension, height: MasterTimelineCellLayout.unreadCircleDimension)
|
r.size = CGSize(width: MasterTimelineCellLayout.unreadCircleDimension, height: MasterTimelineCellLayout.unreadCircleDimension)
|
||||||
r.origin.x = MasterTimelineCellLayout.cellPadding.left
|
r.origin.x = point.x
|
||||||
r.origin.y = titleRect.minY + 6
|
r.origin.y = point.y + 9
|
||||||
return r
|
return r
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func rectForStar(_ unreadIndicatorRect: CGRect) -> CGRect {
|
|
||||||
|
|
||||||
|
static func rectForStar(_ point: CGPoint) -> CGRect {
|
||||||
var r = CGRect.zero
|
var r = CGRect.zero
|
||||||
r.size.width = MasterTimelineCellLayout.starDimension
|
r.size.width = MasterTimelineCellLayout.starDimension
|
||||||
r.size.height = MasterTimelineCellLayout.starDimension
|
r.size.height = MasterTimelineCellLayout.starDimension
|
||||||
r.origin.x = floor(unreadIndicatorRect.origin.x - ((MasterTimelineCellLayout.starDimension - MasterTimelineCellLayout.unreadCircleDimension) / 2.0))
|
r.origin.x = floor(point.x - ((MasterTimelineCellLayout.starDimension - MasterTimelineCellLayout.unreadCircleDimension) / 2.0))
|
||||||
r.origin.y = unreadIndicatorRect.origin.y - 4.0
|
r.origin.y = point.y + 5
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
static func rectForAvatar(_ cellData: MasterTimelineCellData, _ showAvatar: Bool, _ textBoxRect: CGRect, _ width: CGFloat, _ height: CGFloat) -> CGRect {
|
static func rectForAvatar(_ point: CGPoint) -> CGRect {
|
||||||
|
var r = CGRect.zero
|
||||||
|
r.size = MasterTimelineCellLayout.avatarSize
|
||||||
|
r.origin = point
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
static func rectForTitle(_ cellData: MasterTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) {
|
||||||
|
|
||||||
var r = CGRect.zero
|
var r = CGRect.zero
|
||||||
if !showAvatar {
|
if cellData.title.isEmpty {
|
||||||
|
return (r, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.origin = point
|
||||||
|
|
||||||
|
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MasterTimelineCellLayout.titleFont, numberOfLines: MasterTimelineCellLayout.maxNumberOfLines, width: Int(textAreaWidth))
|
||||||
|
|
||||||
|
r.size.width = textAreaWidth
|
||||||
|
r.size.height = sizeInfo.size.height
|
||||||
|
if sizeInfo.numberOfLinesUsed < 1 {
|
||||||
|
r.size.height = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return (r, sizeInfo.numberOfLinesUsed)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static func rectForSummary(_ cellData: MasterTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect {
|
||||||
|
|
||||||
|
let linesLeft = MasterTimelineCellLayout.maxNumberOfLines - linesUsed
|
||||||
|
|
||||||
|
var r = CGRect.zero
|
||||||
|
if cellData.summary.isEmpty || linesLeft < 1 {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
r.size = MasterTimelineCellLayout.avatarSize
|
r.origin = point
|
||||||
r.origin.x = (width - MasterTimelineCellLayout.cellPadding.right) - r.size.width
|
|
||||||
r.origin.y = textBoxRect.origin.y + 4.0
|
let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MasterTimelineCellLayout.summaryFont, numberOfLines: linesLeft, width: Int(textAreaWidth))
|
||||||
|
|
||||||
|
r.size.width = textAreaWidth
|
||||||
|
r.size.height = sizeInfo.size.height
|
||||||
|
if sizeInfo.numberOfLinesUsed < 1 {
|
||||||
|
r.size.height = 0
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func rectForDate(_ cellData: MasterTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||||
|
|
||||||
|
var r = CGRect.zero
|
||||||
|
|
||||||
|
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MasterTimelineCellLayout.dateFont)
|
||||||
|
r.size = size
|
||||||
|
r.origin.x = (point.x + textAreaWidth) - size.width
|
||||||
|
r.origin.y = point.y
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static func rectForFeedName(_ cellData: MasterTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||||
|
|
||||||
|
var r = CGRect.zero
|
||||||
|
r.origin = point
|
||||||
|
|
||||||
|
let size = SingleLineUILabelSizer.size(for: cellData.feedName, font: MasterTimelineCellLayout.feedNameFont)
|
||||||
|
r.size = size
|
||||||
|
|
||||||
|
if r.size.width > textAreaWidth {
|
||||||
|
r.size.width = textAreaWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,8 +12,7 @@ import RSCore
|
||||||
class MasterTimelineTableViewCell: UITableViewCell {
|
class MasterTimelineTableViewCell: UITableViewCell {
|
||||||
|
|
||||||
private let titleView = MasterTimelineTableViewCell.multiLineUILabel()
|
private let titleView = MasterTimelineTableViewCell.multiLineUILabel()
|
||||||
private let summaryView = MasterTimelineTableViewCell.singleLineUILabel()
|
private let summaryView = MasterTimelineTableViewCell.multiLineUILabel()
|
||||||
private let textView = MasterTimelineTableViewCell.multiLineUILabel()
|
|
||||||
private let unreadIndicatorView = MasterUnreadIndicatorView(frame: CGRect.zero)
|
private let unreadIndicatorView = MasterUnreadIndicatorView(frame: CGRect.zero)
|
||||||
private let dateView = MasterTimelineTableViewCell.singleLineUILabel()
|
private let dateView = MasterTimelineTableViewCell.singleLineUILabel()
|
||||||
private let feedNameView = MasterTimelineTableViewCell.singleLineUILabel()
|
private let feedNameView = MasterTimelineTableViewCell.singleLineUILabel()
|
||||||
|
@ -28,10 +27,6 @@ class MasterTimelineTableViewCell: UITableViewCell {
|
||||||
return NonIntrinsicImageView(image: AppAssets.timelineStarImage)
|
return NonIntrinsicImageView(image: AppAssets.timelineStarImage)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private lazy var textFields = {
|
|
||||||
return [self.dateView, self.feedNameView, self.titleView, self.summaryView, self.textView]
|
|
||||||
}()
|
|
||||||
|
|
||||||
var cellData: MasterTimelineCellData! {
|
var cellData: MasterTimelineCellData! {
|
||||||
didSet {
|
didSet {
|
||||||
updateSubviews()
|
updateSubviews()
|
||||||
|
@ -49,21 +44,24 @@ class MasterTimelineTableViewCell: UITableViewCell {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||||
|
let layout = updatedLayout()
|
||||||
|
return CGSize(width: bounds.width, height: layout.height)
|
||||||
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
|
|
||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
let layout = updatedLayout()
|
let layout = updatedLayout()
|
||||||
|
|
||||||
|
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
|
||||||
|
starView.setFrameIfNotEqual(layout.starRect)
|
||||||
|
avatarImageView.setFrameIfNotEqual(layout.avatarImageRect)
|
||||||
setFrame(for: titleView, rect: layout.titleRect)
|
setFrame(for: titleView, rect: layout.titleRect)
|
||||||
setFrame(for: summaryView, rect: layout.summaryRect)
|
setFrame(for: summaryView, rect: layout.summaryRect)
|
||||||
setFrame(for: textView, rect: layout.textRect)
|
|
||||||
|
|
||||||
dateView.setFrameIfNotEqual(layout.dateRect)
|
|
||||||
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
|
|
||||||
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
|
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
|
||||||
avatarImageView.setFrameIfNotEqual(layout.avatarImageRect)
|
dateView.setFrameIfNotEqual(layout.dateRect)
|
||||||
starView.setFrameIfNotEqual(layout.starRect)
|
|
||||||
|
|
||||||
separatorInset = layout.separatorInsets
|
separatorInset = layout.separatorInsets
|
||||||
|
|
||||||
|
@ -79,6 +77,7 @@ private extension MasterTimelineTableViewCell {
|
||||||
let label = NonIntrinsicLabel()
|
let label = NonIntrinsicLabel()
|
||||||
label.lineBreakMode = .byTruncatingTail
|
label.lineBreakMode = .byTruncatingTail
|
||||||
label.allowsDefaultTighteningForTruncation = false
|
label.allowsDefaultTighteningForTruncation = false
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +86,7 @@ private extension MasterTimelineTableViewCell {
|
||||||
label.numberOfLines = 0
|
label.numberOfLines = 0
|
||||||
label.lineBreakMode = .byWordWrapping
|
label.lineBreakMode = .byWordWrapping
|
||||||
label.allowsDefaultTighteningForTruncation = false
|
label.allowsDefaultTighteningForTruncation = false
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,6 @@ private extension MasterTimelineTableViewCell {
|
||||||
addAccessoryView()
|
addAccessoryView()
|
||||||
addSubviewAtInit(titleView, hidden: false)
|
addSubviewAtInit(titleView, hidden: false)
|
||||||
addSubviewAtInit(summaryView, hidden: true)
|
addSubviewAtInit(summaryView, hidden: true)
|
||||||
addSubviewAtInit(textView, hidden: true)
|
|
||||||
addSubviewAtInit(unreadIndicatorView, hidden: true)
|
addSubviewAtInit(unreadIndicatorView, hidden: true)
|
||||||
addSubviewAtInit(dateView, hidden: false)
|
addSubviewAtInit(dateView, hidden: false)
|
||||||
addSubviewAtInit(feedNameView, hidden: true)
|
addSubviewAtInit(feedNameView, hidden: true)
|
||||||
|
@ -133,8 +132,7 @@ private extension MasterTimelineTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updatedLayout() -> MasterTimelineCellLayout {
|
func updatedLayout() -> MasterTimelineCellLayout {
|
||||||
|
return MasterTimelineCellLayout(width: bounds.width, insets: safeAreaInsets, cellData: cellData, showAvatar: avatarImageView.image != nil)
|
||||||
return MasterTimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, hasAvatar: avatarImageView.image != nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTitleView() {
|
func updateTitleView() {
|
||||||
|
@ -144,15 +142,9 @@ private extension MasterTimelineTableViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSummaryView() {
|
func updateSummaryView() {
|
||||||
summaryView.font = MasterTimelineCellLayout.textFont
|
summaryView.font = MasterTimelineCellLayout.summaryFont
|
||||||
summaryView.textColor = MasterTimelineCellLayout.textColor
|
summaryView.textColor = MasterTimelineCellLayout.summaryColor
|
||||||
updateTextFieldText(summaryView, cellData?.text)
|
updateTextFieldText(summaryView, cellData?.summary)
|
||||||
}
|
|
||||||
|
|
||||||
func updateTextView() {
|
|
||||||
textView.font = MasterTimelineCellLayout.textFont
|
|
||||||
textView.textColor = MasterTimelineCellLayout.textColor
|
|
||||||
updateTextFieldText(textView, cellData?.text)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDateView() {
|
func updateDateView() {
|
||||||
|
@ -234,7 +226,6 @@ private extension MasterTimelineTableViewCell {
|
||||||
func updateSubviews() {
|
func updateSubviews() {
|
||||||
updateTitleView()
|
updateTitleView()
|
||||||
updateSummaryView()
|
updateSummaryView()
|
||||||
updateTextView()
|
|
||||||
updateDateView()
|
updateDateView()
|
||||||
updateFeedNameView()
|
updateFeedNameView()
|
||||||
updateUnreadIndicator()
|
updateUnreadIndicator()
|
||||||
|
|
|
@ -14,12 +14,6 @@ import Articles
|
||||||
class MasterTimelineViewController: ProgressTableViewController, UndoableCommandRunner {
|
class MasterTimelineViewController: ProgressTableViewController, UndoableCommandRunner {
|
||||||
|
|
||||||
private static var minAvatarDimension: CGFloat = 20.0
|
private static var minAvatarDimension: CGFloat = 20.0
|
||||||
private var rowHeightWithFeedName: CGFloat = 0.0
|
|
||||||
private var rowHeightWithoutFeedName: CGFloat = 0.0
|
|
||||||
|
|
||||||
private var currentRowHeight: CGFloat {
|
|
||||||
return navState?.showFeedNames ?? false ? rowHeightWithFeedName : rowHeightWithoutFeedName
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
|
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
|
||||||
@IBOutlet weak var firstUnreadButton: UIBarButtonItem!
|
@IBOutlet weak var firstUnreadButton: UIBarButtonItem!
|
||||||
|
@ -34,7 +28,6 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
updateRowHeights()
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
|
||||||
|
@ -312,28 +305,6 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Cell Configuring
|
|
||||||
|
|
||||||
private func calculateRowHeight(showingFeedNames: Bool) -> CGFloat {
|
|
||||||
|
|
||||||
let longTitle = "But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
|
|
||||||
|
|
||||||
let prototypeID = "prototype"
|
|
||||||
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, userDeleted: false, dateArrived: Date())
|
|
||||||
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: nil, dateModified: nil, authors: nil, attachments: nil, status: status)
|
|
||||||
|
|
||||||
let prototypeCellData = MasterTimelineCellData(article: prototypeArticle, showFeedName: showingFeedNames, feedName: "Prototype Feed Name", avatar: nil, showAvatar: false, featuredImage: nil)
|
|
||||||
let height = MasterTimelineCellLayout.height(for: 100, cellData: prototypeCellData)
|
|
||||||
return height
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateRowHeights() {
|
|
||||||
rowHeightWithFeedName = calculateRowHeight(showingFeedNames: true)
|
|
||||||
rowHeightWithoutFeedName = calculateRowHeight(showingFeedNames: false)
|
|
||||||
updateTableViewRowHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
@ -347,7 +318,6 @@ private extension MasterTimelineViewController {
|
||||||
|
|
||||||
func resetUI() {
|
func resetUI() {
|
||||||
|
|
||||||
updateTableViewRowHeight()
|
|
||||||
title = navState?.timelineName
|
title = navState?.timelineName
|
||||||
navigationController?.title = navState?.timelineName
|
navigationController?.title = navState?.timelineName
|
||||||
|
|
||||||
|
@ -431,11 +401,6 @@ private extension MasterTimelineViewController {
|
||||||
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
|
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTableViewRowHeight() {
|
|
||||||
tableView.rowHeight = currentRowHeight
|
|
||||||
tableView.estimatedRowHeight = currentRowHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
func performBlockAndRestoreSelection(_ block: (() -> Void)) {
|
||||||
let indexPaths = tableView.indexPathsForSelectedRows
|
let indexPaths = tableView.indexPathsForSelectedRows
|
||||||
block()
|
block()
|
||||||
|
|
Loading…
Reference in New Issue