Added initial POC version of NetNewsWire for iOS to use as a starting point for the actual app.

This commit is contained in:
Maurice Parker
2019-04-15 15:03:05 -05:00
parent 8f1f153e98
commit 8526db8b4c
47 changed files with 4454 additions and 220 deletions

View File

@@ -0,0 +1,128 @@
//
// MasterTableViewCell.swift
// NetNewsWire
//
// Created by Brent Simmons on 8/1/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
import RSCore
import Account
import RSTree
class MasterTableViewCell : UITableViewCell {
override var accessibilityLabel: String? {
set {}
get {
if unreadCount > 0 {
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessiblity")
return "\(name) \(unreadCount) \(unreadLabel)"
} else {
return name
}
}
}
var faviconImage: UIImage? {
didSet {
if let image = faviconImage {
faviconImageView.image = shouldShowImage ? image : nil
}
else {
faviconImageView.image = nil
}
}
}
var shouldShowImage = false {
didSet {
if shouldShowImage != oldValue {
setNeedsLayout()
}
faviconImageView.image = shouldShowImage ? faviconImage : nil
}
}
var unreadCount: Int {
get {
return unreadCountView.unreadCount
}
set {
if unreadCountView.unreadCount != newValue {
unreadCountView.unreadCount = newValue
unreadCountView.isHidden = (newValue < 1)
setNeedsLayout()
}
}
}
var name: String {
get {
return titleView.text ?? ""
}
set {
if titleView.text != newValue {
titleView.text = newValue
setNeedsDisplay()
setNeedsLayout()
}
}
}
private let titleView: UILabel = {
let label = UILabel()
label.numberOfLines = 1
label.lineBreakMode = .byTruncatingTail
label.allowsDefaultTighteningForTruncation = false
return label
}()
private let faviconImageView: UIImageView = {
return UIImageView(image: AppAssets.feedImage)
}()
private let unreadCountView = MasterUnreadCountView(frame: CGRect.zero)
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
override func layoutSubviews() {
super.layoutSubviews()
let layout = MasterTableViewCellLayout(cellSize: bounds.size, shouldShowImage: shouldShowImage, label: titleView, unreadCountView: unreadCountView)
layoutWith(layout)
}
}
private extension MasterTableViewCell {
func commonInit() {
addSubviewAtInit(unreadCountView)
addSubviewAtInit(faviconImageView)
addSubviewAtInit(titleView)
}
func addSubviewAtInit(_ view: UIView) {
addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
}
func layoutWith(_ layout: MasterTableViewCellLayout) {
faviconImageView.rs_setFrameIfNotEqual(layout.faviconRect)
titleView.rs_setFrameIfNotEqual(layout.titleRect)
unreadCountView.rs_setFrameIfNotEqual(layout.unreadCountRect)
}
}
extension UIView {
func rs_setFrameIfNotEqual(_ rect: CGRect) {
if !self.frame.equalTo(rect) {
self.frame = rect
}
}
}

View File

@@ -0,0 +1,74 @@
//
// MasterTableViewCellLayout.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/24/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import UIKit
import RSCore
struct MasterTableViewCellLayout {
private static let imageSize = CGSize(width: 16, height: 16)
private static let imageMarginLeft = CGFloat(integerLiteral: 8)
private static let imageMarginRight = CGFloat(integerLiteral: 8)
private static let unreadCountMarginLeft = CGFloat(integerLiteral: 8)
private static let unreadCountMarginRight = CGFloat(integerLiteral: 8)
let faviconRect: CGRect
let titleRect: CGRect
let unreadCountRect: CGRect
init(cellSize: CGSize, shouldShowImage: Bool, label: UILabel, unreadCountView: MasterUnreadCountView) {
let bounds = CGRect(x: 0.0, y: 0.0, width: floor(cellSize.width), height: floor(cellSize.height))
var rFavicon = CGRect.zero
if shouldShowImage {
rFavicon = CGRect(x: MasterTableViewCellLayout.imageMarginLeft, y: 0.0, width: MasterTableViewCellLayout.imageSize.width, height: MasterTableViewCellLayout.imageSize.height)
rFavicon = MasterTableViewCellLayout.centerVertically(rFavicon, bounds)
}
self.faviconRect = rFavicon
let labelSize = SingleLineUILabelSizer.size(for: label.text ?? "", font: label.font!)
var rLabel = CGRect(x: 0.0, y: 0.0, width: labelSize.width, height: labelSize.height)
if shouldShowImage {
rLabel.origin.x = rFavicon.maxX + MasterTableViewCellLayout.imageMarginRight
}
rLabel = MasterTableViewCellLayout.centerVertically(rLabel, bounds)
let unreadCountSize = unreadCountView.intrinsicContentSize
let unreadCountIsHidden = unreadCountView.unreadCount < 1
var rUnread = CGRect.zero
if !unreadCountIsHidden {
rUnread.size = unreadCountSize
rUnread.origin.x = (bounds.maxX - unreadCountSize.width) - MasterTableViewCellLayout.unreadCountMarginRight
rUnread = MasterTableViewCellLayout.centerVertically(rUnread, bounds)
let labelMaxX = rUnread.minX - MasterTableViewCellLayout.unreadCountMarginLeft
if rLabel.maxX > labelMaxX {
rLabel.size.width = labelMaxX - rLabel.minX
}
}
self.unreadCountRect = rUnread
if rLabel.maxX > bounds.maxX {
rLabel.size.width = bounds.maxX - rLabel.maxX
}
self.titleRect = rLabel
}
// Ideally this will be implemented in RSCore (see RSGeometry)
static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect {
var result = originalRect
result.origin.y = containerRect.midY - (result.height / 2.0)
result = result.integral
result.size = originalRect.size
return result
}
}

