Fix handling of how sections were added and remove

This commit is contained in:
Maurice Parker 2021-10-20 20:37:29 -05:00
parent bbc7230e76
commit 08a1e79e7d
5 changed files with 159 additions and 116 deletions

View File

@ -2071,6 +2071,7 @@
51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */,
51CE1C0A23622006005548FC /* RefreshProgressView.swift */,
51CE1C0823621EDA005548FC /* RefreshProgressView.xib */,
5195C1D92720205F00888867 /* ShadowTableChanges.swift */,
51C45260226508F600C03939 /* Cell */,
);
path = MasterFeed;
@ -2691,7 +2692,6 @@
840D617E2029031C009BC708 /* AppDelegate.swift */,
519E743422C663F900A78E47 /* SceneDelegate.swift */,
5126EE96226CB48A00C22AFC /* SceneCoordinator.swift */,
5195C1D92720205F00888867 /* ShadowTableChanges.swift */,
514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */,
511D4410231FC02D00FB1562 /* KeyboardManager.swift */,
51C45254226507D200C03939 /* AppAssets.swift */,

View File

@ -572,7 +572,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
}
}
func reloadFeeds(initialLoad: Bool, changes: [ShadowTableChanges], completion: (() -> Void)? = nil) {
func reloadFeeds(initialLoad: Bool, changes: ShadowTableChanges, completion: (() -> Void)? = nil) {
updateUI()
guard !initialLoad else {
@ -581,26 +581,36 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
return
}
for change in changes {
guard !change.isEmpty else { continue }
tableView.performBatchUpdates {
if let deletes = changes.deletes, !deletes.isEmpty {
tableView.deleteSections(IndexSet(deletes), with: .middle)
}
tableView.performBatchUpdates {
if let deletes = change.deleteIndexPaths, !deletes.isEmpty {
tableView.deleteRows(at: deletes, with: .middle)
if let inserts = changes.inserts, !inserts.isEmpty {
tableView.insertSections(IndexSet(inserts), with: .middle)
}
if let moves = changes.moves, !moves.isEmpty {
for move in moves {
tableView.moveSection(move.from, toSection: move.to)
}
if let inserts = change.insertIndexPaths, !inserts.isEmpty {
tableView.insertRows(at: inserts, with: .middle)
}
if let moves = change.moveIndexPaths, !moves.isEmpty {
for move in moves {
tableView.moveRow(at: move.0, to: move.1)
}
if let rowChanges = changes.rowChanges {
for rowChange in rowChanges {
if let deletes = rowChange.deleteIndexPaths, !deletes.isEmpty {
tableView.deleteRows(at: deletes, with: .middle)
}
if let inserts = rowChange.insertIndexPaths, !inserts.isEmpty {
tableView.insertRows(at: inserts, with: .middle)
}
if let moves = rowChange.moveIndexPaths, !moves.isEmpty {
for move in moves {
tableView.moveRow(at: move.0, to: move.1)
}
}
}
if let reloads = change.reloadIndexPaths, !reloads.isEmpty {
tableView.reloadRows(at: reloads, with: .middle)
}
}
}

View File

@ -0,0 +1,70 @@
//
// ShadowTableChanges.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 10/20/21.
// Copyright © 2021 Ranchero Software. All rights reserved.
//
import Foundation
struct ShadowTableChanges {
struct Move: Hashable {
var from: Int
var to: Int
init(_ from: Int, _ to: Int) {
self.from = from
self.to = to
}
}
struct RowChanges {
var section: Int
var deletes: Set<Int>?
var inserts: Set<Int>?
var moves: Set<ShadowTableChanges.Move>?
var isEmpty: Bool {
return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true)
}
var deleteIndexPaths: [IndexPath]? {
guard let deletes = deletes else { return nil }
return deletes.map { IndexPath(row: $0, section: section) }
}
var insertIndexPaths: [IndexPath]? {
guard let inserts = inserts else { return nil }
return inserts.map { IndexPath(row: $0, section: section) }
}
var moveIndexPaths: [(IndexPath, IndexPath)]? {
guard let moves = moves else { return nil }
return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) }
}
init(section: Int, deletes: Set<Int>?, inserts: Set<Int>?, moves: Set<Move>?) {
self.section = section
self.deletes = deletes
self.inserts = inserts
self.moves = moves
}
}
var deletes: Set<Int>?
var inserts: Set<Int>?
var moves: Set<Move>?
var rowChanges: [RowChanges]?
init(deletes: Set<Int>?, inserts: Set<Int>?, moves: Set<Move>?, rowChanges: [RowChanges]?) {
self.deletes = deletes
self.inserts = inserts
self.moves = moves
self.rowChanges = rowChanges
}
}

View File

@ -89,7 +89,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
private var expandedTable = Set<ContainerIdentifier>()
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
private var shadowTable = [[FeedNode]]()
private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
private(set) var preSearchTimelineFeed: Feed?
private var lastSearchString = ""
@ -205,8 +205,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
let prevIndexPath: IndexPath? = {
if indexPath.row - 1 < 0 {
for i in (0..<indexPath.section).reversed() {
if shadowTable[i].count > 0 {
return IndexPath(row: shadowTable[i].count - 1, section: i)
if shadowTable[i].feedNodes.count > 0 {
return IndexPath(row: shadowTable[i].feedNodes.count - 1, section: i)
}
}
return nil
@ -224,9 +224,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
}
let nextIndexPath: IndexPath? = {
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
if indexPath.row + 1 >= shadowTable[indexPath.section].feedNodes.count {
for i in indexPath.section + 1..<shadowTable.count {
if shadowTable[i].count > 0 {
if shadowTable[i].feedNodes.count > 0 {
return IndexPath(row: 0, section: i)
}
}
@ -316,7 +316,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
for sectionNode in treeController.rootNode.childNodes {
markExpanded(sectionNode)
shadowTable.append([FeedNode]())
shadowTable.append((sectionID: "", feedNodes: [FeedNode]()))
}
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
@ -675,19 +675,19 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
}
func numberOfRows(in section: Int) -> Int {
return shadowTable[section].count
return shadowTable[section].feedNodes.count
}
func nodeFor(_ indexPath: IndexPath) -> Node? {
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else {
return nil
}
return shadowTable[indexPath.section][indexPath.row].node
return shadowTable[indexPath.section].feedNodes[indexPath.row].node
}
func indexPathFor(_ node: Node) -> IndexPath? {
for i in 0..<shadowTable.count {
if let row = shadowTable[i].firstIndex(of: FeedNode(node)) {
if let row = shadowTable[i].feedNodes.firstIndex(of: FeedNode(node)) {
return IndexPath(row: row, section: i)
}
}
@ -699,8 +699,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
}
func cappedIndexPath(_ indexPath: IndexPath) -> IndexPath {
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else {
return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else {
return IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1)
}
return indexPath
}
@ -1502,7 +1502,7 @@ private extension SceneCoordinator {
func addShadowTableToFilterExceptions() {
for section in shadowTable {
for feedNode in section {
for feedNode in section.feedNodes {
if let feed = feedNode.node.representedObject as? Feed, let feedID = feed.feedID {
treeControllerDelegate.addFilterException(feedID)
}
@ -1530,26 +1530,27 @@ private extension SceneCoordinator {
}
}
func rebuildShadowTable() -> [ShadowTableChanges] {
var newShadowTable = [[FeedNode]]()
func rebuildShadowTable() -> ShadowTableChanges {
var newShadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
for i in 0..<treeController.rootNode.numberOfChildNodes {
var result = [FeedNode]()
var feedNodes = [FeedNode]()
let sectionNode = treeController.rootNode.childAtIndex(i)!
if isExpanded(sectionNode) {
for node in sectionNode.childNodes {
result.append(FeedNode(node))
feedNodes.append(FeedNode(node))
if isExpanded(node) {
for child in node.childNodes {
result.append(FeedNode(child))
feedNodes.append(FeedNode(child))
}
}
}
}
newShadowTable.append(result)
let sectionID = (sectionNode.representedObject as? Account)?.accountID ?? ""
newShadowTable.append((sectionID: sectionID, feedNodes: feedNodes))
}
// If we have a current Feed IndexPath it is no longer valid and needs reset.
@ -1557,15 +1558,17 @@ private extension SceneCoordinator {
currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject)
}
// Compute the differences in the shadow tables
var changes = [ShadowTableChanges]()
// Compute the differences in the shadow table rows
var changes = [ShadowTableChanges.RowChanges]()
for (index, newSectionNodes) in newShadowTable.enumerated() {
for (section, newSectionRows) in newShadowTable.enumerated() {
var moves = Set<ShadowTableChanges.Move>()
var inserts = Set<Int>()
var deletes = Set<Int>()
let diff = newSectionNodes.difference(from: shadowTable[index]).inferringMoves()
let oldFeedNodes = shadowTable.first(where: { $0.sectionID == newSectionRows.sectionID })?.feedNodes ?? [FeedNode]()
let diff = newSectionRows.feedNodes.difference(from: oldFeedNodes).inferringMoves()
for change in diff {
switch change {
case .insert(let offset, _, let associated):
@ -1583,17 +1586,42 @@ private extension SceneCoordinator {
}
}
changes.append(ShadowTableChanges(section: index, deletes: deletes, inserts: inserts, moves: moves))
changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, moves: moves))
}
// Compute the difference in the shadow table sections
var moves = Set<ShadowTableChanges.Move>()
var inserts = Set<Int>()
var deletes = Set<Int>()
let oldSections = shadowTable.map { $0.sectionID }
let newSections = newShadowTable.map { $0.sectionID }
let diff = newSections.difference(from: oldSections).inferringMoves()
for change in diff {
switch change {
case .insert(let offset, _, let associated):
if let associated = associated {
moves.insert(ShadowTableChanges.Move(associated, offset))
} else {
inserts.insert(offset)
}
case .remove(let offset, _, let associated):
if let associated = associated {
moves.insert(ShadowTableChanges.Move(offset, associated))
} else {
deletes.insert(offset)
}
}
}
shadowTable = newShadowTable
return changes
return ShadowTableChanges(deletes: deletes, inserts: inserts, moves: moves, rowChanges: changes)
}
func shadowTableContains(_ feed: Feed) -> Bool {
for section in shadowTable {
for feedNode in section {
for feedNode in section.feedNodes {
if let nodeFeed = feedNode.node.representedObject as? Feed, nodeFeed.feedID == feed.feedID {
return true
}
@ -1742,9 +1770,9 @@ private extension SceneCoordinator {
let nextIndexPath: IndexPath = {
if indexPath.row - 1 < 0 {
if indexPath.section - 1 < 0 {
return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
return IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1)
} else {
return IndexPath(row: shadowTable[indexPath.section - 1].count - 1, section: indexPath.section - 1)
return IndexPath(row: shadowTable[indexPath.section - 1].feedNodes.count - 1, section: indexPath.section - 1)
}
} else {
return IndexPath(row: indexPath.row - 1, section: indexPath.section)
@ -1754,7 +1782,7 @@ private extension SceneCoordinator {
if selectPrevUnreadFeedFetcher(startingWith: nextIndexPath) {
return
}
let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1)
let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1)
selectPrevUnreadFeedFetcher(startingWith: maxIndexPath)
}
@ -1768,7 +1796,7 @@ private extension SceneCoordinator {
if indexPath.section == i {
return indexPath.row
} else {
return shadowTable[i].count - 1
return shadowTable[i].feedNodes.count - 1
}
}()
@ -1847,7 +1875,7 @@ private extension SceneCoordinator {
// Increment or wrap around the IndexPath
let nextIndexPath: IndexPath = {
if indexPath.row + 1 >= shadowTable[indexPath.section].count {
if indexPath.row + 1 >= shadowTable[indexPath.section].feedNodes.count {
if indexPath.section + 1 >= shadowTable.count {
return IndexPath(row: 0, section: 0)
} else {
@ -1882,7 +1910,7 @@ private extension SceneCoordinator {
}
}()
for j in startingRow..<shadowTable[i].count {
for j in startingRow..<shadowTable[i].feedNodes.count {
let nextIndexPath = IndexPath(row: j, section: i)
guard let node = nodeFor(nextIndexPath), let unreadCountProvider = node.representedObject as? UnreadCountProvider else {

View File

@ -1,65 +0,0 @@
//
// ShadowTableChanges.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 10/20/21.
// Copyright © 2021 Ranchero Software. All rights reserved.
//
import Foundation
public struct ShadowTableChanges {
public struct Move: Hashable {
public var from: Int
public var to: Int
init(_ from: Int, _ to: Int) {
self.from = from
self.to = to
}
}
public var section: Int
public var deletes: Set<Int>?
public var inserts: Set<Int>?
public var moves: Set<Move>?
public var reloads: Set<Int>?
public var isEmpty: Bool {
return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true) && (reloads?.isEmpty ?? true)
}
public var isOnlyReloads: Bool {
return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true)
}
public var deleteIndexPaths: [IndexPath]? {
guard let deletes = deletes else { return nil }
return deletes.map { IndexPath(row: $0, section: section) }
}
public var insertIndexPaths: [IndexPath]? {
guard let inserts = inserts else { return nil }
return inserts.map { IndexPath(row: $0, section: section) }
}
public var moveIndexPaths: [(IndexPath, IndexPath)]? {
guard let moves = moves else { return nil }
return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) }
}
public var reloadIndexPaths: [IndexPath]? {
guard let reloads = reloads else { return nil }
return reloads.map { IndexPath(row: $0, section: section) }
}
init(section: Int, deletes: Set<Int>? = nil, inserts: Set<Int>? = nil, moves: Set<Move>? = nil, reloads: Set<Int>? = nil) {
self.section = section
self.deletes = deletes
self.inserts = inserts
self.moves = moves
self.reloads = reloads
}
}