Rename MainFeed and MainTimeline folders to Sidebar and Timeline.

This commit is contained in:
Brent Simmons
2025-02-02 11:34:11 -08:00
parent b294fbcc58
commit 2c6c8a7240
26 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
//
// MainTimelineAccessibilityCellLayout.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 4/29/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
let height: CGFloat
let unreadIndicatorRect: CGRect
let starRect: CGRect
let iconImageRect: CGRect
let titleRect: CGRect
let summaryRect: CGRect
let feedNameRect: CGRect
let dateRect: CGRect
init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) {
var currentPoint = CGPoint.zero
currentPoint.x = MainTimelineDefaultCellLayout.cellPadding.left + insets.left + MainTimelineDefaultCellLayout.unreadCircleMarginLeft
currentPoint.y = MainTimelineDefaultCellLayout.cellPadding.top
// Unread Indicator and Star
self.unreadIndicatorRect = MainTimelineAccessibilityCellLayout.rectForUnreadIndicator(currentPoint)
self.starRect = MainTimelineAccessibilityCellLayout.rectForStar(currentPoint)
// Start the point at the beginning position of the main block
currentPoint.x += MainTimelineDefaultCellLayout.unreadCircleDimension + MainTimelineDefaultCellLayout.unreadCircleMarginRight
// Icon Image
if cellData.showIcon {
self.iconImageRect = MainTimelineAccessibilityCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
currentPoint.y = self.iconImageRect.maxY
} else {
self.iconImageRect = CGRect.zero
}
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
// Title Text Block
let (titleRect, numberOfLinesForTitle) = MainTimelineAccessibilityCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
self.titleRect = titleRect
// Summary Text Block
if self.titleRect != CGRect.zero {
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
}
self.summaryRect = MainTimelineAccessibilityCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
currentPoint.y = [self.titleRect, self.summaryRect].maxY()
if cellData.showFeedName != .none {
self.feedNameRect = MainTimelineAccessibilityCellLayout.rectForFeedName(cellData, currentPoint, textAreaWidth)
currentPoint.y = self.feedNameRect.maxY
} else {
self.feedNameRect = CGRect.zero
}
// Feed Name and Pub Date
self.dateRect = MainTimelineAccessibilityCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
self.height = self.dateRect.maxY + MainTimelineDefaultCellLayout.cellPadding.bottom
}
}
// MARK: - Calculate Rects
private extension MainTimelineAccessibilityCellLayout {
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
r.size = size
r.origin = point
return r
}
}

View File

@@ -0,0 +1,84 @@
//
// MainTimelineCellData.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import Articles
struct MainTimelineCellData {
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
let title: String
let attributedTitle: NSAttributedString
let summary: String
let dateString: String
let feedName: String
let byline: String
let showFeedName: ShowFeedName
let iconImage: IconImage? // feed icon, user avatar, or favicon
let showIcon: Bool // Make space even when icon is nil
let read: Bool
let starred: Bool
let numberOfLines: Int
let iconSize: IconSize
init(article: Article, showFeedName: ShowFeedName, feedName: String?, byline: String?, iconImage: IconImage?, showIcon: Bool, numberOfLines: Int, iconSize: IconSize) {
self.title = ArticleStringFormatter.truncatedTitle(article)
self.attributedTitle = ArticleStringFormatter.attributedTruncatedTitle(article)
let truncatedSummary = ArticleStringFormatter.truncatedSummary(article)
if self.title.isEmpty && truncatedSummary.isEmpty {
self.summary = Self.noText
} else {
self.summary = truncatedSummary
}
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
if let feedName = feedName {
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
} else {
self.feedName = ""
}
if let byline = byline {
self.byline = byline
} else {
self.byline = ""
}
self.showFeedName = showFeedName
self.showIcon = showIcon
self.iconImage = iconImage
self.read = article.status.read
self.starred = article.status.starred
self.numberOfLines = numberOfLines
self.iconSize = iconSize
}
init() { // Empty
self.title = ""
self.attributedTitle = NSAttributedString()
self.summary = ""
self.dateString = ""
self.feedName = ""
self.byline = ""
self.showFeedName = .none
self.showIcon = false
self.iconImage = nil
self.read = true
self.starred = false
self.numberOfLines = 0
self.iconSize = .medium
}
}

View File