View File

@@ -0,0 +1,107 @@
//
// MasterUnreadCountView.swift
// NetNewsWire
//
// Created by Brent Simmons on 11/22/15.
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
//
import UIKit
private let padding = UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
private let cornerRadius = 8.0
private let bgColor = UIColor.darkGray
private let textColor = UIColor.white
private let textFont = UIFont.systemFont(ofSize: 11.0, weight: UIFont.Weight.semibold)
private var textAttributes: [NSAttributedString.Key: AnyObject] = [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()]
private var textSizeCache = [Int: CGSize]()
class MasterUnreadCountView : UIView {
var unreadCount = 0 {
didSet {
invalidateIntrinsicContentSize()
setNeedsDisplay()
}
}
var unreadCountString: String {
return unreadCount < 1 ? "" : "\(unreadCount)"
}
private var intrinsicContentSizeIsValid = false
private var _intrinsicContentSize = CGSize.zero
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
}
override var intrinsicContentSize: CGSize {
if !intrinsicContentSizeIsValid {
var size = CGSize.zero
if unreadCount > 0 {
size = textSize()
size.width += (padding.left + padding.right)
size.height += (padding.top + padding.bottom)
}
_intrinsicContentSize = size
intrinsicContentSizeIsValid = true
}
return _intrinsicContentSize
}
override func invalidateIntrinsicContentSize() {
intrinsicContentSizeIsValid = false
}
private func textSize() -> CGSize {
if unreadCount < 1 {
return CGSize.zero
}
if let cachedSize = textSizeCache[unreadCount] {
return cachedSize
}
var size = unreadCountString.size(withAttributes: textAttributes)
size.height = ceil(size.height)
size.width = ceil(size.width)
textSizeCache[unreadCount] = size
return size
}
private func textRect() -> CGRect {
let size = textSize()
var r = CGRect.zero
r.size = size
r.origin.x = (bounds.maxX - padding.right) - r.size.width
r.origin.y = padding.top
return r
}
override func draw(_ dirtyRect: CGRect) {
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: .allCorners, cornerRadii: cornerRadii)
bgColor.setFill()
path.fill()
if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
}
}
}

View File

