Fix handling of how sections were added and remove
This commit is contained in:
parent
bbc7230e76
commit
08a1e79e7d
@ -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 */,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
70
iOS/MasterFeed/ShadowTableChanges.swift
Normal file
70
iOS/MasterFeed/ShadowTableChanges.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user