@@ -0,0 +1,112 @@
//
// MainTimelineCellLayout.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 4/29/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
protocol MainTimelineCellLayout {
var height: CGFloat {get}
var unreadIndicatorRect: CGRect {get}
var starRect: CGRect {get}
var iconImageRect: CGRect {get}
var titleRect: CGRect {get}
var summaryRect: CGRect {get}
var feedNameRect: CGRect {get}
var dateRect: CGRect {get}
}
extension MainTimelineCellLayout {
static func rectForUnreadIndicator(_ point: CGPoint) -> CGRect {
var r = CGRect.zero
r.size = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension)
r.origin.x = point.x
r.origin.y = point.y + 5
return r
}
static func rectForStar(_ point: CGPoint) -> CGRect {
var r = CGRect.zero
r.size.width = MainTimelineDefaultCellLayout.starDimension
r.size.height = MainTimelineDefaultCellLayout.starDimension
r.origin.x = floor(point.x - ((MainTimelineDefaultCellLayout.starDimension - MainTimelineDefaultCellLayout.unreadCircleDimension) / 2.0))
r.origin.y = point.y + 3
return r
}
static func rectForIconView(_ point: CGPoint, iconSize: IconSize) -> CGRect {
var r = CGRect.zero
r.size = iconSize.size
r.origin.x = point.x
r.origin.y = point.y + 4
return r
}
static func rectForTitle(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) {
var r = CGRect.zero
if cellData.title.isEmpty {
return (r, 0)
}
r.origin = point
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MainTimelineDefaultCellLayout.titleFont, numberOfLines: cellData.numberOfLines, 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: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect {
let linesLeft = cellData.numberOfLines - linesUsed
var r = CGRect.zero
if cellData.summary.isEmpty || linesLeft < 1 {
return r
}
r.origin = point
let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MainTimelineDefaultCellLayout.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
}
static func rectForFeedName(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero
r.origin = point
let feedName = cellData.showFeedName == .feed ? cellData.feedName : cellData.byline
let size = SingleLineUILabelSizer.size(for: feedName, font: MainTimelineDefaultCellLayout.feedNameFont)
r.size = size
if r.size.width > textAreaWidth {
r.size.width = textAreaWidth
}
return r
}
}

View File

@@ -0,0 +1,126 @@
//
// MainTimelineDefaultCellLayout.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/6/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20)
static let unreadCircleMarginLeft = CGFloat(integerLiteral: 0)
static let unreadCircleDimension = CGFloat(integerLiteral: 12)
static let unreadCircleSize = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension)
static let unreadCircleMarginRight = CGFloat(integerLiteral: 8)
static let starDimension = CGFloat(integerLiteral: 16)
static let starSize = CGSize(width: MainTimelineDefaultCellLayout.starDimension, height: MainTimelineDefaultCellLayout.starDimension)
static let iconMarginRight = CGFloat(integerLiteral: 8)
static let iconCornerRadius = CGFloat(integerLiteral: 4)
static var titleFont: UIFont {
return UIFont.preferredFont(forTextStyle: .headline)
}
static let titleBottomMargin = CGFloat(integerLiteral: 1)
static var feedNameFont: UIFont {
return UIFont.preferredFont(forTextStyle: .footnote)
}
static let feedRightMargin = CGFloat(integerLiteral: 8)
static var dateFont: UIFont {
return UIFont.preferredFont(forTextStyle: .footnote)
}
static let dateMarginBottom = CGFloat(integerLiteral: 1)
static var summaryFont: UIFont {
return UIFont.preferredFont(forTextStyle: .body)
}
let height: CGFloat
let unreadIndicatorRect: CGRect
let starRect: CGRect
let iconImageRect: CGRect
let titleRect: CGRect
let summaryRect: CGRect
let feedNameRect: CGRect
let dateRect: CGRect
init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) {
var currentPoint = CGPoint.zero
currentPoint.x = MainTimelineDefaultCellLayout.cellPadding.left + insets.left + MainTimelineDefaultCellLayout.unreadCircleMarginLeft
currentPoint.y = MainTimelineDefaultCellLayout.cellPadding.top
// Unread Indicator and Star
self.unreadIndicatorRect = MainTimelineDefaultCellLayout.rectForUnreadIndicator(currentPoint)
self.starRect = MainTimelineDefaultCellLayout.rectForStar(currentPoint)
// Start the point at the beginning position of the main block
currentPoint.x += MainTimelineDefaultCellLayout.unreadCircleDimension + MainTimelineDefaultCellLayout.unreadCircleMarginRight
// Icon Image
if cellData.showIcon {
self.iconImageRect = MainTimelineDefaultCellLayout.rectForIconView(currentPoint, iconSize: cellData.iconSize)
currentPoint.x = self.iconImageRect.maxX + MainTimelineDefaultCellLayout.iconMarginRight
} else {
self.iconImageRect = CGRect.zero
}
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
// Title Text Block
let (titleRect, numberOfLinesForTitle) = MainTimelineDefaultCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
self.titleRect = titleRect
// Summary Text Block
if self.titleRect != CGRect.zero {
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
}
self.summaryRect = MainTimelineDefaultCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
var y = [self.titleRect, self.summaryRect].maxY()
if y == 0 {
y = iconImageRect.origin.y + iconImageRect.height
// Necessary calculation of either feed name or date since we are working with dynamic font-sizes
let tmp = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
y -= tmp.height
}
currentPoint.y = y
// Feed Name and Pub Date
self.dateRect = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
let feedNameWidth = textAreaWidth - (MainTimelineDefaultCellLayout.feedRightMargin + self.dateRect.size.width)
self.feedNameRect = MainTimelineDefaultCellLayout.rectForFeedName(cellData, currentPoint, feedNameWidth)
self.height = [self.iconImageRect, self.feedNameRect].maxY() + MainTimelineDefaultCellLayout.cellPadding.bottom
}
}
// MARK: - Calculate Rects
extension MainTimelineDefaultCellLayout {
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
r.size = size
r.origin.x = (point.x + textAreaWidth) - size.width
r.origin.y = point.y
return r
}
}

