Coder Social home page Coder Social logo

ra1028 / differencekit Goto Github PK

View Code? Open in Web Editor NEW
3.5K 42.0 239.0 8.97 MB

๐Ÿ’ป A fast and flexible O(n) difference algorithm framework for Swift collection.

Home Page: https://ra1028.github.io/DifferenceKit

License: Apache License 2.0

Swift 97.12% Ruby 1.67% Makefile 0.52% Shell 0.68%
diff difference changeset collectionview tableview diffing algorithm paul-heckel-algorithm

differencekit's Introduction

โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘
โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–„โ–‘โ–‘โ–‘โ–„โ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–‘โ–€โ–„โ–‘โ–‘โ–‘โ–„โ–€โ–‘โ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘
โ–‘โ–‘โ–‘โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–ˆโ–€โ–ˆโ–ˆโ–ˆโ–€โ–ˆโ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆโ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–„โ–‘โ–‘
โ–‘โ–„โ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–„โ–‘โ–‘โ–‘โ–‘โ–ˆโ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–„โ–„โ–ˆโ–ˆโ–„โ–„โ–ˆโ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆโ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–„โ–ˆโ–ˆโ–ˆโ–‘
โ–‘โ–‘โ–‘โ–€โ–ˆโ–€โ–‘โ–‘โ–€โ–€โ–‘โ–‘โ–€โ–ˆโ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–ˆโ–‘โ–ˆโ–€โ–€โ–€โ–€โ–€โ–ˆโ–‘โ–ˆโ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–€โ–„โ–€โ–€โ–„โ–€โ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–€โ–„โ–„โ–€โ–„โ–‘โ–‘โ–‘
โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–€โ–‘โ–€โ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–„โ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–„โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–€โ–‘โ–€โ–‘โ–‘โ–€โ–‘โ–€โ–‘โ–‘
โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘โ–‘

differencekit's People

Contributors

alanzeino avatar dahlgren avatar dependabot[bot] avatar hallee avatar halleygen avatar ibsh avatar insidegui avatar kaspik avatar larryonoff avatar marcocanc avatar oskargroth avatar ra1028 avatar sameesunkaria avatar saucym avatar tobiasjordan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

differencekit's Issues

Add `completion` callback when UITableView / UICollectionView finished reloaded

Old reload function name is:

func reload<C>(
        using stagedChangeset: StagedChangeset<C>,
        with animation: @autoclosure () -> RowAnimation,
        interrupt: ((Changeset<C>) -> Bool)? = nil,
        setData: (C) -> Void
        )

I think it can add a completion callback like:

func reload<C>(
        using stagedChangeset: StagedChangeset<C>,
        with animation: @autoclosure () -> RowAnimation,
        interrupt: ((Changeset<C>) -> Bool)? = nil,
        setData: (C) -> Void,
        completion: ((Bool) -> Void)? = nil
        )

Crash on reload data

Checklist

Expected Behavior

I wanted update my table view with new data. My table view could work with 2 different cells: TitleTableViewCell and AppartmentCardTableViewCell. When I reload my table view I want to replace all TitleTableViewCell by some AppartmentCardTableViewCells. This is log of elements that were before updated and that I expected to see after update:

Before:

[ArraySection(
    model: TestApp.DiffableListSection<TestApp.ListSection>,
    elements: [TestApp.TableItemViewModel(differenceIdentifier: "tag-title-Yandex", data: TestApp.TitleDataModel(tag: Optional(TestApp.AddressEntity(dto: TestApp.AddressDTO(addressId: -1, addressName: "Yandex", longitude: 37.588144, latitude: 55.733842))), title: "Yandex"), reuseIdentifier: "TitleTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: Optional((Function)), style: Optional((Function))), TestApp.TableItemViewModel(differenceIdentifier: "tag-title-Yevropeysky", data: TestApp.TitleDataModel(tag: Optional(TestApp.AddressEntity(dto: TestApp.AddressDTO(addressId: -1, addressName: "Yevropeysky", longitude: 37.566278, latitude: 55.744796))), title: "Yevropeysky"), reuseIdentifier: "TitleTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: Optional((Function)), style: Optional((Function)))]
)]

After:

[ArraySection(
    model: TestApp.DiffableListSection<TestApp.ListSection>,
    elements: [TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil), TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil), TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil), TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil)]
)]

Current Behavior

I got this change set from DifferenceKit and then I call
tableView.reload(using: changeset, with: .fade, setData: { dataSource.setSections($0) })

Change set:

[
    Changeset(
        data: [
            ArraySection(
                model: TestApp.DiffableListSection<TestApp.ListSection>,
                elements: []
            )
        ],
        elementDeleted: [
            [element: 0, section: 0],
            [element: 1, section: 0],
            [element: 2, section: 0],
            [element: 3, section: 0],
            [element: 4, section: 0],
            [element: 5, section: 0],
            [element: 6, section: 0],
            [element: 7, section: 0],
            [element: 8, section: 0],
            [element: 9, section: 0]
        ]
    ),
    Changeset(
        data: [
            ArraySection(
                model: TestApp.DiffableListSection<TestApp.ListSection>,
                elements: [TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil), TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil), TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil), TestApp.TableItemViewModel(differenceIdentifier: "tag-47A539B5-3C8D-4687-BC45-32770BA5AD99", data: TestApp.EmptyDataViewModel(tag: Optional("47A539B5-3C8D-4687-BC45-32770BA5AD99")), reuseIdentifier: "AppartmentCardTableViewCell", heightStyle: TestApp.ListItemHeightStyle.automatic, map: nil, style: nil)]
            )
        ],
        elementInserted: [
            [element: 0, section: 0],
            [element: 1, section: 0],
            [element: 2, section: 0],
            [element: 3, section: 0]
        ]
    )
]

And after that I got a crash.

Trace:

2020-05-05 13:24:03.452034+0200 TestApp[86075:4912222] *** Assertion failure in -[TestApp.TableView _Bug_Detected_In_Client_Of_UITableView_Invalid_Number_Of_Rows_In_Section:], /Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3920.26.113/UITableView.m:2444
2020-05-05 13:24:03.457331+0200 TestApp[86075:4912222] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (10) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (0 inserted, 10 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff23e39f0e __exceptionPreprocess + 350
	1   libobjc.A.dylib                     0x00007fff50ad79b2 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff23e39c88 +[NSException raise:format:arguments:] + 88
	3   Foundation                          0x00007fff258a3cd2 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 191
	4   UIKitCore                           0x00007fff48da4a5d -[UITableView _Bug_Detected_In_Client_Of_UITableView_Invalid_Number_Of_Rows_In_Section:] + 193
	5   UIKitCore                           0x00007fff48da4462 -[UITableView _endCellAnimationsWithContext:] + 17036
	6   UIKitCore                           0x00007fff48dbde6d -[UITableView endUpdatesWithContext:] + 112
	7   UIKitCore                           0x00007fff48dbe019 -[UITableView _performBatchUpdates:withContext:completion:] + 253
	8   UIKitCore                           0x00007fff48dbe12f -[UITableView performBatchUpdates:completion:] + 97
	9   DifferenceKit                       0x00000001095f250c $sSo11UITableViewC13DifferenceKitE20_performBatchUpdates33_F8DD3376103BA2712B08CDB09297309BLLyyyyXEF + 348
	10  DifferenceKit                       0x00000001095f0bcf $sSo11UITableViewC13DifferenceKitE6reload5using23deleteSectionsAnimation06inserthI00ehI00g4RowsI00jkI00ekI09interrupt7setDatayAC15StagedChangesetVyxG_So0ab3RowI0VyXKARyXKARyXKARyXKARyXKARyXKSbAC0P0VyxGcSgyxXEtSlRzlF + 3151
	11  DifferenceKit                       0x00000001095efdea $sSo11UITableViewC13DifferenceKitE6reload5using4with9interrupt7setDatayAC15StagedChangesetVyxG_So0aB12RowAnimationVyXKSbAC0L0VyxGcSgyxXEtSlRzlF + 298
	12  TestApp                            0x0000000107d8c515 $s8TestApp11ListUpdaterC15updateTableView_4with11newSections0E9AnimationySo07UITableG0C_AA0fG18DataSourceAnimatedCyxGSayxGAA0c6UpdateK0OtAA0C7SectionCRbzlFZyycfU_ + 3333
	13  TestApp                            0x0000000107ba4990 $sIeg_IeyB_TR + 48
	14  libdispatch.dylib                   0x000000010b3f6f11 _dispatch_call_block_and_release + 12
	15  libdispatch.dylib                   0x000000010b3f7e8e _dispatch_client_callout + 8
	16  libdispatch.dylib                   0x000000010b405d97 _dispatch_main_queue_callback_4CF + 1149
	17  CoreFoundation                      0x00007fff23d9da89 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
	18  CoreFoundation                      0x00007fff23d985d9 __CFRunLoopRun + 2041
	19  CoreFoundation                      0x00007fff23d97ac4 CFRunLoopRunSpecific + 404
	20  GraphicsServices                    0x00007fff38b2fc1a GSEventRunModal + 139
	21  UIKitCore                           0x00007fff48bc7f80 UIApplicationMain + 1605
	22  TestApp                            0x0000000107d806db main + 75
	23  libdyld.dylib                       0x00007fff519521fd start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Environments

  • Library version: since 1.1.5 at least (reproducible on the latest version as well)
  • Swift version: Swift 5
  • iOS version: Any

Support for `NSTableView` sections (aka group rows)

Checklist

Description

The NSTableView extension doesn't apply section changes and indices assume a single section.

Motivation and Context

Like the UITableView, NSTableView supports sections (called "group rows" in AppKit). It would be great if they would just work in combination w/ ArraySection's.

Proposed Solution

In NSTableView the sections (group rows) are part of the collection feed to the tableview. E.g.

let source = [
    ArraySection(model: "Section 1", elements: ["A", "B", "C"]),
    ArraySection(model: "Section 2", elements: ["D", "E", "F"]),
]

Is presented to NSTableView as:

[0] "Section 1"
[1] "A"
[2] "B"
[3] "C"
[4] "Section 2"
[5] "D"
...

It's not obvious that this isn't supported. Maybe AppKitExtension.swift should have

            assert(changeset.sectionInserted.isEmpty, 
                        "srz, section updates not yet supported on AppKit!")
...

in the reload.

Not quite sure about the best way to implement this. The reload should have all the grouping information necessary. Essentially there would need to be a separate step adjusting the indices to the flat ones, while applying the changes.

Sample TV datasource/delegate on top of ArraySection

extension AppDelegate: NSTableViewDelegate {
  func tableView(_ tv: NSTableView, isGroupRow row: Int) -> Bool {
    switch test.data[flat: row] {
      case .some(.model): return true
      default: return false
    }
  }
  func tableView(_ tv: NSTableView, viewFor tc: NSTableColumn?, row: Int)
       -> NSView?
  {
    let label = NSTextField(labelWithString: test.data[flat: row]?.stringValue ?? "-")
    return label
  }
}
extension AppDelegate: NSTableViewDataSource {
  func numberOfRows(in tableView: NSTableView) -> Int {
    test.data.flatCount
  }
}

final class SectionedTest {
  
  var data = [ ArraySection<String, String> ]()
  
  func applyNewData(_ newValue: [ ArraySection<String, String> ],
                    on tableView: NSTableView)
  {
    let changeset = StagedChangeset(source: data, target: newValue)
    tableView.reload(using: changeset, with: .effectFade) { newValue in
      self.data = newValue
    }
  }
}

enum Row {
  case model(String)
  case element(String)
  var stringValue: String {
    switch self {
      case .model(let s), .element(let s): return s
    }
  }
}
extension Collection where Element == ArraySection<String, String> {
 
  var flatCount : Int {
    reduce(0) { $0 + 1 + $1.elements.count }
  }
  
  subscript(flat index: Int) -> Row? {
    var cursor = 0
    for section in self {
      if cursor == index { return .model(section.model) }
      cursor += 1
      if cursor > index { break }
      
      let offset = index - cursor
      assert(offset >= 0)
      if offset < section.elements.count {
        return .element(section.elements[offset])
      }
      cursor += section.elements.count
    }
    assertionFailure("index out of range \(index) \(count)")
    return nil
  }
}

Algorithm.swift using hashValue needs to be updated to use new Hashable pattern

The TableKey class does hashValue like this, which raises a warning in the upcoming Swift 5 compiler:

    internal let hashValue: Int
...
    internal static func == (lhs: TableKey, rhs: TableKey) -> Bool {
        return lhs.hashValue == rhs.hashValue
            && (lhs.pointer.distance(to: rhs.pointer) == 0 || lhs.pointer.pointee == rhs.pointer.pointee)
    }

This needs to be updated to use hash(into:) instead, which was added in Swift 4.2.

I believe this is as simple as just removing hashValue from TableKey and adding this:

    public func hash(into hasher: inout Hasher) {
        hasher.combine(pointer.pointee)
    }

If that makes sense I'm happy to open a PR.

Diff calculation sometimes misses `insert` operation

Sometimes I have a crash when one item is deleted and another one is inserted in the same section. Their diff identifiers are not equal. It looks that issue occurs when a table view is updated the second time or later (not the first).

The data structure looks in the following way.
before = [s1 :[1, 2], s2: [3]]
current = [s1 :[1, 2], s2: [4]]

So library calculates only delete of 3 in the section s2, but not insert of 4.

I don't have a good sample yet. But I'll do as soon as I catch it during debug.

Fix pod compilation in Xcode 10.2

I created a blank swift 4.2 project with Xcode 10.2 & cocoapods 1.6.0 and I specified the following Podfile:

platform :ios, '11.0'

target 'Test' do
  use_frameworks!

  pod 'DifferenceKit', '~> 1.0'

end

The project does not compile.

The DifferenceKit project compiles correctly on Xcode 10.2. So it seems to be only related to cocoapods.

`NSOutlineView` support

While NSOutlineView is a subclass of NSTableView, the existing NSTableView extension is insufficient if you're using an NSOutlineView, because it has its own insertion/deletion/reload API.

[QUESTION] StagedChangeset private(set) changesets

First of all, let me explain my use case, to see if this makes sense at all.

I have my UICollectionView organized by "modules", each section have a direct relation with a module.

At some point, I want to know which "sections" were removed, so I can notify my modules that they no longer need to update.

In order to do that, I would need to iterate through StagedChangeset's changesets, so I can join all of their sectionDeleted and get those indexes.

My question is: Is there a reason for changesets: ContiguousArray<Changeset<Collection>> getter being private.

I mean, what if it was public private(set)?
Would still avoid anyone from changing it, and it would allow to do some checks on its internal value.

public struct StagedChangeset<Collection: Swift.Collection> {
    private var changesets: ContiguousArray<Changeset<Collection>>
    ...

If you have any other idea on how I can do this, other than this one, please say so.
If not, may I submit a PR changing this to public private(set)

Thanks.

Delete/Insert vs. Update

Hi,

I love this library, but I'm having trouble figuring out the best practice for triggering an "update" to an existing element at a given location, versus having it be deleted and inserted. In my model, I'm just making a slight change to an element at a certain indexPath, and I'd like it to reload just that item instead of deleting the item and re-inserting it.

I'm assuming the problem is with my hashable or equatable implementation. Maybe I'm telling the library something wrong that's causing it to see an update as a delete/insert?

For a given item that implements Hashable and Equatable, how does DifferenceKit decide if an item is an updated version of the same element, or an entirely different element at the same location?

Thanks,

  • Conrad

Cells rendered incorrectly when using `UITableView.reload(using:, with:)` under certain contexts

Checklist

Given

An original list of three elements.

Component(title: "Shuffle Emojis", subtitle: "Shuffle sectioned Emojis in UICollectionView"),
Component(title: "Header Footer Section", subtitle: "Update header/footer by reload section in UITableView"),
Component(title: "Random", subtitle: "Random diff in UICollectionView")

If the list is updated so that the...

  • middle element is updated
  • first element is moved to the end

The resulting changeset will look like...

[
    Changeset(
        data: [
            Component(title: "Shuffle Emojis", subtitle: "Shuffle sectioned Emojis in UICollectionView"),
            Component(title: "Header Footer Section", subtitle: "XXX Update header/footer by reload section in UITableView"),
            Component(title: "Random", subtitle: "Random diff in UICollectionView")
        ],
        elementUpdated: [
            [element: 1, section: 0]
        ]
    ),
    Changeset(
        data: [
            Component(title: "Header Footer Section", subtitle: "XXX Update header/footer by reload section in UITableView"),
            Component(title: "Random", subtitle: "Random diff in UICollectionView"),
            Component(title: "Shuffle Emojis", subtitle: "Shuffle sectioned Emojis in UICollectionView")
        ],
        elementMoved: [
            (source: [element: 1, section: 0], target: [element: 0, section: 0]),
            (source: [element: 2, section: 0], target: [element: 1, section: 0])
        ]
    )
]

When

I apply this changeset to the UITableView with reload(using:, with:)...

Expected Behavior

The cells should animate appropriately and the cells should all be in their new positions with their updated values.

Current Behavior

Cells at index 0 and 1 are identical. Element 0 (originally 1) is not present in the UITableView.

Steps to Reproduce

I've modified the demo app in a fork to demonstrate the issue.

  1. Checkout https://github.com/OliverPearmain/DifferenceKit/tree/reload-with-move-bug
  2. Run the demo app
  3. The changeset will automatically be applied to the initial view contoller (so don't navigate anywhere, just watch the animation and resulting UITableView).

Detailed Description (Include Screenshots)

Please note that the "context" under which this happens is important because an identifcal changeset may not result in the same problem.

For instance, in the example code, if I rename the HomeViewController's components stored property to data then the problem goes away, WTF! The naming of properties should not affect the resulting animation, this is most peculiar. Demo code.

Reproducible Demo Project

https://github.com/OliverPearmain/DifferenceKit/tree/reload-with-move-bug

Environments

  • Library version: 1.1.5
  • Swift version: Swift 5
  • iOS version: 13.3
  • Xcode version: 11.3.1
  • Devices/Simulators: iPhone 11 - 13.3 Simulator
  • CocoaPods/Carthage version: n/a

Scrolling shutter since #40

Hey ๐Ÿ‘‹
First of all, awesome diff lib.

I started to detect some scrolling shutters when I updated to 0.7.2.

Checking the commits, I see that #40 introduced some setContentOffset calls and I think it may be related to that.

I notice this when I scroll on my UIColllectionView
Sometimes, my scroll is canceled, probably due to a setContentOffset call when a reload is applied when I'm on the middle of a scroll event.

Did anyone notice this on 0.7.2?

I can build sample app to demonstrate if needed.

Support macOS with Apple Silicon

Apple will release a new type of Mac having a different architecture than x86_64. This architecture is called Apple Silicon (arm64) and requires projects with custom VALID_ARCHS to add it in their VALID_ARCHS.

Checklist

Expected Behavior

DifferenceKit can be used on macOS having the Apple Silicon architecture.

Current Behavior

The Apple Silicon architecture (arm64) is not in the valid architectures of the project.

Steps to Reproduce

Build the framework and use the lipo -info command to see that the architecture is missing.

AppKitExtension wrong usage of changeset

Expected Behavior

The following code at line 80,81 of <AppKitExtension.swift> is referencing elementInserted where it should be elementUpdated

   if !changeset.elementUpdated.isEmpty {
                reloadData(forRowIndexes: IndexSet(changeset.elementInserted.map { $0.element }), columnIndexes: IndexSet(changeset.elementInserted.map { $0.section }))
            } 

Support `NSCollectionView` sections

Checklist

Description

The NSCollectionView reload extension method doesn't support sections.

Motivation and Context

Would be nice to have sections, just like on iOS :-)

Proposed Solution

Unlike #93 (NSTableView section support) this one should be an almost exact copy of the UICollectionView.reload implementation? AFAIK the NSCollectionView essentially works the same, at least since ~10.12. (The main difference is that NSCollectionView uses VCs instead of plain views as items, but that doesn't matter here).
deleteSections/insertSections etc. are all available.

Unexpected behavior when using the default implementations of `Differentiable` for `Hashable`

DifferenceKit provides a default implementation of Differentiable for Hashable. However, when using the default implementation, DifferenceKit always trigers a delete -> insert change instead of a update change.

Unexpected

I have a simple model that uses the default differenceIdentifier implementation.

struct ElementModel {
    // The identifier.
    var id: Int
    
    // The contents.
    var title: String
    var value: String
}

// Use the default `differenceIdentifier` implementation.
extension ElementModel: Hashable, Differentiable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

Create a changeset between two items that have the same id but different value.

let list1 = [ElementModel(id: 1, title: "color", value: "red")]
let list2 = [ElementModel(id: 1, title: "color", value: "blue")]

let changeset = StagedChangeset(source: list1, target: list2)
changeset.forEach { print($0) }

Unfortunately, the output message shows that there is a delete -> insert update.

Changeset(
    data: [],
    elementDeleted: [
        [element: 0, section: 0]
    ]
)
Changeset(
    data: [
        ElementModel(id: 1, title: "color", value: "blue")
    ],
    elementInserted: [
        [element: 0, section: 0]
    ]
)

Expected

If I explicitly implement the differenceIdentifier property by return hashValue, everything will be fine.

extension ElementModel: Hashable, Differentiable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
    
    var differenceIdentifier: Int {
        // Return `hashValue` insteal of `Self`
        return hashValue
    }
}

The expected update change.

Changeset(
    data: [
        ElementModel(id: 1, title: "color", value: "blue")
    ],
    elementUpdated: [
        [element: 0, section: 0]
    ]
)

Question

I'm not sure if this is a mistake of my code of a bug of DifferenceKit.

DifferenceKit provides a default implementation of Differentiable for Hashable, but the implementation of differenceIdentifier just return the Hashable instance itself. I think maybe the hashValue is the right return value.

// Return self
var differenceIdentifier: Self {
    return self
}

// Return hashValue
var differenceIdentifier: Int {
    return hashValue
}

`setData` closure gets called outside of `performBatchUpdates`

Hey! Thanks for an amazing tool
I tried it out in stress tests (will probably share the code later) and found unexpected crashes.
The issue was due to the following code
screen shot 2018-08-27 at 17 40 53

Here setData gets called not inside of performBatchUpdates.
But according to the docs, it should be done inside:

To avoid problems, you should update your data model inside the updates block or ensure the layout is updated before you call performBatchUpdates(_:completion:).

handle multiple sections

I want my tableview handle multiple sections with different Items.

my model is :

struct Object: Hashable, Comparable, Equatable {

    var index: Int
    var name: String
 
    var data: [Any]
}

and Object is section and data is row of cell

I went this way:

extension Object: Differentiable {

    var differenceIdentifier: String {
        return "\(index)"
    }

    func isContentEqual(to source: TypeStorableItem) -> Bool {
        index == source.index && name == source.name
    }
}

and in ViewController :

typealias AnyArraySection =  ArraySection<TypeStorableItem, AnyDifferentiable>

  var typeManager: [AnyArraySection] = []
    var inputTypeManager: [AnyArraySection] {
        get { typeManager }
        set {
            let changeset = StagedChangeset(source: typeManager, target: newValue)
            
            self.tableView.reload(using: changeset, with: .automatic) { data in
                self.typeManager = data
            }
        }
    }

when I want add item

let model = Object(index: index,
                                       name: identifier,
                                       data: data)
        item = ArraySection(model: model, elements: model.data)


how is it possible?
what about multiple sections with multiple types.

Detailed Description (Include Screenshots)

Environment

  • Library version:

  • Swift version:

  • iOS version:
    13

  • Xcode version:
    11.7

  • Devices/Simulators:

  • CocoaPods/Carthage version:

When a diff results in a section update + row move + section update, the data is incorrect and causes a crash.

Checklist

Expected Behavior

When a diff results in a section update + row move + section update, the data returned in the closure after each change should match the changeset.

Current Behavior

When a diff results in a section update + move move + section update, the data returned in the closure after the row move change does not reflect the move and causes a UITableView crash when applied.

Steps to Reproduce

I've provided a Playground to reproduce this issue, but here is a description of the steps I took:

  1. Create a section model such that isContentEqual returns false if the count of items changes.
  2. Create a simple row model
  3. Create a dataset so one row moves from one section to the other.
  4. This dataset should result in a changeset that is a section update/row move/section update when a row is moved.

Reproducible Demo Project

https://gist.github.com/SkylerSeamans/647b772a83be036f88946cee318420d5

Environments

  • Library version:
    1.1.5
  • Swift version:
    5
  • iOS version:
    13
  • Xcode version:
    11.5
  • Devices/Simulators:
    Both
  • CocoaPods/Carthage version:
    1.9.1

Align Differentiable with Identifiable protocol

Checklist

Description

Since the identity component of Differentiable is semantically identical to the new Identifiable protocol in Swift 5.1, I think it would be beneficial to align the two APIs.

Motivation and Context

This allows us to easily conform to Identifiable as well as Differentiable through a single implementation.

Proposed Solution

  • Separate Equality (ContentEquatable) and Identification (ContentIdentifiable) component through the use of protocol composition for Differentiable
  • Align the API of ContentIdentifiable to that of Identifiable

See #81 for implementation details

Add prebuilt carthage framework

Checklist

Description

Add a prebuilt carthage framework to every DifferenceKit release to speed up build times when using it in apps.

Motivation and Context

I'm using carthage as dependency manager for my projects and I use DifferenceKit extensively, thank you for the awesome work! Carthage builds all the dependencies from source every time, unless they have a prebuilt framework attached to the github release. By attaching it, the client simply downloads the framework and build times are much faster.

Proposed Solution

## do this only the first time on your mac or CI
## https://github.com/github/hub
brew install hub

## to create a release, from the project's root
carthage build --archive
hub release create -a DifferenceKit.framework.zip -m "tag comment" "tag version"

And you will get a release like this (taken from RxSwift)
Schermata 2019-05-10 alle 11 44 06

reload cell or header after move Transition ?

Hello i love Difference Kit and i have a question.

When i have changes in my datasource i call:

collectionView.reload(using: changeset) { (data) in
                self.data = data
            }

this works, but i need an update cell ,when i have a move transition from an element from section A to section B. In the section A all elements has cell.alpha = 1 and in sectionB all elements has cell.alpha = 0.5. (i use this in my example for a shopping list, section A has the items I still need and section B I already have :-))