@@ -0,0 +1,121 @@
//
// MasterPrimaryViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import RSCore
import RSTree
class MasterPrimaryViewController: MasterViewController {
// MARK: Actions
@IBAction func showOPMLImportExport(_ sender: UIBarButtonItem) {
let optionMenu = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let importOPML = UIAlertAction(title: "Import OPML", style: .default) { [unowned self] alertAction in
let docPicker = UIDocumentPickerViewController(documentTypes: ["public.xml", "org.opml.opml"], in: .import)
docPicker.delegate = self
docPicker.modalPresentationStyle = .formSheet
self.present(docPicker, animated: true)
}
optionMenu.addAction(importOPML)
let exportOPML = UIAlertAction(title: "Export OPML", style: .default) { [unowned self] alertAction in
let filename = "MySubscriptions.opml"
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
let opmlString = OPMLExporter.OPMLString(with: AccountManager.shared.localAccount, title: filename)
do {
try opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8)
} catch {
self.presentError(title: "OPML Export Error", message: error.localizedDescription)
}
let docPicker = UIDocumentPickerViewController(url: tempFile, in: .exportToService)
docPicker.modalPresentationStyle = .formSheet
self.present(docPicker, animated: true)
}
optionMenu.addAction(exportOPML)
optionMenu.addAction(UIAlertAction(title: "Cancel", style: .cancel))
if let popoverController = optionMenu.popoverPresentationController {
popoverController.barButtonItem = sender
}
self.present(optionMenu, animated: true)
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return treeController.rootNode.numberOfChildNodes
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return treeController.rootNode.childAtIndex(section)?.numberOfChildNodes ?? 0
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let nameProvider = treeController.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else {
return nil
}
return nameProvider.nameForDisplay
}
// MARK: API
override func delete(indexPath: IndexPath) {
guard let containerNode = treeController.rootNode.childAtIndex(indexPath.section),
let deleteNode = containerNode.childAtIndex(indexPath.row),
let container = containerNode.representedObject as? Container else {
return
}
animatingChanges = true
if let feed = deleteNode.representedObject as? Feed {
container.deleteFeed(feed)
}
if let folder = deleteNode.representedObject as? Folder {
container.deleteFolder(folder)
}
treeController.rebuild()
tableView.deleteRows(at: [indexPath], with: .automatic)
animatingChanges = false
}
override func nodeFor(indexPath: IndexPath) -> Node? {
return treeController.rootNode.childAtIndex(indexPath.section)?.childAtIndex(indexPath.row)
}
}
extension MasterPrimaryViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
for url in urls {
do {
try OPMLImporter.parseAndImport(fileURL: url, account: AccountManager.shared.localAccount)
} catch {
presentError(title: "OPML Import Error", message: error.localizedDescription)
}
}
}
}

View File

@@ -0,0 +1,74 @@
//
// MasterSecondaryViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import RSCore
import RSTree
class MasterSecondaryViewController: MasterViewController {
var viewRootNode: Node?
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewRootNode?.numberOfChildNodes ?? 0
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
guard let containerNode = viewRootNode,
let deleteNode = containerNode.childAtIndex(indexPath.row),
let container = containerNode.representedObject as? Container,
let feed = deleteNode.representedObject as? Feed else {
return
}
animatingChanges = true
container.deleteFeed(feed)
treeController.rebuild()
tableView.deleteRows(at: [indexPath], with: .fade)
animatingChanges = false
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
// MARK: API
override func delete(indexPath: IndexPath) {
guard let containerNode = viewRootNode,
let deleteNode = containerNode.childAtIndex(indexPath.row),
let container = containerNode.representedObject as? Container,
let feed = deleteNode.representedObject as? Feed else {
return
}
animatingChanges = true
container.deleteFeed(feed)
treeController.rebuild()
tableView.deleteRows(at: [indexPath], with: .fade)
animatingChanges = false
}
override func nodeFor(indexPath: IndexPath) -> Node? {
return viewRootNode?.childAtIndex(indexPath.row)
}
}

View File

