Skip to content

Commit dbe746e

Browse files
Merge pull request #468 from Iterable/tapash/mob-2757-animation
[MOB-2757] - Fix animation glitch
2 parents 1315a39 + bf45fc9 commit dbe746e

File tree

3 files changed

+144
-19
lines changed

3 files changed

+144
-19
lines changed

swift-sdk/Internal/InboxViewControllerViewModel.swift

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,18 @@
55
import Foundation
66
import UIKit
77

8+
enum RowDiff {
9+
case insert(IndexPath)
10+
case delete(IndexPath)
11+
case update(IndexPath)
12+
case sectionInsert(IndexSet)
13+
case sectionDelete(IndexSet)
14+
case sectionUpdate(IndexSet)
15+
}
16+
817
protocol InboxViewControllerViewModelView: AnyObject {
918
// All these methods should be called on the main thread
10-
func onViewModelChanged(diff: [SectionedDiffStep<Int, InboxMessageViewModel>])
19+
func onViewModelChanged(diffs: [RowDiff])
1120
func onImageLoaded(for indexPath: IndexPath)
1221
var currentlyVisibleRowIndexPaths: [IndexPath] { get }
1322
}
@@ -238,13 +247,51 @@ class InboxViewControllerViewModel: InboxViewControllerViewModelProtocol {
238247
ITBInfo()
239248
newSectionedMessages = sortAndFilter(messages: getMessages())
240249

241-
let diff = Dwifft.diff(lhs: sectionedMessages, rhs: newSectionedMessages)
242-
if diff.count > 0 {
243-
view?.onViewModelChanged(diff: diff)
250+
let dwifftDiffs = Dwifft.diff(lhs: sectionedMessages, rhs: newSectionedMessages)
251+
if dwifftDiffs.count > 0 {
252+
let rowDiffs = Self.dwifftDiffsToRowDiffs(dwifftDiffs: dwifftDiffs)
253+
view?.onViewModelChanged(diffs: rowDiffs)
244254
updateVisibleRows()
245255
}
246256
}
247257

258+
private static func dwifftDiffsToRowDiffs(dwifftDiffs: [SectionedDiffStep<Int, InboxMessageViewModel>]) -> [RowDiff] {
259+
var result = [RowDiff]()
260+
var rowDeletes = [IndexPath: Int]()
261+
var sectionDeletes = [Int: Int]()
262+
263+
for (pos, dwiffDiff) in dwifftDiffs.enumerated() {
264+
switch dwiffDiff {
265+
case let .delete(section, row, _):
266+
let indexPath = IndexPath(row: row, section: section)
267+
result.append(.delete(indexPath))
268+
rowDeletes[indexPath] = pos
269+
case let .insert(section, row, _):
270+
let indexPath = IndexPath(row: row, section: section)
271+
if let pos = rowDeletes[indexPath] {
272+
result.remove(at: pos)
273+
rowDeletes.removeValue(forKey: indexPath)
274+
result.append(.update(indexPath))
275+
} else {
276+
result.append(.insert(indexPath))
277+
}
278+
case let .sectionDelete(section, _):
279+
result.append(.sectionDelete(IndexSet(integer: section)))
280+
sectionDeletes[section] = pos
281+
case let .sectionInsert(section, _):
282+
if let pos = sectionDeletes[section] {
283+
result.remove(at: pos)
284+
sectionDeletes.removeValue(forKey: section)
285+
result.append(.sectionUpdate(IndexSet(integer: section)))
286+
} else {
287+
result.append(.sectionInsert(IndexSet(integer: section)))
288+
}
289+
}
290+
}
291+
292+
return result
293+
}
294+
248295
@objc private func onAppWillEnterForeground(notification _: NSNotification) {
249296
ITBInfo()
250297
if sessionManager.startSessionWhenAppMovesToForeground {

swift-sdk/IterableInboxViewController.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -405,15 +405,15 @@ open class IterableInboxViewController: UITableViewController {
405405
}
406406

407407
extension IterableInboxViewController: InboxViewControllerViewModelView {
408-
func onViewModelChanged(diff: [SectionedDiffStep<Int, InboxMessageViewModel>]) {
408+
func onViewModelChanged(diffs: [RowDiff]) {
409409
ITBInfo()
410410

411411
guard Thread.isMainThread else {
412412
ITBError("\(#function) must be called from main thread")
413413
return
414414
}
415415

416-
updateTableView(diff: diff)
416+
updateTableView(diffs: diffs)
417417
updateUnreadBadgeCount()
418418
}
419419

@@ -437,23 +437,25 @@ extension IterableInboxViewController: InboxViewControllerViewModelView {
437437
navigationController?.tabBarItem?.badgeValue = badgeValue
438438
}
439439

440-
private func updateTableView(diff: [SectionedDiffStep<Int, InboxMessageViewModel>]) {
440+
private func updateTableView(diffs: [RowDiff]) {
441441
tableView.beginUpdates()
442442
viewModel.beganUpdates()
443443

444-
for result in diff {
445-
switch result {
446-
case let .delete(section, row, _): tableView.deleteRows(at: [IndexPath(row: row, section: section)], with: deletionAnimation)
447-
case let .insert(section, row, _): tableView.insertRows(at: [IndexPath(row: row, section: section)], with: insertionAnimation)
448-
case let .sectionDelete(section, _): tableView.deleteSections(IndexSet(integer: section), with: deletionAnimation)
449-
case let .sectionInsert(section, _): tableView.insertSections(IndexSet(integer: section), with: insertionAnimation)
444+
for diff in diffs {
445+
switch diff {
446+
case .delete(let indexPath): tableView.deleteRows(at: [indexPath], with: deletionAnimation)
447+
case .insert(let indexPath): tableView.insertRows(at: [indexPath], with: insertionAnimation)
448+
case .update(let indexPath): tableView.reloadRows(at: [indexPath], with: .automatic)
449+
case .sectionDelete(let indexSet): tableView.deleteSections(indexSet, with: deletionAnimation)
450+
case .sectionInsert(let indexSet): tableView.insertSections(indexSet, with: insertionAnimation)
451+
case .sectionUpdate(let indexSet): tableView.reloadSections(indexSet, with: .automatic)
450452
}
451453
}
452-
454+
453455
tableView.endUpdates()
454456
viewModel.endedUpdates()
455457
}
456-
458+
457459
private func isRowVisible(atIndexPath indexPath: IndexPath) -> IndexPath? {
458460
let topMargin = CGFloat(10.0)
459461
let bottomMargin = CGFloat(10.0)

tests/swift-sdk-swift-tests/inbox-tests/InboxViewControllerViewModelTests.swift

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class InboxViewControllerViewModelTests: XCTestCase {
264264
let internalAPI = InternalIterableAPI.initializeForTesting(networkSession: mockNetworkSession, inAppFetcher: fetcher)
265265
let model = InboxViewControllerViewModel(internalAPIProvider: internalAPI)
266266

267-
let mockView = MockViewModelView()
267+
let mockView = MockViewModelView(model: model)
268268
mockView.onImageLoadedCallback = { indexPath in
269269
XCTAssertNotNil(model.message(atIndexPath: indexPath).imageData)
270270
expectation1.fulfill()
@@ -302,7 +302,7 @@ class InboxViewControllerViewModelTests: XCTestCase {
302302
let internalAPI = InternalIterableAPI.initializeForTesting(networkSession: mockNetworkSession, inAppFetcher: fetcher)
303303
let model = InboxViewControllerViewModel(internalAPIProvider: internalAPI)
304304

305-
let mockView = MockViewModelView()
305+
let mockView = MockViewModelView(model: model)
306306
mockView.onImageLoadedCallback = { indexPath in
307307
XCTAssertNotNil(model.message(atIndexPath: indexPath).imageData)
308308
expectation1.fulfill()
@@ -373,15 +373,91 @@ class InboxViewControllerViewModelTests: XCTestCase {
373373
wait(for: [expectation1], timeout: testExpectationTimeout)
374374
}
375375

376+
func testRowDiff() {
377+
let expectation1 = expectation(description: "add one section and two rows")
378+
let expectation2 = expectation(description: "update first row")
379+
380+
let firstMessageDate = Date()
381+
let secondMessageDate = firstMessageDate.addingTimeInterval(-5.0)
382+
let fetcher = MockInAppFetcher()
383+
let internalAPI = InternalIterableAPI.initializeForTesting(inAppFetcher: fetcher)
384+
let model = InboxViewControllerViewModel(internalAPIProvider: internalAPI)
385+
let mockView = MockViewModelView(model: model)
386+
mockView.onViewModelChangedCallback = { diffs in
387+
if diffs.count == 3 {
388+
expectation1.fulfill()
389+
let messages = [
390+
IterableInAppMessage(messageId: "message1",
391+
campaignId: 1,
392+
trigger: IterableInAppTrigger(dict: [JsonKey.InApp.type: "never"]),
393+
createdAt: firstMessageDate,
394+
content: IterableHtmlInAppContent(edgeInsets: .zero, html: ""),
395+
saveToInbox: true,
396+
inboxMetadata: nil,
397+
customPayload: ["messageSection": 1],
398+
read: true),
399+
IterableInAppMessage(messageId: "message2",
400+
campaignId: 1,
401+
trigger: IterableInAppTrigger(dict: [JsonKey.InApp.type: "never"]),
402+
createdAt: secondMessageDate,
403+
content: IterableHtmlInAppContent(edgeInsets: .zero, html: ""),
404+
saveToInbox: true,
405+
inboxMetadata: nil,
406+
customPayload: nil),
407+
]
408+
fetcher.mockMessagesAvailableFromServer(internalApi: internalAPI, messages: messages)
409+
} else {
410+
if diffs.count == 1 {
411+
if case RowDiff.update(let indexPath) = diffs[0] {
412+
XCTAssertEqual(indexPath, IndexPath(row: 0, section: 0))
413+
expectation2.fulfill()
414+
}
415+
}
416+
}
417+
}
418+
model.view = mockView
419+
420+
let messages = [
421+
IterableInAppMessage(messageId: "message1",
422+
campaignId: 1,
423+
trigger: IterableInAppTrigger(dict: [JsonKey.InApp.type: "never"]),
424+
createdAt: firstMessageDate,
425+
content: IterableHtmlInAppContent(edgeInsets: .zero, html: ""),
426+
saveToInbox: true,
427+
inboxMetadata: nil,
428+
customPayload: ["messageSection": 1]),
429+
IterableInAppMessage(messageId: "message2",
430+
campaignId: 1,
431+
trigger: IterableInAppTrigger(dict: [JsonKey.InApp.type: "never"]),
432+
createdAt: secondMessageDate,
433+
content: IterableHtmlInAppContent(edgeInsets: .zero, html: ""),
434+
saveToInbox: true,
435+
inboxMetadata: nil,
436+
customPayload: nil),
437+
]
438+
fetcher.mockMessagesAvailableFromServer(internalApi: internalAPI, messages: messages)
439+
440+
wait(for: [expectation1, expectation2], timeout: testExpectationTimeout, enforceOrder: true)
441+
}
442+
376443
private class MockViewModelView: InboxViewControllerViewModelView {
377-
let currentlyVisibleRowIndexPaths: [IndexPath] = []
444+
init(model: InboxViewControllerViewModel) {
445+
self.model = model
446+
}
378447

448+
let currentlyVisibleRowIndexPaths: [IndexPath] = []
379449
var onImageLoadedCallback: ((IndexPath) -> Void)?
450+
var onViewModelChangedCallback: (([RowDiff]) -> Void)?
380451

381-
func onViewModelChanged(diff _: [SectionedDiffStep<Int, InboxMessageViewModel>]) {}
452+
func onViewModelChanged(diffs: [RowDiff]) {
453+
model.beganUpdates()
454+
onViewModelChangedCallback?(diffs)
455+
}
382456

383457
func onImageLoaded(for indexPath: IndexPath) {
384458
onImageLoadedCallback?(indexPath)
385459
}
460+
461+
private let model: InboxViewControllerViewModel
386462
}
387463
}

0 commit comments

Comments
 (0)