No cellForItemAtIndexPath or willDisplayCell are calling in the UICollectionViewDataSource.

I hope someone can help me

Greetz

Pod DifferenceKit not found

pod 'DifferenceKit/Core'
Unable to find a specification for DifferenceKit/Core

pod search DifferenceKit
[!] Unable to find a pod with name, author, summary, or description matching DifferenceKit

[Question] What is the proper way to load large amount of data from database

Thanks for the awesome work :)

I have a large number of records in database (10w), and present them in an collection view. What is the proper way to utilize differencekit in this scenario.

My understandings:

  • differencekit each compare takes O(n), so no matter how tiny the changes are, it still O(n).
  • requires all dataset in memory.

Compare with Changeset?

Thanks for releasing DifferenceKit, it looks really cool.

Iโ€™m the author of Changeset, another fairly popular lightweight diffing framework. When you have the time, maybe I can persuade you to include it in your comparison?

Thanks in advance.

-[__NSDictionaryM setObject:forKey:]: key cannot be nil'

I am integrating differencekit with asyncdisplaykit's collectionNode in my application. I followed the instructions as mentioned in the doc and examples. But getting weird error I couldn't figure out where exactly it is happening.
Normal reloadData() works perfectly fine. But using

//self is a ASCollectionNode
self.view.reload(using: changeset) { (data) in
                    self.products = newValue
                }