View File

@@ -0,0 +1,314 @@
//
// MainTimelineTableViewCell.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/31/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
final class MainTimelineTableViewCell: VibrantTableViewCell {
private let titleView = MainTimelineTableViewCell.multiLineUILabel()
private let summaryView = MainTimelineTableViewCell.multiLineUILabel()
private let unreadIndicatorView = MainUnreadIndicatorView(frame: CGRect.zero)
private let dateView = MainTimelineTableViewCell.singleLineUILabel()
private let feedNameView = MainTimelineTableViewCell.singleLineUILabel()
private lazy var iconView = IconView()
private lazy var starView = {
return NonIntrinsicImageView(image: AppImage.timelineStar)
}()
private var unreadIndicatorPropertyAnimator: UIViewPropertyAnimator?
private var starViewPropertyAnimator: UIViewPropertyAnimator?
var cellData: MainTimelineCellData! {
didSet {
updateSubviews()
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func prepareForReuse() {
unreadIndicatorPropertyAnimator?.stopAnimation(true)
unreadIndicatorPropertyAnimator = nil
unreadIndicatorView.isHidden = true
starViewPropertyAnimator?.stopAnimation(true)
starViewPropertyAnimator = nil
starView.isHidden = true
}
override var frame: CGRect {
didSet {
setNeedsLayout()
}
}
override func updateVibrancy(animated: Bool) {
updateLabelVibrancy(titleView, color: labelColor, animated: animated)
updateLabelVibrancy(summaryView, color: labelColor, animated: animated)
updateLabelVibrancy(dateView, color: secondaryLabelColor, animated: animated)
updateLabelVibrancy(feedNameView, color: secondaryLabelColor, animated: animated)
if animated {
UIView.animate(withDuration: Self.duration) {
if self.isHighlighted || self.isSelected {
self.unreadIndicatorView.backgroundColor = AppColor.vibrantText
} else {
self.unreadIndicatorView.backgroundColor = AppColor.secondaryAccent
}
}
} else {
if self.isHighlighted || self.isSelected {
self.unreadIndicatorView.backgroundColor = AppColor.vibrantText
} else {
self.unreadIndicatorView.backgroundColor = AppColor.secondaryAccent
}
}
}
override func sizeThatFits(_ size: CGSize) -> CGSize {
let layout = updatedLayout(width: size.width)
return CGSize(width: size.width, height: layout.height)
}
override func layoutSubviews() {
super.layoutSubviews()
let layout = updatedLayout(width: bounds.width)
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
starView.setFrameIfNotEqual(layout.starRect)
iconView.setFrameIfNotEqual(layout.iconImageRect)
setFrame(for: titleView, rect: layout.titleRect)
setFrame(for: summaryView, rect: layout.summaryRect)
feedNameView.setFrameIfNotEqual(layout.feedNameRect)
dateView.setFrameIfNotEqual(layout.dateRect)
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func setIconImage(_ image: IconImage) {
iconView.iconImage = image
}
}
// MARK: - Private
private extension MainTimelineTableViewCell {
static func singleLineUILabel() -> UILabel {
let label = NonIntrinsicLabel()
label.lineBreakMode = .byTruncatingTail
label.allowsDefaultTighteningForTruncation = false
label.adjustsFontForContentSizeCategory = true
return label
}
static func multiLineUILabel() -> UILabel {
let label = NonIntrinsicLabel()
label.numberOfLines = 0
label.lineBreakMode = .byTruncatingTail
label.allowsDefaultTighteningForTruncation = false
label.adjustsFontForContentSizeCategory = true
return label
}
func setFrame(for label: UILabel, rect: CGRect) {
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
hideView(label)
} else {
showView(label)
label.setFrameIfNotEqual(rect)
}
}
func addSubviewAtInit(_ view: UIView, hidden: Bool) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = hidden
}
func commonInit() {
addSubviewAtInit(titleView, hidden: false)
addSubviewAtInit(summaryView, hidden: true)
addSubviewAtInit(unreadIndicatorView, hidden: true)
addSubviewAtInit(dateView, hidden: false)
addSubviewAtInit(feedNameView, hidden: true)
addSubviewAtInit(iconView, hidden: true)
addSubviewAtInit(starView, hidden: true)
}
func updatedLayout(width: CGFloat) -> MainTimelineCellLayout {
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
return MainTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
} else {
return MainTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
}
}
func updateTitleView() {
titleView.font = MainTimelineDefaultCellLayout.titleFont
titleView.textColor = labelColor
updateTextFieldAttributedText(titleView, cellData?.attributedTitle)
}
func updateSummaryView() {
summaryView.font = MainTimelineDefaultCellLayout.summaryFont
summaryView.textColor = labelColor
updateTextFieldText(summaryView, cellData?.summary)
}
func updateDateView() {
dateView.font = MainTimelineDefaultCellLayout.dateFont
dateView.textColor = secondaryLabelColor
updateTextFieldText(dateView, cellData.dateString)
}
func updateTextFieldText(_ label: UILabel, _ text: String?) {
let s = text ?? ""
if label.text != s {
label.text = s
setNeedsLayout()
}
}
func updateTextFieldAttributedText(_ label: UILabel, _ text: NSAttributedString?) {
var s = text ?? NSAttributedString(string: "")
if let fieldFont = label.font {
s = s.adding(font: fieldFont)
}
if label.attributedText != s {
label.attributedText = s
setNeedsLayout()
}
}
func updateFeedNameView() {
switch cellData.showFeedName {
case .feed:
showView(feedNameView)
feedNameView.font = MainTimelineDefaultCellLayout.feedNameFont
feedNameView.textColor = secondaryLabelColor
updateTextFieldText(feedNameView, cellData.feedName)
case .byline:
showView(feedNameView)
feedNameView.font = MainTimelineDefaultCellLayout.feedNameFont
feedNameView.textColor = secondaryLabelColor
updateTextFieldText(feedNameView, cellData.byline)
case .none:
hideView(feedNameView)
}
}
func updateUnreadIndicator() {
if !unreadIndicatorView.isHidden && cellData.read && !cellData.starred {
unreadIndicatorPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
self?.unreadIndicatorView.alpha = 0
}
unreadIndicatorPropertyAnimator?.addCompletion { [weak self] _ in
self?.unreadIndicatorView.isHidden = true
self?.unreadIndicatorView.alpha = 1
self?.unreadIndicatorPropertyAnimator = nil
}
unreadIndicatorPropertyAnimator?.startAnimation()
} else {
unreadIndicatorView.alpha = 1
showOrHideView(unreadIndicatorView, cellData.read || cellData.starred)
}
}
func updateStarView() {
if !starView.isHidden && cellData.read && !cellData.starred {
starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
self?.starView.alpha = 0
}
starViewPropertyAnimator?.addCompletion { [weak self] _ in
self?.starView.isHidden = true
self?.starView.alpha = 1
self?.starViewPropertyAnimator = nil
}
starViewPropertyAnimator?.startAnimation()
} else {
starView.alpha = 1
showOrHideView(starView, !cellData.starred)
}
}
func updateIconImage() {
guard let image = cellData.iconImage, cellData.showIcon else {
makeIconEmpty()
return
}
showView(iconView)
if iconView.iconImage !== cellData.iconImage {
iconView.iconImage = image
setNeedsLayout()
}
}
func updateAccessiblityLabel() {
let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : ""
let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), "
let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)"
accessibilityLabel = label
}
func makeIconEmpty() {
if iconView.iconImage != nil {
iconView.iconImage = nil
setNeedsLayout()
}
hideView(iconView)
}
func hideView(_ view: UIView) {
if !view.isHidden {
view.isHidden = true
}
}
func showView(_ view: UIView) {
if view.isHidden {
view.isHidden = false
}
}
func showOrHideView(_ view: UIView, _ shouldHide: Bool) {
if shouldHide {
hideView(view)
} else {
showView(view)
}
}
func updateSubviews() {
updateTitleView()
updateSummaryView()
updateDateView()
updateFeedNameView()
updateUnreadIndicator()
updateStarView()
updateIconImage()
updateAccessiblityLabel()
}
}

