Make coordinator not force-unwrapped — fix crashing bug. Fix #4618.

This commit is contained in:
Brent Simmons
2025-04-26 16:06:33 -07:00
parent 0493e86811
commit 016726a21d

View File

@@ -28,10 +28,72 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
private lazy var dataSource = makeDataSource()
private let searchController = UISearchController(searchResultsController: nil)
weak var coordinator: SceneCoordinator!
weak var coordinator: SceneCoordinator?
var undoableCommands = [UndoableCommand]()
let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0)
private var timelineFeed: Feed? {
assert(coordinator != nil)
return coordinator?.timelineFeed
}
private var showIcons: Bool {
assert(coordinator != nil)
return coordinator?.showIcons ?? false
}
private var currentArticle: Article? {
assert(coordinator != nil)
return coordinator?.currentArticle
}
private var timelineMiddleIndexPath: IndexPath? {
get {
coordinator?.timelineMiddleIndexPath
}
set {
coordinator?.timelineMiddleIndexPath = newValue
}
}
private var isTimelineViewControllerPending: Bool {
get {
coordinator?.isTimelineViewControllerPending ?? false
}
set {
coordinator?.isTimelineViewControllerPending = newValue
}
}
private var timelineIconImage: IconImage? {
assert(coordinator != nil)
return coordinator?.timelineIconImage
}
private var timelineDefaultReadFilterType: ReadFilterType {
return timelineFeed?.defaultReadFilterType ?? .none
}
private var isReadArticlesFiltered: Bool {
assert(coordinator != nil)
return coordinator?.isReadArticlesFiltered ?? false
}
private var isTimelineUnreadAvailable: Bool {
assert(coordinator != nil)
return coordinator?.isTimelineUnreadAvailable ?? false
}
private var isRootSplitCollapsed: Bool {
assert(coordinator != nil)
return coordinator?.isRootSplitCollapsed ?? false
}
private var articles: ArticleArray? {
assert(coordinator != nil)
return coordinator?.articles
}
private let keyboardManager = KeyboardManager(type: .timeline)
override var keyCommands: [UIKeyCommand]? {
@@ -49,6 +111,8 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
override func viewDidLoad() {
assert(coordinator != nil)
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
@@ -106,7 +170,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
// Load the table and then scroll to the saved position if available
applyChanges(animated: false) {
if let restoreIndexPath = self.coordinator.timelineMiddleIndexPath {
if let restoreIndexPath = self.timelineMiddleIndexPath {
self.tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
}
}
@@ -134,7 +198,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
coordinator.isTimelineViewControllerPending = false
isTimelineViewControllerPending = false
if navigationController?.navigationBar.alpha == 0 {
UIView.animate(withDuration: 0.5) {
@@ -146,15 +210,23 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
// MARK: Actions
@objc func openInBrowser(_ sender: Any?) {
coordinator.showBrowserForCurrentArticle()
assert(coordinator != nil)
coordinator?.showBrowserForCurrentArticle()
}
@objc func openInAppBrowser(_ sender: Any?) {
coordinator.showInAppBrowser()
assert(coordinator != nil)
coordinator?.showInAppBrowser()
}
@IBAction func toggleFilter(_ sender: Any) {
coordinator.toggleReadArticlesFilter()
assert(coordinator != nil)
coordinator?.toggleReadArticlesFilter()
}
private func markAllAsReadInTimeline() {
assert(coordinator != nil)
coordinator?.markAllAsReadInTimeline()
}
@IBAction func markAllAsRead(_ sender: Any) {
@@ -162,7 +234,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
if let source = sender as? UIBarButtonItem {
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in
self?.coordinator.markAllAsReadInTimeline()
self?.markAllAsReadInTimeline()
}
}
@@ -172,13 +244,14 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
}
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markAllAsReadInTimeline()
self?.markAllAsReadInTimeline()
}
}
}
@IBAction func firstUnread(_ sender: Any) {
coordinator.selectFirstUnread()
assert(coordinator != nil)
coordinator?.selectFirstUnread()
}
@objc func refreshAccounts(_ sender: Any) {
@@ -194,29 +267,34 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
// MARK: Keyboard shortcuts
@objc func selectNextUp(_ sender: Any?) {
coordinator.selectPrevArticle()
assert(coordinator != nil)
coordinator?.selectPrevArticle()
}
@objc func selectNextDown(_ sender: Any?) {
coordinator.selectNextArticle()
assert(coordinator != nil)
coordinator?.selectNextArticle()
}
@objc func navigateToSidebar(_ sender: Any?) {
coordinator.navigateToFeeds()
assert(coordinator != nil)
coordinator?.navigateToFeeds()
}
@objc func navigateToDetail(_ sender: Any?) {
coordinator.navigateToDetail()
assert(coordinator != nil)
coordinator?.navigateToDetail()
}
@objc func showFeedInspector(_ sender: Any?) {
coordinator.showFeedInspector()
assert(coordinator != nil)
coordinator?.showFeedInspector()
}
// MARK: API
func restoreSelectionIfNecessary(adjustScroll: Bool) {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
if let article = currentArticle, let indexPath = dataSource.indexPath(for: article) {
if adjustScroll {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: [])
} else {
@@ -234,7 +312,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
}
func updateArticleSelection(animations: Animations) {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
if let article = currentArticle, let indexPath = dataSource.indexPath(for: article) {
if tableView.indexPathForSelectedRow != indexPath {
tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations)
}
@@ -281,7 +359,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
NSLocalizedString("Mark as Read", comment: "Mark as Read")
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in
self?.coordinator.toggleRead(article)
self?.toggleRead(article)
completion(true)
}
@@ -301,7 +379,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
NSLocalizedString("Star", comment: "Star")
let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (action, view, completion) in
self?.coordinator.toggleStar(article)
self?.toggleStar(article)
completion(true)
}
@@ -433,7 +511,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
becomeFirstResponder()
let article = dataSource.itemIdentifier(for: indexPath)
coordinator.selectArticle(article, animations: [.scroll, .select, .navigation])
coordinator?.selectArticle(article, animations: [.scroll, .select, .navigation])
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
@@ -466,7 +544,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.iconView.iconImage = coordinator.timelineIconImage
titleView.iconView.iconImage = timelineIconImage
}
guard let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
@@ -483,7 +561,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
}
@objc func avatarDidBecomeAvailable(_ note: Notification) {
guard coordinator.showIcons, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
guard showIcons, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else {
return
}
tableView.indexPathsForVisibleRows?.forEach { indexPath in
@@ -500,9 +578,9 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
@objc func faviconDidBecomeAvailable(_ note: Notification) {
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.iconView.iconImage = coordinator.timelineIconImage
titleView.iconView.iconImage = timelineIconImage
}
if coordinator.showIcons {
if showIcons {
queueReloadAvailableCells()
}
}
@@ -525,7 +603,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
@objc func displayNameDidChange(_ note: Notification) {
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
titleView.label.text = timelineFeed?.nameForDisplay
}
}
@@ -534,7 +612,7 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
}
@objc func scrollPositionDidChange() {
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
timelineMiddleIndexPath = tableView.middleVisibleRow()
}
// MARK: Reloading
@@ -585,12 +663,12 @@ class MainTimelineViewController: UITableViewController, UndoableCommandRunner {
extension MainTimelineViewController: UISearchControllerDelegate {
func willPresentSearchController(_ searchController: UISearchController) {
coordinator.beginSearching()
coordinator?.beginSearching()
searchController.searchBar.showsScopeBar = true
}
func willDismissSearchController(_ searchController: UISearchController) {
coordinator.endSearching()
coordinator?.endSearching()
searchController.searchBar.showsScopeBar = false
}
@@ -600,7 +678,7 @@ extension MainTimelineViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
let searchScope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex)!
coordinator.searchArticles(searchController.searchBar.text!, searchScope)
searchArticles(searchController.searchBar.text!, searchScope)
}
}
@@ -608,7 +686,7 @@ extension MainTimelineViewController: UISearchResultsUpdating {
extension MainTimelineViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
let searchScope = SearchScope(rawValue: selectedScope)!
coordinator.searchArticles(searchBar.text!, searchScope)
searchArticles(searchBar.text!, searchScope)
}
}
@@ -616,6 +694,11 @@ extension MainTimelineViewController: UISearchBarDelegate {
private extension MainTimelineViewController {
func searchArticles(_ searchString: String, _ searchScope: SearchScope) {
assert(coordinator != nil)
coordinator?.searchArticles(searchString, searchScope)
}
func configureToolbar() {
guard !(splitViewController?.isCollapsed ?? true) else {
return
@@ -632,10 +715,9 @@ private extension MainTimelineViewController {
func resetUI(resetScroll: Bool) {
title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline"
title = timelineFeed?.nameForDisplay ?? "Timeline"
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
let timelineIconImage = coordinator.timelineIconImage
titleView.iconView.iconImage = timelineIconImage
if let preferredColor = timelineIconImage?.preferredColor {
titleView.iconView.tintColor = UIColor(cgColor: preferredColor)
@@ -643,10 +725,10 @@ private extension MainTimelineViewController {
titleView.iconView.tintColor = nil
}
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
titleView.label.text = timelineFeed?.nameForDisplay
updateTitleUnreadCount()
if coordinator.timelineFeed is WebFeed {
if timelineFeed is WebFeed {
titleView.buttonize()
titleView.addGestureRecognizer(feedTapGestureRecognizer)
} else {
@@ -657,14 +739,14 @@ private extension MainTimelineViewController {
navigationItem.titleView = titleView
}
switch coordinator.timelineDefaultReadFilterType {
switch timelineDefaultReadFilterType {
case .none, .read:
navigationItem.rightBarButtonItem = filterButton
case .alwaysRead:
navigationItem.rightBarButtonItem = nil
}
if coordinator.isReadArticlesFiltered {
if isReadArticlesFiltered {
filterButton?.image = AppAssets.filterActiveImage
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
} else {
@@ -687,10 +769,10 @@ private extension MainTimelineViewController {
func updateToolbar() {
guard firstUnreadButton != nil else { return }
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
markAllAsReadButton.isEnabled = isTimelineUnreadAvailable
firstUnreadButton.isEnabled = isTimelineUnreadAvailable
if coordinator.isRootSplitCollapsed {
if isRootSplitCollapsed {
if let toolbarItems = toolbarItems, toolbarItems.last != firstUnreadButton {
var items = toolbarItems
items.append(firstUnreadButton)
@@ -706,12 +788,12 @@ private extension MainTimelineViewController {
func updateTitleUnreadCount() {
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount
titleView.unreadCountView.unreadCount = coordinator?.timelineUnreadCount ?? 0
}
}
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
if coordinator.articles.count == 0 {
if (articles?.count ?? 0) == 0 {
tableView.rowHeight = tableView.estimatedRowHeight
} else {
tableView.rowHeight = UITableView.automaticDimension
@@ -719,7 +801,7 @@ private extension MainTimelineViewController {
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
snapshot.appendSections([0])
snapshot.appendItems(coordinator.articles, toSection: 0)
snapshot.appendItems(articles ?? ArticleArray(), toSection: 0)
dataSource.apply(snapshot, animatingDifferences: animated) { [weak self] in
self?.restoreSelectionIfNecessary(adjustScroll: false)
@@ -742,19 +824,24 @@ private extension MainTimelineViewController {
let iconImage = iconImageFor(article)
let showFeedNames = coordinator.showFeedNames
let showIcon = coordinator.showIcons && iconImage != nil
let showFeedNames = coordinator?.showFeedNames ?? ShowFeedName.none
let showIcon = showIcons && iconImage != nil
cell.cellData = MainTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.webFeed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcon, numberOfLines: numberOfTextLines, iconSize: iconSize)
}
func iconImageFor(_ article: Article) -> IconImage? {
if !coordinator.showIcons {
if !showIcons {
return nil
}
return article.iconImage()
}
func toggleRead(_ article: Article) {
assert(coordinator != nil)
coordinator?.toggleRead(article)
}
func toggleArticleReadStatusAction(_ article: Article) -> UIAction? {
guard !article.status.read || article.isAvailableToMarkUnread else { return nil }
@@ -764,12 +851,17 @@ private extension MainTimelineViewController {
let image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
let action = UIAction(title: title, image: image) { [weak self] action in
self?.coordinator.toggleRead(article)
self?.toggleRead(article)
}
return action
}
func toggleStar(_ article: Article) {
assert(coordinator != nil)
coordinator?.toggleStar(article)
}
func toggleArticleStarStatusAction(_ article: Article) -> UIAction {
let title = article.status.starred ?
@@ -778,14 +870,24 @@ private extension MainTimelineViewController {
let image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
let action = UIAction(title: title, image: image) { [weak self] action in
self?.coordinator.toggleStar(article)
self?.toggleStar(article)
}
return action
}
func markAboveAsRead(_ article: Article) {
assert(coordinator != nil)
coordinator?.markAboveAsRead(article)
}
func canMarkAboveAsRead(for article: Article) -> Bool {
assert(coordinator != nil)
return coordinator?.canMarkAboveAsRead(for: article) ?? false
}
func markAboveAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
guard canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
}
@@ -793,14 +895,24 @@ private extension MainTimelineViewController {
let image = AppAssets.markAboveAsReadImage
let action = UIAction(title: title, image: image) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markAboveAsRead(article)
self?.markAboveAsRead(article)
}
}
return action
}
func markBelowAsRead(_ article: Article) {
assert(coordinator != nil)
coordinator?.markBelowAsRead(article)
}
func canMarkBelowAsRead(for article: Article) -> Bool {
assert(coordinator != nil)
return coordinator?.canMarkBelowAsRead(for: article) ?? false
}
func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
guard canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
}
@@ -808,14 +920,14 @@ private extension MainTimelineViewController {
let image = AppAssets.markBelowAsReadImage
let action = UIAction(title: title, image: image) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markBelowAsRead(article)
self?.markBelowAsRead(article)
}
}
return action
}
func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
guard canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
}
@@ -826,7 +938,7 @@ private extension MainTimelineViewController {
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markAboveAsRead(article)
self?.markAboveAsRead(article)
completion(true)
}
}
@@ -834,7 +946,7 @@ private extension MainTimelineViewController {
}
func markBelowAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
guard canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil
}
@@ -845,36 +957,51 @@ private extension MainTimelineViewController {
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markBelowAsRead(article)
self?.markBelowAsRead(article)
completion(true)
}
}
return action
}
func timelineFeedIsEqualTo(_ feed: WebFeed) -> Bool {
assert(coordinator != nil)
return coordinator?.timelineFeedIsEqualTo(feed) ?? false
}
func discloseWebFeed(_ feed: WebFeed, animations: Animations = []) {
assert(coordinator != nil)
coordinator?.discloseWebFeed(feed, animations: animations)
}
func discloseFeedAction(_ article: Article) -> UIAction? {
guard let webFeed = article.webFeed,
!coordinator.timelineFeedIsEqualTo(webFeed) else { return nil }
!timelineFeedIsEqualTo(webFeed) else { return nil }
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in
self?.coordinator.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
self?.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
}
return action
}
func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let webFeed = article.webFeed,
!coordinator.timelineFeedIsEqualTo(webFeed) else { return nil }
!timelineFeedIsEqualTo(webFeed) else { return nil }
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
self?.discloseWebFeed(webFeed, animations: [.scroll, .navigation])
completion(true)
}
return action
}
func markAllAsRead(_ articles: ArticleArray) {
assert(coordinator != nil)
coordinator?.markAllAsRead(articles)
}
func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard let webFeed = article.webFeed else { return nil }
guard let fetchedArticles = try? webFeed.fetchArticles() else {
@@ -892,7 +1019,7 @@ private extension MainTimelineViewController {
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markAllAsRead(articles)
self?.markAllAsRead(articles)
}
}
return action
@@ -917,7 +1044,7 @@ private extension MainTimelineViewController {
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markAllAsRead(articles)
self?.markAllAsRead(articles)
completion(true)
}
}
@@ -942,12 +1069,16 @@ private extension MainTimelineViewController {
return action
}
func showBrowserForArticle(_ article: Article) {
assert(coordinator != nil)
coordinator?.showBrowserForArticle(article)
}
func openInBrowserAction(_ article: Article) -> UIAction? {
guard let _ = article.preferredURL else { return nil }
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in
self?.coordinator.showBrowserForArticle(article)
self?.showBrowserForArticle(article)
}
return action
}
@@ -957,7 +1088,7 @@ private extension MainTimelineViewController {
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
self?.coordinator.showBrowserForArticle(article)
self?.showBrowserForArticle(article)
completion(true)
}
return action