Making the application to crash.

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
*** First throw call stack:
(
	0   CoreFoundation                      0x0000000102e9b1e6 __exceptionPreprocess + 294
	1   libobjc.A.dylib                     0x0000000101feb031 objc_exception_throw + 48
	2   CoreFoundation                      0x0000000102edb0bc _CFThrowFormattedException + 194
	3   CoreFoundation                      0x0000000102dae72a -[__NSDictionaryM setObject:forKey:] + 1002
	4   UIKit                               0x00000001044faf07 -[UICollectionView _setVisibleView:forLayoutAttributes:] + 171
	5   UIKit                               0x0000000104513dcb __71-[UICollectionView _updateWithItems:tentativelyForReordering:animator:]_block_invoke.1997 + 744
	6   UIKit                               0x0000000103aef537 +[UIView(UIViewAnimationWithBlocks) _setupAnimationWithDuration:delay:view:options:factory:animations:start:animationStateGenerator:completion:] + 560
	7   UIKit                               0x0000000103aefa0f +[UIView(UIViewAnimationWithBlocks) animateWithDuration:delay:options:animations:completion:] + 99
	8   UIKit                               0x00000001045130f5 -[UICollectionView _updateWithItems:tentativelyForReordering:animator:] + 6349
	9   UIKit                               0x000000010450ccc3 -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:animator:] + 17420
	10  UIKit                               0x000000010451500e -[UICollectionView _endUpdatesWithInvalidationContext:tentativelyForReordering:animator:] + 71
	11  UIKit                               0x0000000104515357 -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:animator:] + 439
	12  UIKit                               0x000000010451517d -[UICollectionView _performBatchUpdates:completion:invalidationContext:tentativelyForReordering:] + 91
	13  UIKit                               0x00000001045150ff -[UICollectionView _performBatchUpdates:completion:invalidationContext:] + 74
	14  UIKit                               0x0000000104515054 -[UICollectionView performBatchUpdates:completion:] + 53
	15  AsyncDisplayKit                     0x0000000100cd3e3b -[ASCollectionView _superPerformBatchUpdates:completion:] + 571
	16  AsyncDisplayKit                     0x0000000100ce9552 __64-[ASCollectionView rangeController:updateWithChangeSet:updates:]_block_invoke + 882
	17  AsyncDisplayKit                     0x0000000100ce91ab _ZL30ASPerformBlockWithoutAnimationbU13block_pointerFvvE + 123
	18  AsyncDisplayKit                     0x0000000100ce8e83 -[ASCollectionView rangeController:updateWithChangeSet:updates:] + 3859
	19  AsyncDisplayKit                     0x0000000100df694d -[ASRangeController dataController:updateWithChangeSet:updates:] + 717
	20  AsyncDisplayKit                     0x0000000100d02bf6 __40-[ASDataController updateWithChangeSet:]_block_invoke_2.224 + 230
	21  AsyncDisplayKit                     0x0000000100dcb0b4 __30-[ASMainSerialQueue runBlocks]_block_invoke + 308
	22  libdispatch.dylib                   0x00000001075c27ab _dispatch_call_block_and_release + 12
	23  libdispatch.dylib                   0x00000001075c37ec _dispatch_client_callout + 8
	24  libdispatch.dylib                   0x00000001075ce8cf _dispatch_main_queue_callback_4CF + 628
	25  CoreFoundation                      0x0000000102e5dc99 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
	26  CoreFoundation                      0x0000000102e21ea6 __CFRunLoopRun + 2342
	27  CoreFoundation                      0x0000000102e2130b CFRunLoopRunSpecific + 635
	28  GraphicsServices                    0x000000010ae53a73 GSEventRunModal + 62
	29  UIKit                               0x0000000103a2d057 UIApplicationMain + 159
	30  Sodimac                             0x0000000100705027 main + 55
	31  libdyld.dylib                       0x0000000107640955 start + 1
	32  ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