View File

@@ -0,0 +1,19 @@
//
// MainUnreadIndicatorView.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/16/16.
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
//
import UIKit
final class MainUnreadIndicatorView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = frame.size.width / 2.0
clipsToBounds = true
}
}

View File

@@ -0,0 +1,181 @@
//
// UILabelSizerSpecifier.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import UIKit
// Get the height of an NSTextField given a string, font, and width.
// Uses a cache. Avoids actually measuring text as much as possible.
// Main thread only.
typealias WidthHeightCache = [Int: Int] // width: height
private struct UILabelSizerSpecifier: Hashable {
let numberOfLines: Int
let font: UIFont
}
struct TextFieldSizeInfo {
let size: CGSize // Integral size (ceiled)
let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then.
}
final class MultilineUILabelSizer {
private let numberOfLines: Int
private let font: UIFont
private let singleLineHeightEstimate: Int
private let doubleLineHeightEstimate: Int
private var cache = [String: WidthHeightCache]() // Each string has a cache.
private static var sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
private init(numberOfLines: Int, font: UIFont) {
self.numberOfLines = numberOfLines
self.font = font
self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y", 200, font)
self.doubleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font)
}
static func size(for string: String, font: UIFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo {
return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width)
}
static func emptyCache() {
sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
}
}
// MARK: - Private
private extension MultilineUILabelSizer {
static func sizer(numberOfLines: Int, font: UIFont) -> MultilineUILabelSizer {
let specifier = UILabelSizerSpecifier(numberOfLines: numberOfLines, font: font)
if let cachedSizer = sizers[specifier] {
return cachedSizer
}
let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font)
sizers[specifier] = newSizer
return newSizer
}
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
let textFieldHeight = height(for: string, width: width)
let numberOfLinesUsed = numberOfLines(for: textFieldHeight)
let size = CGSize(width: width, height: textFieldHeight)
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
return sizeInfo
}
func height(for string: String, width: Int) -> Int {
if cache[string] == nil {
cache[string] = WidthHeightCache()
}
if let height = cache[string]![width] {
return height
}
if let height = heightConsideringNeighbors(cache[string]!, width) {
return height
}
var height = MultilineUILabelSizer.calculateHeight(string, width, font)
if numberOfLines != 0 {
let maxHeight = singleLineHeightEstimate * numberOfLines
if height > maxHeight {
height = maxHeight
}
}
cache[string]![width] = height
return height
}
static func calculateHeight(_ string: String, _ width: Int, _ font: UIFont) -> Int {
let height = string.height(withConstrainedWidth: CGFloat(width), font: font)
return Int(ceil(height))
}
func numberOfLines(for height: Int) -> Int {
// Well have to see if this really works reliably.
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
let lines = Int(round(CGFloat(height) / averageHeight))
return lines
}
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate)
}
func heightIsProbablyDoubleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, doubleLineHeightEstimate)
}
func heightIsProbablyEqualToEstimate(_ height: Int, _ estimate: Int) -> Bool {
let slop = 4
let minimum = estimate - slop
let maximum = estimate + slop
return height >= minimum && height <= maximum
}
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
// Given width, if the height at width - something and width + something is equal,
// then that height must be correct for the given width.
// Also:
// If a narrower neighbors height is single line height, then this wider width must also be single-line height.
// If a wider neighbors height is double line height, and numberOfLines == 2, then this narrower width must able be double-line height.
var smallNeighbor = (width: 0, height: 0)
var largeNeighbor = (width: 0, height: 0)
for (oneWidth, oneHeight) in heightCache {
if oneWidth < width && heightIsProbablySingleLineHeight(oneHeight) {
return oneHeight
}
if numberOfLines == 2 && oneWidth > width && heightIsProbablyDoubleLineHeight(oneHeight) {
return oneHeight
}
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
smallNeighbor = (oneWidth, oneHeight)
} else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
largeNeighbor = (oneWidth, oneHeight)
}
if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height {
return smallNeighbor.height
}
}
return nil
}
}