@@ -0,0 +1,135 @@
//
// MasterTreeControllerDelegate.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import RSTree
import Articles
import Account
final class MasterTreeControllerDelegate: TreeControllerDelegate {
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
if node.isRoot {
return childNodesForRootNode(node)
}
if node.representedObject is Container {
return childNodesForContainerNode(node)
}
if node.representedObject is SmartFeedsController {
return childNodesForSmartFeeds(node)
}
return nil
}
}
private extension MasterTreeControllerDelegate {
func childNodesForRootNode(_ rootNode: Node) -> [Node]? {
// The top-level nodes are Smart Feeds and accounts.
let smartFeedsNode = rootNode.existingOrNewChildNode(with: SmartFeedsController.shared)
smartFeedsNode.canHaveChildNodes = true
smartFeedsNode.isGroupItem = true
return [smartFeedsNode] + sortedAccountNodes(rootNode)
}
func childNodesForSmartFeeds(_ parentNode: Node) -> [Node] {
return SmartFeedsController.shared.smartFeeds.map { parentNode.existingOrNewChildNode(with: $0) }
}
func childNodesForContainerNode(_ containerNode: Node) -> [Node]? {
let container = containerNode.representedObject as! Container
var children = [AnyObject]()
children.append(contentsOf: Array(container.topLevelFeeds))
if let folders = container.folders {
children.append(contentsOf: Array(folders))
}
var updatedChildNodes = [Node]()
children.forEach { (representedObject) in
if let existingNode = containerNode.childNodeRepresentingObject(representedObject) {
if !updatedChildNodes.contains(existingNode) {
updatedChildNodes += [existingNode]
return
}
}
if let newNode = self.createNode(representedObject: representedObject, parent: containerNode) {
updatedChildNodes += [newNode]
}
}
return updatedChildNodes.sortedAlphabeticallyWithFoldersAtEnd()
}
func createNode(representedObject: Any, parent: Node) -> Node? {
if let feed = representedObject as? Feed {
return createNode(feed: feed, parent: parent)
}
if let folder = representedObject as? Folder {
return createNode(folder: folder, parent: parent)
}
if let account = representedObject as? Account {
return createNode(account: account, parent: parent)
}
return nil
}
func createNode(feed: Feed, parent: Node) -> Node {
return parent.createChildNode(feed)
}
func createNode(folder: Folder, parent: Node) -> Node {
let node = parent.createChildNode(folder)
node.canHaveChildNodes = true
return node
}
func createNode(account: Account, parent: Node) -> Node {
let node = parent.createChildNode(account)
node.canHaveChildNodes = true
node.isGroupItem = true
return node
}
func sortedAccountNodes(_ parent: Node) -> [Node] {
let nodes = AccountManager.shared.accounts.map { (account) -> Node in
let accountNode = parent.existingOrNewChildNode(with: account)
accountNode.canHaveChildNodes = true
accountNode.isGroupItem = true
return accountNode
}
return nodes.sortedAlphabetically()
}
func nodeInArrayRepresentingObject(_ nodes: [Node], _ representedObject: AnyObject) -> Node? {
for oneNode in nodes {
if oneNode.representedObject === representedObject {
return oneNode
}
}
return nil
}
}

View File