I don't know what I am missing. Any help would be much appreciated. Thanks in advance.

Upgrade supported Swift versions to include Swift 5

Checklist

Description

The podspec's swift_version supports providing multiple versions. It would be great to add Swift 5 or just upgrade to Swift 5-only altogether.

Motivation and Context

Swift 5 is the latest stable version of Swift, and we're trying to get all of our pods building using Swift 5.

Proposed Solution

spec.swift_versions = ['4.2', '5.0'] or spec.swift_version = '5.0'

Array<T: ContentEquatable> does not conform to ContentEquatable

Checklist

Description

Similar to the conditional Equatable conformance Array<T: ContentEquatable> should also conditionally conform to ContentEquatable.

Motivation and Context

This potentially simplifies the implementation of ContentEquatable and thus Differentiable of a container type, if it contains an array of equatable/differentiable items.

Proposed Solution

extension Array: ContentEquatable where Element: ContentEquatable {
    @inlinable
    public func isContentEqual(to source: Array<Element>) -> Bool {
        return count == source.count
            && zip(self, source).allSatisfy { $0.isContentEqual(to: $1) }
    }
}

Multiple sections with different types

I want my collection handle multiple sections with different Items.

Section1: Int
Section2: String

how is it possible?
you have showcased one section with multiple items with AnyDifferentiable, but what about multiple sections with multiple types.