View File

@@ -0,0 +1,64 @@
//
// SingleLineUILabelSizer.swift
// NetNewsWire
//
// Created by Brent Simmons on 2/19/18.
// Copyright © 2018 Ranchero Software. All rights reserved.
//
import UIKit
// Get the size of an UILabel configured with a specific font with a specific size.
// Uses a cache.
// Main thready only.
final class SingleLineUILabelSizer {
let font: UIFont
private var cache = [String: CGSize]()
init(font: UIFont) {
self.font = font
}
func size(for text: String) -> CGSize {
if let cachedSize = cache[text] {
return cachedSize
}
let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font)
let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font)
let calculatedSize = CGSize(width: ceil(width), height: ceil(height))
cache[text] = calculatedSize
return calculatedSize
}
static private var sizers = [UIFont: SingleLineUILabelSizer]()
static func sizer(for font: UIFont) -> SingleLineUILabelSizer {
if let cachedSizer = sizers[font] {
return cachedSizer
}
let newSizer = SingleLineUILabelSizer(font: font)
sizers[font] = newSizer
return newSizer
}
// Use this call. Its easiest.
static func size(for text: String, font: UIFont) -> CGSize {
return sizer(for: font).size(for: text)
}
static func emptyCache() {
sizers = [UIFont: SingleLineUILabelSizer]()
}
}

