Skip to content

Commit

Permalink
Fix crash when diffing to/from empty/nil states to/from non-empty/nil…
Browse files Browse the repository at this point in the history
… states

Closes #124
Internal: IP-3275
  • Loading branch information
jessesquires committed Jul 10, 2018
1 parent 01bfcf9 commit d552263
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 99 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The changelog for `ReactiveLists`. Also see the [releases](https://github.com/pl

### Fixed

- Fixed a crash in diffing when transitioning to or from empty/nil states ([#125](https://github.com/plangrid/ReactiveLists/pull/125), [@jessesquires](https://github.com/jessesquires))

- Fixed incorrect calculation for `TableViewModel.isEmpty`. It now correctly returns true only if all sections return `true` for `isEmpty`. ([#123](https://github.com/plangrid/ReactiveLists/pull/123), [@jessesquires](https://github.com/jessesquires))

### New
Expand Down
18 changes: 5 additions & 13 deletions ReactiveLists.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -201,33 +201,26 @@
path = Example;
sourceTree = "<group>";
};
257A97BB2017A5AA00164403 /* Fixtures */ = {
isa = PBXGroup;
children = (
257A97BC2017A5AA00164403 /* TestCollectionViewModels.swift */,
257A97BD2017A5AA00164403 /* TestTableViewModels.swift */,
);
path = Fixtures;
sourceTree = "<group>";
};
257A97C32017A80B00164403 /* TableView */ = {
isa = PBXGroup;
children = (
257A97C52017A80B00164403 /* TableViewDriverTests.swift */,
257A97C62017A80B00164403 /* TableViewDiffingTests.swift */,
257A97C52017A80B00164403 /* TableViewDriverTests.swift */,
257A97C42017A80B00164403 /* TableViewMocks.swift */,
257A97C72017A80B00164403 /* TableViewModelTests.swift */,
257A97BD2017A5AA00164403 /* TestTableViewModels.swift */,
);
path = TableView;
sourceTree = "<group>";
};
257A97C82017A80B00164403 /* CollectionView */ = {
isa = PBXGroup;
children = (
257A97DB2017AA3500164403 /* CollectionViewMocks.swift */,
25B1B0B720195F160036545F /* CollectionViewDriverDiffingTests.swift */,
257A97C92017A80B00164403 /* CollectionViewDriverTests.swift */,
257A97DB2017AA3500164403 /* CollectionViewMocks.swift */,
257A97CA2017A80B00164403 /* CollectionViewModelTests.swift */,
25B1B0B720195F160036545F /* CollectionViewDriverDiffingTests.swift */,
257A97BC2017A5AA00164403 /* TestCollectionViewModels.swift */,
);
path = CollectionView;
sourceTree = "<group>";
Expand Down Expand Up @@ -288,7 +281,6 @@
isa = PBXGroup;
children = (
257A97C82017A80B00164403 /* CollectionView */,
257A97BB2017A5AA00164403 /* Fixtures */,
258E319F1F0D8CBC00D6F324 /* Info.plist */,
257A97C32017A80B00164403 /* TableView */,
257A97CB2017A80B00164403 /* Utils */,
Expand Down
61 changes: 36 additions & 25 deletions Sources/CollectionViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class CollectionViewDriver: NSObject {
assert(Thread.isMainThread, "Must set \(#function) on main thread")
}
didSet {
self._collectionViewModelDidChange()
self._collectionViewModelDidChange(from: oldValue)
}
}

Expand Down Expand Up @@ -85,7 +85,7 @@ public class CollectionViewDriver: NSObject {
super.init()
collectionView.dataSource = self
collectionView.delegate = self
self._collectionViewModelDidChange()
self._collectionViewModelDidChange(from: nil)
}

// MARK: Change and UI Update Handling
Expand Down Expand Up @@ -134,36 +134,46 @@ public class CollectionViewDriver: NSObject {

// MARK: Private

private func _collectionViewModelDidChange() {
guard let newModel = self.collectionViewModel else {
self.collectionView.reloadData()
return
private func _collectionViewModelDidChange(from: CollectionViewModel?) {
if let newModel = self.collectionViewModel {
self.collectionView.registerViews(for: newModel)
}

self.collectionView.registerViews(for: newModel)
let previousStateNilOrEmpty = (from == nil || from!.isEmpty)
let nextStateNilOrEmpty = (self.collectionViewModel == nil || self.collectionViewModel!.isEmpty)

if self.automaticDiffingEnabled {
if !self._didReceiveFirstNonNilValue {
// For the first non-nil value, we want to reload data, to avoid a weird
// animation where we animate in the initial state
self.collectionView.reloadData()
self._didReceiveFirstNonNilValue = true
// 1. we're moving *from* a nil/empty state
// or
// 2. we're moving *to* a nil/empty state
// in either case, simply reload and short-circuit, no need to diff
if previousStateNilOrEmpty || nextStateNilOrEmpty {
self.collectionView.reloadData()

if self.automaticDiffingEnabled
&& self.collectionViewModel != nil
&& !self._didReceiveFirstNonNilValue {
// Special case for the first non-nil value
// Now that we have this initial state, setup the differ with that initial state,
// so that the diffing works properly from here on out
self._didReceiveFirstNonNilValue = true
self._collectionViewDiffer = CollectionViewDiffCalculator<DiffingKey, DiffingKey>(
collectionView: self.collectionView,
initialSectionedValues: newModel.diffingKeys
)
} else if self._didReceiveFirstNonNilValue {
// If the current collection view model is empty, default to an empty set of diffing keys
if let differ = self._collectionViewDiffer {
let diffingKeys = newModel.diffingKeys
differ.sectionedValues = diffingKeys
self.refreshViews()
} else {
self.refreshViews()
}
initialSectionedValues: self.collectionViewModel!.diffingKeys)
}
return
}

guard let newModel = self.collectionViewModel else { return }

if self.automaticDiffingEnabled {

// If the current collection view model is empty, default to an empty set of diffing keys
if let differ = self._collectionViewDiffer {
let diffingKeys = newModel.diffingKeys
differ.sectionedValues = diffingKeys
self.refreshViews()
} else {
self.refreshViews()
}
} else {
self.refreshViews()
Expand Down Expand Up @@ -194,7 +204,8 @@ extension CollectionViewDriver: UICollectionViewDataSource {

/// :nodoc:
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.collectionViewModel?[section]?.cellViewModels.count ?? 0
guard let sectionModel = self.collectionViewModel?[section] else { return 0 }
return sectionModel.cellViewModels.count
}

/// :nodoc:
Expand Down
67 changes: 39 additions & 28 deletions Sources/TableViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ open class TableViewDriver: NSObject {
assert(Thread.isMainThread, "Must set \(#function) on main thread")
}
didSet {
self._tableViewModelDidChange()
self._tableViewModelDidChange(from: oldValue)
}
}

Expand Down Expand Up @@ -99,7 +99,7 @@ open class TableViewDriver: NSObject {
super.init()
tableView.dataSource = self
tableView.delegate = self
self._tableViewModelDidChange()
self._tableViewModelDidChange(from: nil)
}

// MARK: Change and UI Update Handling
Expand Down Expand Up @@ -161,40 +161,51 @@ open class TableViewDriver: NSObject {

// MARK: Private

private func _tableViewModelDidChange() {
guard let newModel = self.tableViewModel, !newModel.isEmpty else {
self.tableView.reloadData()
return
private func _tableViewModelDidChange(from: TableViewModel?) {
if let newModel = self.tableViewModel {
self.tableView.registerViews(for: newModel)
}

self.tableView.registerViews(for: newModel)
let previousStateNilOrEmpty = (from == nil || from!.isEmpty)
let nextStateNilOrEmpty = (self.tableViewModel == nil || self.tableViewModel!.isEmpty)

if self._automaticDiffingEnabled {
if !self._didReceiveFirstNonNilNonEmptyValue {
// For the first non-nil value, we want to reload data, to avoid a weird
// animation where we animate in the initial state
self.tableView.reloadData()
self._didReceiveFirstNonNilNonEmptyValue = true
// 1. we're moving *from* a nil/empty state
// or
// 2. we're moving *to* a nil/empty state
// in either case, simply reload and short-circuit, no need to diff
if previousStateNilOrEmpty || nextStateNilOrEmpty {
self.tableView.reloadData()

if self._automaticDiffingEnabled
&& self.tableViewModel != nil
&& !self._didReceiveFirstNonNilNonEmptyValue {
// Special case for the first non-nil value
// Now that we have this initial state, setup the differ with that initial state,
// so that the diffing works properly from here on out
self._didReceiveFirstNonNilNonEmptyValue = true
self._tableViewDiffer = TableViewDiffCalculator<DiffingKey, DiffingKey>(
tableView: self.tableView,
initialSectionedValues: newModel.diffingKeys
)
self._tableViewDiffer?.insertionAnimation = self.insertionAnimation
self._tableViewDiffer?.deletionAnimation = self.deletionAnimation
} else if self._didReceiveFirstNonNilNonEmptyValue {
// If the current table view model is empty, default to an empty set of diffing keys
if let differ = self._tableViewDiffer {
let diffingKeys = newModel.diffingKeys
let diff = Dwifft.diff(lhs: differ.sectionedValues, rhs: diffingKeys)
differ.sectionedValues = diffingKeys
let context: TableRefreshContext = !diff.isEmpty ? .rowsModified : .contentOnly
self.refreshViews(refreshContext: context)
} else {
self.refreshViews()
}
initialSectionedValues: self.tableViewModel!.diffingKeys)
}
return
}

guard let newModel = self.tableViewModel else { return }

if self._automaticDiffingEnabled && self._didReceiveFirstNonNilNonEmptyValue {

self._tableViewDiffer?.insertionAnimation = self.insertionAnimation
self._tableViewDiffer?.deletionAnimation = self.deletionAnimation

// If the current table view model is empty, default to an empty set of diffing keys
if let differ = self._tableViewDiffer {
let diffingKeys = newModel.diffingKeys
let diff = Dwifft.diff(lhs: differ.sectionedValues, rhs: diffingKeys)
differ.sectionedValues = diffingKeys
let context: TableRefreshContext = !diff.isEmpty ? .rowsModified : .contentOnly
self.refreshViews(refreshContext: context)
} else {
self.refreshViews()
}
} else {
self.refreshViews()
Expand Down
37 changes: 11 additions & 26 deletions Tests/CollectionView/CollectionViewDriverDiffingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,8 @@ final class CollectionViewDriverDiffingTests: XCTestCase {

self.collectionViewDataSource.collectionViewModel = updatedModel

XCTAssertEqual(self.mockCollectionView.callsToInsertItems.count, 1)
XCTAssertEqual(
self.mockCollectionView.callsToInsertItems[0],
[IndexPath(row: 0, section: 0)]
)
XCTAssertEqual(self.mockCollectionView.callsToInsertItems.count, 0)
XCTAssertEqual(self.mockCollectionView.callsToReloadData, 3)
}

/// Tests that changes to individual sections result in the correct calls to update the
Expand All @@ -76,38 +73,26 @@ final class CollectionViewDriverDiffingTests: XCTestCase {
/// - Note: We're only testing one type of section update since this is sufficient to test the
/// communication between the diffing lib and the collection view. The diffing lib itself has
/// extensive tests for the various diffing scenarios.
func testChangingSections() {

/// This is broken. Dwifft algo does not work.
func BROKEN_testChangingSections() {
let section = CollectionSectionViewModel(cellViewModels: generateCollectionCellViewModels(), diffingKey: "2")

let initialModel = CollectionViewModel(
sectionModels: [
CollectionSectionViewModel(
cellViewModels: [],
diffingKey: "1"
),
CollectionSectionViewModel(
cellViewModels: [],
diffingKey: "2"
)
CollectionSectionViewModel( cellViewModels: generateCollectionCellViewModels(), diffingKey: "1"),
section,
]
)

self.collectionViewDataSource.collectionViewModel = initialModel

let updatedModel = CollectionViewModel(
sectionModels: [
CollectionSectionViewModel(
cellViewModels: [],
diffingKey: "2"
)
]
)
let updatedModel = CollectionViewModel(sectionModels: [section])

self.collectionViewDataSource.collectionViewModel = updatedModel

XCTAssertEqual(self.mockCollectionView.callsToDeleteSections.count, 1)
XCTAssertEqual(
self.mockCollectionView.callsToDeleteSections[0],
IndexSet(integer: 0)
)
XCTAssertEqual(self.mockCollectionView.callsToDeleteSections[0], IndexSet(integer: 0))
}
}

Expand Down
18 changes: 15 additions & 3 deletions Tests/CollectionView/CollectionViewMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ typealias _RegisterClassCallInfo = (viewClass: AnyClass?, viewKind: Supplementar
class TestCollectionView: UICollectionView {

var callsToRegisterClass: [_RegisterClassCallInfo?] = []
var callsToDeselect: Int = 0
var callsToInsertItems: [[IndexPath]] = []
var callsToDeleteSections: [IndexSet] = []
var callsToDeselect = 0

var callsToReloadData = 0

var callsToInsertItems = [[IndexPath]]()
var callsToDeleteSections = [IndexSet]()

override func dequeueReusableCell(withReuseIdentifier identifier: String, for indexPath: IndexPath) -> UICollectionViewCell {
return TestCollectionViewCell(identifier: identifier)
Expand All @@ -39,17 +42,26 @@ class TestCollectionView: UICollectionView {
} else {
self.callsToRegisterClass.append(nil)
}
super.register(viewClass, forSupplementaryViewOfKind: elementKind, withReuseIdentifier: identifier)
}

override func deselectItem(at indexPath: IndexPath, animated: Bool) {
super.deselectItem(at: indexPath, animated: animated)
self.callsToDeselect += 1
}

override func insertItems(at indexPaths: [IndexPath]) {
super.insertItems(at: indexPaths)
self.callsToInsertItems.append(indexPaths)
}

override func deleteSections(_ sections: IndexSet) {
super.deleteSections(sections)
self.callsToDeleteSections.append(sections)
}

override func reloadData() {
super.reloadData()
self.callsToReloadData += 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ struct TestCollectionCellViewModel: CollectionCellViewModel {
let accessibilityFormat: CellAccessibilityFormat = "access-%{section}.%{row}"
let shouldHighlight = false

var diffingKey: DiffingKey {
return self.label
}

func applyViewModelToCell(_ cell: UICollectionViewCell) {
guard let testCell = cell as? TestCollectionViewCell else { return }
testCell.label = self.label
Expand Down
6 changes: 3 additions & 3 deletions Tests/TableView/TableViewDiffingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ final class TableViewDiffingTests: XCTestCase {
func testChangingSections() {
let initialModel = TableViewModel(sectionModels: [
TableSectionViewModel(
cellViewModels: [UserCell(user: "Name")],
cellViewModels: generateTableCellViewModels(),
diffingKey: "1"
),
TableSectionViewModel(
cellViewModels: [UserCell(user: "user")],
cellViewModels: generateTableCellViewModels(),
diffingKey: "2"
),
])
Expand All @@ -94,7 +94,7 @@ final class TableViewDiffingTests: XCTestCase {

let updatedModel = TableViewModel(sectionModels: [
TableSectionViewModel(
cellViewModels: [UserCell(user: "new")],
cellViewModels: generateTableCellViewModels(),
diffingKey: "2"
),
])
Expand Down
2 changes: 1 addition & 1 deletion Tests/TableView/TableViewMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class TestTableView: UITableView {
}

override func reloadData() {
super.reloadData()
super.reloadData()
self.callsToReloadData += 1
}
}
Expand Down
Loading

0 comments on commit d552263

Please sign in to comment.