Unnecessary Moves

Checklist

Example Test Scenario

    func testMovedIssue() {
        let section = 1
        
        let source1 = [1, 2, 3]
        let target1 = [3, 2, 1]

        // Test1: This test passes, but it should fail.
        XCTAssertExactDifferences(
            source: source1,
            target: target1,
            section: section,
            expected: [
                Changeset(
                    data: target1,
                    elementMoved: [
                        (source: ElementPath(element: 2, section: section), target: ElementPath(element: 0, section: section)),
                        (source: ElementPath(element: 1, section: section), target: ElementPath(element: 1, section: section)),
                    ]
                )
            ]
        )
        
        // Test2: This test fails, but it should pass.
        XCTAssertExactDifferences(
            source: source1,
            target: target1,
            section: section,
            expected: [
                Changeset(
                    data: target1,
                    elementMoved: [
                        (source: ElementPath(element: 2, section: section), target: ElementPath(element: 0, section: section)),
                    ]
                )
            ]
        )
    }

Expected Behavior

Test1 should fail, Test2 should pass.

Moves contains (2 -> 0), (1 -> 1).

Current Behavior

Test1 passes, Test2 fails.

Moves should contain (2 -> 0).

Steps to Reproduce

Add the test I wrote above and run the unit tests.

Detailed Description (Include Screenshots)