View File

@@ -0,0 +1,16 @@
//
// MainTimelineDataSource.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 8/30/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
final class MainTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
}

View File

@@ -0,0 +1,56 @@
//
// MainTimelineTitleView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 9/21/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
final class MainTimelineTitleView: UIView {
@IBOutlet weak var iconView: IconView!
@IBOutlet weak var label: UILabel!
@IBOutlet weak var unreadCountView: MainTimelineUnreadCountView!
private lazy var pointerInteraction: UIPointerInteraction = {
UIPointerInteraction(delegate: self)
}()
override var accessibilityLabel: String? {
get {
if let name = label.text {
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)"
} else {
return nil
}
}
set {
}
}
func buttonize() {
heightAnchor.constraint(equalToConstant: 40.0).isActive = true
accessibilityTraits = .button
addInteraction(pointerInteraction)
}
func debuttonize() {
heightAnchor.constraint(equalToConstant: 40.0).isActive = true
accessibilityTraits.remove(.button)
removeInteraction(pointerInteraction)
}
}
extension MainTimelineTitleView: UIPointerInteractionDelegate {
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
var rect = self.frame
rect.origin.x -= 10
rect.size.width += 20
return UIPointerStyle(effect: .automatic(UITargetedPreview(view: self)), shape: .roundedRect(rect))
}
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="MainTimelineTitleView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="190" height="38"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="250" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5F6-2v-qSS">
<rect key="frame" x="28" y="9" width="43.5" height="20"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="z9o-XA-3t4" customClass="MainTimelineUnreadCountView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="79.5" y="9" width="110.5" height="20"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5gI-Wl-lnK" customClass="IconView" customModule="NetNewsWire" customModuleProvider="target">
<rect key="frame" x="0.0" y="9" width="20" height="20"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" constant="20" id="TgA-1l-WQJ"/>
<constraint firstAttribute="height" constant="20" id="VUB-ip-zXU"/>
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="5gI-Wl-lnK" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="1Yh-ha-Pu8"/>
<constraint firstItem="5F6-2v-qSS" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="CeZ-D5-NOV"/>
<constraint firstItem="z9o-XA-3t4" firstAttribute="leading" secondItem="5F6-2v-qSS" secondAttribute="trailing" constant="8" symbolic="YES" id="CiV-5P-T1S"/>
<constraint firstAttribute="trailing" secondItem="z9o-XA-3t4" secondAttribute="trailing" id="OVL-Ac-Rtt"/>
<constraint firstItem="z9o-XA-3t4" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ZkY-jG-eZO"/>
<constraint firstItem="5gI-Wl-lnK" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="eQU-mX-qmd"/>
<constraint firstItem="5F6-2v-qSS" firstAttribute="leading" secondItem="5gI-Wl-lnK" secondAttribute="trailing" constant="8" id="fVr-vW-alg"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="iconView" destination="5gI-Wl-lnK" id="IiR-qS-d22"/>
<outlet property="label" destination="5F6-2v-qSS" id="ec7-8Y-PRv"/>
<outlet property="unreadCountView" destination="z9o-XA-3t4" id="JBy-aa-feL"/>
</connections>
<point key="canvasLocation" x="14.492753623188406" y="-224.33035714285714"/>
</view>
</objects>
</document>

