From 6ac6136612b9a91960deafd77904ad5b01111655 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Mon, 27 Jan 2020 21:57:52 -0700 Subject: [PATCH] Make animations individually selectable and no longer animate navigation selections. Issue #1439 --- NetNewsWire.xcodeproj/project.pbxproj | 8 +++ iOS/MasterFeed/MasterFeedViewController.swift | 24 +++---- .../MasterTimelineViewController.swift | 14 ++-- iOS/SceneCoordinator.swift | 68 +++++++++---------- iOS/UIKit Extensions/Animations.swift | 27 ++++++++ .../UITableView-Extensions.swift | 37 ++++++++++ submodules/RSCore | 2 +- 7 files changed, 126 insertions(+), 54 deletions(-) create mode 100644 iOS/UIKit Extensions/Animations.swift create mode 100644 iOS/UIKit Extensions/UITableView-Extensions.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 97e9c47e1..c2c003b99 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -258,6 +258,8 @@ 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; }; 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; }; 51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */; }; + 51F9F3F923DFB16300A314FD /* UITableView-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F3F823DFB16300A314FD /* UITableView-Extensions.swift */; }; + 51F9F3FB23DFB25700A314FD /* Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F9F3FA23DFB25700A314FD /* Animations.swift */; }; 51FA73A42332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; 51FA73A52332BE110090D516 /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; 51FA73A72332BE880090D516 /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; }; @@ -1383,6 +1385,8 @@ 51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = ""; }; 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = ""; }; 51F9F3F623DF6DB200A314FD /* ArticleIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleIconSchemeHandler.swift; sourceTree = ""; }; + 51F9F3F823DFB16300A314FD /* UITableView-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView-Extensions.swift"; sourceTree = ""; }; + 51F9F3FA23DFB25700A314FD /* Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animations.swift; sourceTree = ""; }; 51FA73A32332BE110090D516 /* ArticleExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractor.swift; sourceTree = ""; }; 51FA73A62332BE880090D516 /* ExtractedArticle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtractedArticle.swift; sourceTree = ""; }; 51FA73B62332D5F70090D516 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; @@ -1896,10 +1900,12 @@ 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */, 51F85BF622749FA100C787DC /* UIFont-Extensions.swift */, 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */, + 51F9F3F823DFB16300A314FD /* UITableView-Extensions.swift */, 518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */, 51FFF0C3235EE8E5002762AA /* VibrantButton.swift */, 5186A634235EF3A800C97195 /* VibrantLabel.swift */, 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */, + 51F9F3FA23DFB25700A314FD /* Animations.swift */, ); path = "UIKit Extensions"; sourceTree = ""; @@ -3838,6 +3844,7 @@ 512E08E72268801200BDCFDD /* WebFeedTreeControllerDelegate.swift in Sources */, 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, 51EF0F79227716380050506E /* ColorHash.swift in Sources */, + 51F9F3FB23DFB25700A314FD /* Animations.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */, 518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */, @@ -3872,6 +3879,7 @@ 51314704235C41FC00387FDC /* Intents.intentdefinition in Sources */, FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */, 51C4525C226508DF00C03939 /* String-Extensions.swift in Sources */, + 51F9F3F923DFB16300A314FD /* UITableView-Extensions.swift in Sources */, 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */, diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 629be7359..df1ba37c7 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -154,7 +154,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return } - discloseFeed(webFeed, animated: true) + discloseFeed(webFeed, animations: [.scroll, .navigation]) } @objc func contentSizeCategoryDidChange(_ note: Notification) { @@ -334,7 +334,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { becomeFirstResponder() - coordinator.selectFeed(indexPath, animated: true) + coordinator.selectFeed(indexPath, animations: [.navigation, .select, .scroll]) } override func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { @@ -516,29 +516,29 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { func restoreSelectionIfNecessary(adjustScroll: Bool) { if let indexPath = coordinator.masterFeedIndexPathForCurrentTimeline() { if adjustScroll { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: []) } else { tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) } } } - func updateFeedSelection(animated: Bool) { + func updateFeedSelection(animations: Animations) { if dataSource.snapshot().numberOfItems > 0 { if let indexPath = coordinator.currentFeedIndexPath { if tableView.indexPathForSelectedRow != indexPath { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: animated) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations) } } else { - if animated { + if animations.contains(.select) { // This nasty bit of duct tape is because there is something, somewhere // interrupting the deselection animation, which will leave the row selected. // This seems to get it far enough away the problem that it always works. DispatchQueue.main.async { - self.tableView.selectRow(at: nil, animated: animated, scrollPosition: .none) + self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none) } } else { - self.tableView.selectRow(at: nil, animated: animated, scrollPosition: .none) + self.tableView.selectRow(at: nil, animated: false, scrollPosition: .none) } } } @@ -572,7 +572,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } - func discloseFeed(_ webFeed: WebFeed, animated: Bool, completion: (() -> Void)? = nil) { + func discloseFeed(_ webFeed: WebFeed, animations: Animations, completion: (() -> Void)? = nil) { func discloseFeedInAccount() { guard let node = coordinator.rootNode.descendantNodeRepresentingObject(webFeed as AnyObject) else { @@ -581,7 +581,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } if let indexPath = dataSource.indexPath(for: node) { - coordinator.selectFeed(indexPath, animated: animated) { + coordinator.selectFeed(indexPath, animations: animations) { completion?() } return @@ -598,7 +598,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { applyChanges(animated: true, adjustScroll: true) { [weak self] in if let indexPath = self?.dataSource.indexPath(for: node) { - self?.coordinator.selectFeed(indexPath, animated: animated) { + self?.coordinator.selectFeed(indexPath, animations: animations) { completion?() } } @@ -1223,7 +1223,7 @@ private extension MasterFeedViewController { deleteCommand.perform() if indexPath == coordinator.currentFeedIndexPath { - coordinator.selectFeed(nil, animated: false) + coordinator.selectFeed(nil) } } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 2c96b458c..22ac0af25 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -172,7 +172,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner func restoreSelectionIfNecessary(adjustScroll: Bool) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if adjustScroll { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: false) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: []) } else { tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) } @@ -187,13 +187,13 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner applyChanges(animated: animated) } - func updateArticleSelection(animated: Bool) { + func updateArticleSelection(animations: Animations) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if tableView.indexPathForSelectedRow != indexPath { - tableView.selectRowAndScrollIfNotVisible(at: indexPath, animated: true) + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations) } } else { - tableView.selectRow(at: nil, animated: animated, scrollPosition: .none) + tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none) } updateUI() @@ -359,7 +359,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { becomeFirstResponder() let article = dataSource.itemIdentifier(for: indexPath) - coordinator.selectArticle(article, animated: true) + coordinator.selectArticle(article, animations: [.scroll, .select, .navigation]) } override func scrollViewDidScroll(_ scrollView: UIScrollView) { @@ -773,7 +773,7 @@ private extension MasterTimelineViewController { let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in - self?.coordinator.discloseFeed(webFeed, animated: true) + self?.coordinator.discloseFeed(webFeed, animations: [.scroll, .navigation]) } return action } @@ -783,7 +783,7 @@ private extension MasterTimelineViewController { let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.discloseFeed(webFeed, animated: true) + self?.coordinator.discloseFeed(webFeed, animations: [.scroll, .navigation]) completion(true) } return action diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 2510c8974..911d2f108 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -373,7 +373,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func handle(_ activity: NSUserActivity) { - selectFeed(nil, animated: false) { + selectFeed(nil) { guard let activityType = ActivityType(rawValue: activity.activityType) else { return } switch activityType { case .restoration: @@ -417,14 +417,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func selectFirstUnreadInAllUnread() { masterFeedViewController.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 1, section: 0), animated: false) { + self.selectFeed(IndexPath(row: 1, section: 0)) { self.selectFirstUnreadArticleInTimeline() } } } func showSearch() { - selectFeed(nil, animated: false) { + selectFeed(nil) { self.installTimelineControllerIfNecessary(animated: false) DispatchQueue.main.asyncAfter(deadline: .now()) { self.masterTimelineViewController!.showSearchAll() @@ -698,14 +698,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return indexPathFor(node) } - func selectFeed(_ indexPath: IndexPath?, animated: Bool, deselectArticle: Bool = true, completion: (() -> Void)? = nil) { + func selectFeed(_ indexPath: IndexPath?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) { guard indexPath != currentFeedIndexPath else { completion?() return } currentFeedIndexPath = indexPath - masterFeedViewController.updateFeedSelection(animated: animated) + masterFeedViewController.updateFeedSelection(animations: animations) if deselectArticle { selectArticle(nil) @@ -714,7 +714,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed { self.activityManager.selecting(feed: feed) - self.installTimelineControllerIfNecessary(animated: animated) + self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation)) setTimelineFeed(feed, animated: false) { completion?() } @@ -724,7 +724,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { setTimelineFeed(nil, animated: false) { self.activityManager.invalidateSelecting() if self.rootSplitViewController.isCollapsed && self.navControllerForTimeline().viewControllers.last is MasterTimelineViewController { - self.navControllerForTimeline().popViewController(animated: animated) + self.navControllerForTimeline().popViewController(animated: animations.contains(.navigation)) } completion?() } @@ -735,35 +735,35 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func selectPrevFeed() { if let indexPath = prevFeedIndexPath { - selectFeed(indexPath, animated: true) + selectFeed(indexPath, animations: [.navigation, .scroll]) } } func selectNextFeed() { if let indexPath = nextFeedIndexPath { - selectFeed(indexPath, animated: true) + selectFeed(indexPath, animations: [.navigation, .scroll]) } } func selectTodayFeed() { masterFeedViewController?.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 0, section: 0), animated: true) + self.selectFeed(IndexPath(row: 0, section: 0), animations: [.navigation, .scroll]) } } func selectAllUnreadFeed() { masterFeedViewController?.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 1, section: 0), animated: true) + self.selectFeed(IndexPath(row: 1, section: 0), animations: [.navigation, .scroll]) } } func selectStarredFeed() { masterFeedViewController?.ensureSectionIsExpanded(0) { - self.selectFeed(IndexPath(row: 2, section: 0), animated: true) + self.selectFeed(IndexPath(row: 2, section: 0), animations: [.navigation, .scroll]) } } - func selectArticle(_ article: Article?, animated: Bool = false) { + func selectArticle(_ article: Article?, animations: Animations = []) { guard article != currentArticle else { return } currentArticle = article @@ -772,23 +772,23 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { if article == nil { if rootSplitViewController.isCollapsed { if masterNavigationController.children.last is ArticleViewController { - masterNavigationController.popViewController(animated: animated) + masterNavigationController.popViewController(animated: animations.contains(.navigation)) } } else { articleViewController?.article = nil } - masterTimelineViewController?.updateArticleSelection(animated: animated) + masterTimelineViewController?.updateArticleSelection(animations: animations) return } let currentArticleViewController: ArticleViewController if articleViewController == nil { - currentArticleViewController = installArticleController(animated: animated) + currentArticleViewController = installArticleController(animated: animations.contains(.navigation)) } else { currentArticleViewController = articleViewController! } - masterTimelineViewController?.updateArticleSelection(animated: animated) + masterTimelineViewController?.updateArticleSelection(animations: animations) currentArticleViewController.article = article markArticles(Set([article!]), statusKey: .read, flag: true) @@ -861,13 +861,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func selectPrevArticle() { if let article = prevArticle { - selectArticle(article) + selectArticle(article, animations: [.navigation, .scroll]) } } func selectNextArticle() { if let article = nextArticle { - selectArticle(article) + selectArticle(article, animations: [.navigation, .scroll]) } } @@ -999,12 +999,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred) } - func discloseFeed(_ feed: WebFeed, animated: Bool, completion: (() -> Void)? = nil) { + func discloseFeed(_ feed: WebFeed, animations: Animations = [], completion: (() -> Void)? = nil) { if isSearching { masterTimelineViewController?.hideSearch() } - masterFeedViewController.discloseFeed(feed, animated: animated) { + masterFeedViewController.discloseFeed(feed, animations: animations) { completion?() } } @@ -1061,7 +1061,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func showAdd(_ type: AddControllerType, initialFeed: String? = nil, initialFeedName: String? = nil) { - selectFeed(nil, animated: false) + selectFeed(nil) let addViewController = UIStoryboard.add.instantiateInitialViewController() as! UINavigationController @@ -1193,7 +1193,7 @@ extension SceneCoordinator: UINavigationControllerDelegate { // If we are showing the Feeds and only the feeds start clearing stuff if viewController === masterFeedViewController && !isThreePanelMode && !isTimelineViewControllerPending { activityManager.invalidateCurrentActivities() - selectFeed(nil, animated: true) + selectFeed(nil, animations: [.scroll, .select, .navigation]) return } @@ -1203,7 +1203,7 @@ extension SceneCoordinator: UINavigationControllerDelegate { // ArticleViewController will be pushed, but we will breifly show the Timeline. Don't clear things out when that happens. if viewController === masterTimelineViewController && !isThreePanelMode && rootSplitViewController.isCollapsed && !isArticleViewControllerPending { currentArticle = nil - masterTimelineViewController?.updateArticleSelection(animated: animated) + masterTimelineViewController?.updateArticleSelection(animations: [.scroll, .select, .navigation]) activityManager.invalidateReading() // Restore any bars hidden by the article controller @@ -1296,7 +1296,7 @@ private extension SceneCoordinator { func clearTimelineIfNoLongerAvailable() { if let feed = timelineFeed, !shadowTableContains(feed) { - selectFeed(nil, animated: false, deselectArticle: true) + selectFeed(nil, deselectArticle: true) } } @@ -1461,7 +1461,7 @@ private extension SceneCoordinator { } if unreadCountProvider.unreadCount > 0 { - selectFeed(prevIndexPath, animated: true) + selectFeed(prevIndexPath, animations: [.scroll, .navigation]) return true } @@ -1502,7 +1502,7 @@ private extension SceneCoordinator { for i in startingRow.. 0 { - selectFeed(nextIndexPath, animated: false, deselectArticle: false) { + selectFeed(nextIndexPath, animations: [.scroll, .navigation], deselectArticle: false) { self.currentArticle = nil completion(true) } @@ -1889,7 +1889,7 @@ private extension SceneCoordinator { case .smartFeed: guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return } if let indexPath = indexPathFor(smartFeed) { - selectFeed(indexPath, animated: false) { + selectFeed(indexPath) { self.masterFeedViewController.focus() } } @@ -1902,7 +1902,7 @@ private extension SceneCoordinator { return } if let indexPath = indexPathFor(folderNode) { - selectFeed(indexPath, animated: false) { + selectFeed(indexPath) { self.masterFeedViewController.focus() } } @@ -1915,7 +1915,7 @@ private extension SceneCoordinator { treeControllerDelegate.addFilterException(folderFeedID) } if let feed = feedNode.representedObject as? WebFeed { - discloseFeed(feed, animated: false) { + discloseFeed(feed) { self.masterFeedViewController.focus() } } @@ -1951,7 +1951,7 @@ private extension SceneCoordinator { treeControllerDelegate.addFilterException(webFeedFeedID) addParentFolderToFilterExceptions(webFeedNode) - discloseFeed(webFeed, animated: false) { + discloseFeed(webFeed) { self.selectArticleInCurrentFeed(articleID) } } @@ -1969,7 +1969,7 @@ private extension SceneCoordinator { case .smartFeed: guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false } if let indexPath = indexPathFor(smartFeed) { - selectFeed(indexPath, animated: false) { + selectFeed(indexPath) { self.selectArticleInCurrentFeed(articleID) } treeControllerDelegate.addFilterException(feedIdentifier) @@ -2034,7 +2034,7 @@ private extension SceneCoordinator { func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool { if let feedIndexPath = indexPathFor(feedNode) { - selectFeed(feedIndexPath, animated: false) { + selectFeed(feedIndexPath) { self.selectArticleInCurrentFeed(articleID) } return true diff --git a/iOS/UIKit Extensions/Animations.swift b/iOS/UIKit Extensions/Animations.swift new file mode 100644 index 000000000..e43d83a5c --- /dev/null +++ b/iOS/UIKit Extensions/Animations.swift @@ -0,0 +1,27 @@ +// +// Animations.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 1/27/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation + +/// Used to select which animations should be performed +public struct Animations: OptionSet { + + /// Select and deslections will be animated. + public static let select = Animations(rawValue: 1) + + /// Scrolling will be animated + public static let scroll = Animations(rawValue: 2) + + /// Pushing and popping navigation view controllers will be animated + public static let navigation = Animations(rawValue: 4) + + public let rawValue: Int + public init(rawValue: Int) { + self.rawValue = rawValue + } +} diff --git a/iOS/UIKit Extensions/UITableView-Extensions.swift b/iOS/UIKit Extensions/UITableView-Extensions.swift new file mode 100644 index 000000000..3196a9c5a --- /dev/null +++ b/iOS/UIKit Extensions/UITableView-Extensions.swift @@ -0,0 +1,37 @@ +// +// UITableView-Extensions.swift +// RSCoreiOS +// +// Created by Maurice Parker on 9/6/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import UIKit + +extension UITableView { + + /** + Selects a row and scrolls it to the middle if it is not visible + */ + public func selectRowAndScrollIfNotVisible(at indexPath: IndexPath, animations: Animations) { + selectRow(at: indexPath, animated: animations.contains(.select), scrollPosition: .none) + + if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame) { + if !(visibleIndexPaths.contains(indexPath) && cellCompletelyVisable(indexPath)) { + selectRow(at: indexPath, animated: animations.contains(.scroll), scrollPosition: .middle) + } + } + } + + func cellCompletelyVisable(_ indexPath: IndexPath) -> Bool { + let rect = rectForRow(at: indexPath) + return safeAreaLayoutGuide.layoutFrame.contains(rect) + } + + public func middleVisibleRow() -> IndexPath? { + if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame), visibleIndexPaths.count > 2 { + return visibleIndexPaths[visibleIndexPaths.count / 2] + } + return nil + } +} diff --git a/submodules/RSCore b/submodules/RSCore index c548b018b..a59bb69cf 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit c548b018beb865c2a1792d15767a2b378bbdaa89 +Subproject commit a59bb69cf591cd68e7203caac424d2f69d980f1f