@@ -0,0 +1,320 @@
//
// MasterViewController.swift
// NetNewsWire
//
// Created by Maurice Parker on 4/8/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import RSCore
import RSTree
class MasterViewController: UITableViewController {
var animatingChanges = false
let treeControllerDelegate = MasterTreeControllerDelegate()
lazy var treeController: TreeController = {
return TreeController(delegate: treeControllerDelegate)
}()
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil)
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = splitViewController!.isCollapsed
super.viewWillAppear(animated)
}
@objc private func refreshAccounts(_ sender: Any) {
AccountManager.shared.refreshAll()
}
@objc dynamic func progressDidChange(_ notification: Notification) {
if AccountManager.shared.combinedRefreshProgress.isComplete {
refreshControl?.endRefreshing()
} else {
refreshControl?.beginRefreshing()
}
}
@objc func containerChildrenDidChange(_ note: Notification) {
rebuildTreeAndReloadDataIfNeeded()
}
@objc func batchUpdateDidPerform(_ notification: Notification) {
rebuildTreeAndReloadDataIfNeeded()
}
@objc func unreadCountDidChange(_ note: Notification) {
guard let representedObject = note.object else {
return
}
configureUnreadCountForCellsForRepresentedObject(representedObject as AnyObject)
}
@objc func faviconDidBecomeAvailable(_ note: Notification) {
applyToAvailableCells(configureFavicon)
}
@objc func feedSettingDidChange(_ note: Notification) {
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
return
}
if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
configureCellsForRepresentedObject(feed)
}
}
@objc func displayNameDidChange(_ note: Notification) {
guard let object = note.object else {
return
}
rebuildTreeAndReloadDataIfNeeded()
configureCellsForRepresentedObject(object as AnyObject)
}
// MARK: Table View
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterTableViewCell
guard let node = nodeFor(indexPath: indexPath) else {
return cell
}
configure(cell, node)
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
guard let node = nodeFor(indexPath: indexPath), !(node.representedObject is PseudoFeed) else {
return false
}
return true
}
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
// Set up the delete action
let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completionHandler) in
self?.delete(indexPath: indexPath)
completionHandler(true)
}
deleteAction.backgroundColor = UIColor.red
// Set up the rename action
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (action, view, completionHandler) in
self?.rename(indexPath: indexPath)
completionHandler(true)
}
renameAction.backgroundColor = UIColor.gray
return UISwipeActionsConfiguration(actions: [deleteAction, renameAction])
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let node = nodeFor(indexPath: indexPath) else {
assertionFailure()
return
}
if let pseudoFeed = node.representedObject as? PseudoFeed {
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
timeline.title = pseudoFeed.nameForDisplay
timeline.representedObjects = [pseudoFeed]
self.navigationController?.pushViewController(timeline, animated: true)
}
if let folder = node.representedObject as? Folder {
let secondary = UIStoryboard.main.instantiateController(ofType: MasterSecondaryViewController.self)
secondary.title = folder.nameForDisplay
secondary.viewRootNode = node
self.navigationController?.pushViewController(secondary, animated: true)
}
if let feed = node.representedObject as? Feed {
let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self)
timeline.title = feed.nameForDisplay
timeline.representedObjects = [feed]
self.navigationController?.pushViewController(timeline, animated: true)
}
}
// MARK: Actions
@IBAction func addFeed(_ sender: UIBarButtonItem) {
let feedViewController = UIStoryboard(name: "AddFeed", bundle: nil).instantiateViewController(withIdentifier: "AddFeedNavigationController")
feedViewController.modalPresentationStyle = .popover
feedViewController.popoverPresentationController?.barButtonItem = sender
self.present(feedViewController, animated: true)
}
@IBAction func addFolder(_ sender: UIBarButtonItem) {
let feedViewController = UIStoryboard(name: "AddFolder", bundle: nil).instantiateViewController(withIdentifier: "AddFolderNavigationController")
feedViewController.modalPresentationStyle = .popover
feedViewController.popoverPresentationController?.barButtonItem = sender
self.present(feedViewController, animated: true)
}
// MARK: API
func configure(_ cell: MasterTableViewCell, _ node: Node) {
cell.name = nameFor(node)
configureUnreadCount(cell, node)
configureFavicon(cell, node)
cell.shouldShowImage = node.representedObject is SmallIconProvider
}
func configureUnreadCount(_ cell: MasterTableViewCell, _ node: Node) {
cell.unreadCount = unreadCountFor(node)
}
func configureFavicon(_ cell: MasterTableViewCell, _ node: Node) {
cell.faviconImage = imageFor(node)
}
func imageFor(_ node: Node) -> UIImage? {
if let smallIconProvider = node.representedObject as? SmallIconProvider {
return smallIconProvider.smallIcon
}
return nil
}
func nameFor(_ node: Node) -> String {
if let displayNameProvider = node.representedObject as? DisplayNameProvider {
return displayNameProvider.nameForDisplay
}
return ""
}
func unreadCountFor(_ node: Node) -> Int {
if let unreadCountProvider = node.representedObject as? UnreadCountProvider {
return unreadCountProvider.unreadCount
}
return 0
}
func delete(indexPath: IndexPath) {
assertionFailure()
}
func rename(indexPath: IndexPath) {
let title = NSLocalizedString("Rename", comment: "Rename")
let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert)
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
// TODO: Add the title of what is being renamed...
// let formatString = NSLocalizedString("Rename %@", comment: "Feed finder")
// let message = NSString.localizedStringWithFormat(formatString as NSString, )
let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in
guard let node = self?.nodeFor(indexPath: indexPath),
let name = alertController.textFields?[0].text,
!name.isEmpty else {
return
}
if let feed = node.representedObject as? Feed {
feed.editedName = name
} else if let folder = node.representedObject as? Folder {
folder.name = name
}
}
alertController.addAction(renameAction)
alertController.addTextField() { textField in
textField.placeholder = NSLocalizedString("Name", comment: "Name")
}
self.present(alertController, animated: true) {
}
}
func nodeFor(indexPath: IndexPath) -> Node? {
assertionFailure()
return nil
}
}
// MARK: Private
private extension MasterViewController {
func rebuildTreeAndReloadDataIfNeeded() {
if !animatingChanges && !BatchUpdate.shared.isPerforming {
treeController.rebuild()
tableView.reloadData()
}
}
func configureCellsForRepresentedObject(_ representedObject: AnyObject) {
applyToCellsForRepresentedObject(representedObject, configure)
}
func configureUnreadCountForCellsForRepresentedObject(_ representedObject: AnyObject) {
applyToCellsForRepresentedObject(representedObject, configureUnreadCount)
}
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ callback: (MasterTableViewCell, Node) -> Void) {
applyToAvailableCells { (cell, node) in
if node.representedObject === representedObject {
callback(cell, node)
}
}
}
func applyToAvailableCells(_ callback: (MasterTableViewCell, Node) -> Void) {
tableView.visibleCells.forEach { cell in
guard let indexPath = tableView.indexPath(for: cell), let node = nodeFor(indexPath: indexPath) else {
return
}
callback(cell as! MasterTableViewCell, node)
}
}
}