Moving the item from the same index to the same index should always be unnecessary. In general optimizing to make moves correct and minimal is preferred.

Environments

  • Library version: Master

  • Swift version: 4.2

  • iOS version: 12

  • Xcode version: 10.1

  • Devices/Simulators: DifferenceKit, "My Mac", CMD+U

  • CocoaPods/Carthage version: N/A

AppKit sample

Hi,

isn't the dataInput property superfluous?

Dropping dataInput and just adding a didSet seems to work fine, but maybe I'm missing something:

    private var data = (0x1F600...0x1F647).compactMap { UnicodeScalar($0).map(String.init) } {
        didSet {
            let changeset = StagedChangeset(source: oldValue, target: data)
            collectionView.reload(using: changeset) { data in }
            tableView.reload(using: changeset, with: .effectFade) { data in }
        }
    }

Using DifferenceKit in Objective-C project

Hello,
Can this framework be used in an Objective-C project?

If yes, are there any examples of how to do this?
If no, what alternative can you suggest?

It seems to me that no. I've still tried but when I set my object to conform to protocol Differentiable Xcode can find it's declaration (I've imported the xxx-Swift.h in my object header).

how can we declare an array with protocol that extends from Differentiable

my datasource is an array with a protocol like

protocol Entity {
    var title:String {get}
    var id:String {get}
    and so ......
}

var dataSource:[Entity] = []

the code works good
but if I extends the protocol with Differentiable :

protocol Entity:  Differentiable {
    var title:String {get}
    var id:String {get}
}

var dataSource:[Entity] = []

xcode says that Protocol 'Entity' can only be used as a generic constraint because it has Self or associated type requirements

now my solution is copying your codes in to my project and delete the DifferenceIdentifier in Differentiable and change func isContentEqual(to source: Self) -> Bool to func isContentEqual(to source: Any) -> Bool

but is there any better solutions ?
thanks, DifferenceKit is awesome

Including DifferenceKit as a sub-project / SDKROOT issues

Hey there,

first of all thanks for putting this online!

I'm having issues integrating DiffereceKit via an Xcode sub-project (added via git submodule) in an iOS project, on Xcode 10 beta 6.
I found that Xcode's not able to pick and set the right SDKROOT for DiffereceKit in command-line archive builds (same on Xcode server). So while the main project builds fine for iphoneos, once Xcode starts building DifferenceKit, it misses that and instead builds it with the macos SDK. (And then later fails linking).

I've put together a sample project: https://github.com/diederich/DifferenceKitArchive
This is how I can make the archive fail:
xcodebuild archive -project DifferenceKitArchive.xcodeproj -scheme DifferenceKitArchive

I haven't tried this on Xcode 9 yet, neither with the old build system.
Have you seen anything like that?

Is it possible to create a diff with (source: nil, target: foo)?

Checklist

I have a situation where I'm dealing with generics.

I have an object of protocol Foo, where Foo is a Collection, and Foo.Element conforms to Differentiable.

In the case where I get my first Foo, I have one value. I can provide a source or target but not both. I can't instantiate Foo because I'm dealing with protocols.

Once I get my second Foo, happy day. We have two things to diff.

My data is not sectioned. It's just two lists that I want to apply the heckel diff to, in order to get the insert/update/delete/move info.

How can I deal with the first situation, where I have just one Foo?

How can I use this library if I don't care about sectioning?

How can I use this when I'm dealing with protocols/non-concrete types?

Is it possible to make something close to the internal differentiate function public, with a simpler interface (exposing few as possible implementation details of the internal usages (e.g. from StagedChangeset)

Clues in how to debug "failed to demangle witness for associated type 'DifferenceIdentifier' in conformance ....

This may be an Apple bug, but if anyone has time to give me pointers in diagnosing the root cause, I would appreciate it.

I'm seeing a crash:

failed to demangle witness for associated type 'DifferenceIdentifier' in conformance 'DifferenceKit.ArraySection<Tapestry.ProgressSection, DifferenceKit.AnyDifferentiable>: Differentiable' from mangled name '20DifferenceIdentifier0A3Kit14DifferentiablePQz'"

The crash trace says it occurs in this line:

let changeset = StagedChangeset(source: self.data, target: newData)

And inside that this line:

#6	(null) in diff<A, B>(source:target:useTargetIndexForUpdated:mapIndex:updatedElementsPointer:notDeletedEleme... ()

The crash only seems to occur on iOS 9.x devices (specifically, the old iPad Minis, so possibly the key is that it is 32 bit).

The crash does not occur in builds that are directly loaded from XCode onto a device, only on builds that are processed in TestFlight or the AppStore.

My guess is that this is therefore an Apple bug in how they are recompiling for older devices.

But if you have the time to offer any clues or hints as to the root cause of the bug so I can try and work around it, that would be very much appreciated.

Invalid Update: invalid number of sections. crash

Checklist

Expected Behavior

TableView to display some messages sectioned by date (Date is a string) and messages are Differentiable with ids to differ them

Current Behavior

It crashes at UIKitExtensions.swift > performBatchUpdates(:) with the following exception

"Invalid update: invalid number of sections. The number of sections contained in the table view after the update (3) must be equal to the number of sections contained in the table view before the update (0), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted)."

Detailed Description (Include Screenshots)

The Section is String which conforms to Differentiable

the Elements Models are like this
image
image

I get the messages from the ViewModel and map it to ArraySection<String, Message>
like this
image

then the reload is like this
image

I've been following articles and the documentation and made sure I follow them strictly but It keeps giving me this error

am I missing something?

Environment

  • Library version: 1.1.5

  • Swift version: 5.1

  • iOS version: 13.5

  • Xcode version: 11.5

  • Devices/Simulators: Simulator iPhone 11

  • CocoaPods/Carthage version: CocoaPods 1.9.1

[BUG] An incorrect data array is generated for StagedChangeset

I have encountered a bug where an incorrect data array is generated when an element moves from an existing section to a new section.

I have been able to modify one of the example projects to demonstrate this bug (Specifically the table view for the iOS example). Just tap the refresh button a couple of times to see the crash. ๐Ÿ’ฅ

Here's a link to the commit.
Sameesunkaria@9582bff

Thanks for your efforts! ๐Ÿป

Number of minimal stages that can perform batch updates with no crashes.

Checklist

Description

Decrease the number of minimal stages to 1 when changeset is applied to a view with single section.

Motivation and Context

When a stage is applied it makes the UICollectionViewLayout to be recalculated. So, we have multiple stages, multiple batch updates and multiple layout recalculations.

It's not an issue when you have regular cells with regular behavior. But, when it comes to layout invalidation on bounds change, the number of stages matter.

I have a layout with some levitating headers and footers. They are hidden/shown whenever you scroll the opposite direction. Also they have their own behavior range.

Whenever layout is recalculated - the content size changes. And since we have deletes before insertions, the content size shrinks first and behavior range of some view also shrinks. But it won't be restored when the last stage inserts are applied, the layout isn't aware of the number of stages. And I believe it shouldn't be...

However, in performBatchUpdates of UITableView, UICollectionView, etc, there are combinations of diffs that cause crash when applied simultaneously.

Since some other frameworks provide a single changeset for 1-dimensional collection, I assumed that it is possible to live without crashes in a single UIKit section. I decided to move from RxDataSources to some other diffing framework because RxDataSources is a 3-staged solution. Also, I made the layout to have a single UIKit section but with multiple virtual sections.

So, I've made a generic solution to test on real data in my app. I've wrapped DifferenceKit, IGListDiffKit, DeepDiff and Swift.CollectionDifference solutions and run them one by one for each view update.

The most common scenario is scrolling down the infinite collection and append more and more data. Usually DifferenceKit is 2x-3x time slower than Swift.CollectionDifference in this scenario. IGListDiffKit and DeepDiff are 5x-6x slower than Swift.CollectionDifference. But there are cases when Swift.CollectionDifference is 10x-20x slower than DifferenceKit and calculation time might be 1-2 seconds while DifferenceKit usually consumes not more than 100ms.

DifferenceKit looks like the most stable solution and I'd love to move forward with it. But it doesn't provide a 1-staged solution for 1-dimensional collection.

Are there any known combinations to crash a view with single section?
Is it possible to provide a 1-staged solution for 1-dimensional collection?

Proposed Solution

I have nothing to propose...

didn't expected difference result of Reversed array

Checklist

Expected Behavior

i am using difference kit to get diffing from 2 array, and apply the difference manually. i am only using 1 dimension array
to resolve moved, i will

func handleDiff(_ lhs: [Array], rhs: [Array]) {
   let changes = StagedChangeset(source: lhs, target: rhs)
   changes.forEach { change in
       change.elementMoved.forEach { source, target in
                ...
                /// handle delete, insert
                ...
                let sourceElement = result[source.element]
                result.remove(at: source.element)
                result.insert(sourceElement, at: target.element)
              // after this function, result should be == with rhs
       }
   }
}

let's say i have array [1, 2, 3, 4, 5]
and the array reversed into [5, 4, 3, 2, 1]

the changes should be 4 -> 0, 4 -> 1, 4->2, 4->3
instead current behaviour 4->0, 3->1, 2->2, 1->3

if i resolve the move, then the result will be [4, 0, 1, 2, 3]

Current Behavior

result is wrong, got [4, 0, 1, 2, 3]

Steps to Reproduce

let array = [AnyDifferentiable("0"), AnyDifferentiable("1"), AnyDifferentiable("2"), AnyDifferentiable("3")]
let array2 = [AnyDifferentiable("3"), AnyDifferentiable("2"), AnyDifferentiable("0"), AnyDifferentiable("1")]

let changes = StagedChangeset(source: array, target: array2)
print(changes) // -> got `3->0, 2->1`, should be `3->0, 3->1, 3->2`

Environments

  • Library version: 1.1.5
  • Swift version: 5.2.4
  • iOS version: 11
  • Xcode version: 11.6
  • Devices/Simulators:
  • CocoaPods/Carthage version:

Missing Deletions

Checklist

Expected Behavior

My StagedChangeset's stages should contain a deletion of indexes: [1]

Current Behavior

My StagedChangeset's stages contains deletions of [] for all stages.

Steps to Reproduce

        // MockItem uses:
        // Just `diffID` comparison for `differenceIdentifier`.
        // Just `content` comparison for `isContentEqual`.
        // Both `diffID` and `content` comparison for `==` `Equatable` implementation of `MockItem`.

        let mock1a = MockItem(diffID: 1, content: "a")  
        let mock2 = MockItem(diffID: 2, content: "a")  
        let mock3a = MockItem(diffID: 3, content: "a")
        let mock3b = MockItem(diffID: 3, content: "b")  
        let mock4 = MockItem(diffID: 4, content: "a")
        
        let first = [mock1a, mock2, mock3a]
        let second = [mock3b, mock1a, mock4]

Expect changes to contain three changes:

  1. Updates: [2]. Should transform [mock1a, mock2, mock3a] to [mock1a, mock2, mock3b].
  2. Deletes: [1]. Should transform [mock1a, mock2, mock3b] to [mock1a, mock3b].
  3. Inserts and Moves. Inserts: [2]. Moves: (0, 1) or (1, 0). Should transform [mock1a, mock3b] to [mock3b, mock1a, mock4].

Actual Deletes is [] instead of [1] during step (2).

Environments

  • Library version: Master

  • Swift version: 4.2

  • iOS version: 12

  • Xcode version: 10.1

  • Devices/Simulators: DifferenceKit, "My Mac", CMD+U

  • CocoaPods/Carthage version: N/A

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.