View File

@@ -0,0 +1,39 @@
//
// MainTimelineUnreadCountView.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 9/30/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
final class MainTimelineUnreadCountView: MainFeedUnreadCountView {
override var padding: UIEdgeInsets {
return UIEdgeInsets(top: 2.0, left: 9.0, bottom: 2.0, right: 9.0)
}
override var textColor: UIColor {
return UIColor.systemBackground
}
override var intrinsicContentSize: CGSize {
return contentSize
}
override func draw(_ dirtyRect: CGRect) {
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
let rect = CGRect(x: 1, y: 1, width: bounds.width - 2, height: bounds.height - 2)
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: cornerRadii)
AppColor.accent.setFill()
path.fill()
if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
}
}
}

View File

@@ -0,0 +1,85 @@
//
// UndoAvailableAlertController.swift
// NetNewsWire
//
// Created by Phil Viso on 9/29/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import UIKit
protocol MarkAsReadAlertControllerSourceType {}
extension CGRect: MarkAsReadAlertControllerSourceType {}
extension UIView: MarkAsReadAlertControllerSourceType {}
extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {}
struct MarkAsReadAlertController {
static func confirm<T>(
_ controller: UIViewController?,
coordinator: SceneCoordinator?,
confirmTitle: String,
sourceType: T,
cancelCompletion: (() -> Void)? = nil,
completion: @escaping () -> Void
) where T: MarkAsReadAlertControllerSourceType {
guard let controller = controller, let coordinator = coordinator else {
completion()
return
}
if AppDefaults.confirmMarkAllAsRead {
let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in
completion()
}
controller.present(alertController, animated: true)
} else {
completion()
}
}
private static func alert<T>(
coordinator: SceneCoordinator,
confirmTitle: String,
cancelCompletion: (() -> Void)?,
sourceType: T,
completion: @escaping (UIAlertAction) -> Void
) -> UIAlertController where T: MarkAsReadAlertControllerSourceType {
let title = NSLocalizedString("Mark As Read", comment: "Mark As Read")
let message = NSLocalizedString("You can turn this confirmation off in Settings.",
comment: "You can turn this confirmation off in Settings.")
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings")
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
cancelCompletion?()
}
let settingsAction = UIAlertAction(title: settingsTitle, style: .default) { _ in
coordinator.showSettings(scrollToArticlesSection: true)
}
let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion)
alertController.addAction(markAction)
alertController.addAction(settingsAction)
alertController.addAction(cancelAction)
if let barButtonItem = sourceType as? UIBarButtonItem {
alertController.popoverPresentationController?.barButtonItem = barButtonItem
}
if let rect = sourceType as? CGRect {
alertController.popoverPresentationController?.sourceRect = rect
}
if let view = sourceType as? UIView {
alertController.popoverPresentationController?.sourceView = view
}
return alertController
}
}

File diff suppressed because it is too large Load Diff