Git Product home page Git Product logo

datasource's Introduction

DataSource

Swift 5 Carthage compatible CocoaPods compatible

Framework to simplify the setup and configuration of UITableView data sources and cells. It allows a type-safe setup of UITableViewDataSource and (optionally) UITableViewDelegate. DataSource also provides out-of-the-box diffing and animated deletions, inserts, moves and changes.

Usage

An example app is included demonstrating DataSource's functionality. The example demonstrates various uses cases ranging from a simple list of strings to more complex uses cases such as setting up a dynamic form.

Getting Started

Create a DataSource with a CellDescriptor that describes how the UITableViewCell (in this case a TitleCell) is configured using a data model (Example). Additionally, we also add a handler for didSelect which handles the didSelectRowAtIndexPath method of UITableViewDelegate.

let dataSource: DataSource = {
    DataSource(
        cellDescriptors: [
            CellDescriptor<Example, TitleCell>()
                .configure { (example, cell, indexPath) in
                    cell.textLabel?.text = example.title
                    cell.accessoryType = .disclosureIndicator
                }
                .didSelect { (example, indexPath) in
                    self.performSegue(withIdentifier: example.segue, sender: nil)
                    return .deselect
                }
            ])
    }()
)

Next, setup your dataSource as the dataSource and delegate of UITableView.

tableView.dataSource = dataSource
tableView.delegate = dataSource

Next, create and set the models. Don't forget to call reloadData.

dataSource.sections = [
    Section(items: [
        Example(title: "Random Persons", segue: "showRandomPersons"),
        Example(title: "Form", segue: "showForm"),
        Example(title: "Lazy Rows", segue: "showLazyRows"),
        Example(title: "Diff & Update", segue: "showDiff"),
    ])
]

dataSource.reloadData(tableView, animated: false)

Sections

DataSource can also be used to configure section headers and footers. Similar to CellDescriptors you can define one or more SectionDescriptors:

let dataSource: DataSource = {
    DataSource(
        cellDescriptors: [
            CellDescriptor()
                .configure { (person, cell, indexPath) in
                    cell.configure(person: person)
                }
        ],
        sectionDescriptors: [
            SectionDescriptor<String>()
                .header { (title, _) in
                    .title(title)
                }
        ])
}()

Sections headers and footers can have custom views (.view(...)) or simple titles (.title(...)). Delegate methods such as heightForHeaderInSection are supported as well (headerHeight).

Diffing

Diffing and animated changes between two sets of data are supported if your data models implement the Diffable protocol.

public protocol Diffable {    
    var diffIdentifier: String { get }    
    func isEqualToDiffable(_ other: Diffable?) -> Bool
}

diffIdentifier is a String identifier which describes whether the identity of two models is different. Think of it as a primary key in a database. Different diffIdentifiers will lead to an animated insert, delete or move change. Additionally, isEqualToDiffable can be used to describe whether the data or content of a models has changed even if the diffIdentifier is the same. For example, if the name of a person was changed in a database, the primarykey of that person would usually remain the same. In such cases you usually don't want an insert, delete or move but instead and (potentially animated) update of the corresponding row in your table.

Diffing is demonstrated by two examples:

RandomPersonsViewController creates a random set of persons in two sections and animates the changes between the datasets.

private func randomData() -> [SectionType] {
    let count = Int.random(5, 15)

    let persons = (0 ..< count).map { _ in Person.random()  }.sorted()

    let letters = Set(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"])

    let firstGroup = persons.filter {
        $0.lastNameStartsWith(letters: letters)
    }

    let secondGroup = persons.filter {
        !$0.lastNameStartsWith(letters: letters)
    }

    return [
        Section("A - L", items: firstGroup),
        Section("M - Z", items: secondGroup),
    ]
}

@IBAction func refresh(_ sender: Any) {
    dataSource.sections = randomData()
    dataSource.reloadData(tableView, animated: true)
}

DiffViewController creates rows of numbers where the diffIdentifier is the number itself and the content is the name of that number in English or German. This shows how the animated row changes can be accomplished.

struct DiffItem {    
    let value: Int
    let text: String
    let diffIdentifier: String

    init(_ value: Int, text: String) {
        self.value = value
        self.text = text
        self.diffIdentifier = String(value)
    }
}

extension DiffItem: Diffable {    
    public func isEqualToDiffable(_ other: Diffable?) -> Bool {
        guard let other = other as? DiffItem else { return false }
        return self.text == other.text
    }
}

Please refer to the examples for the full code.

Hiding Rows and Sections

Both, CellDescriptor and SectionDescriptor provide an isHidden closure, which allow to simply hide and show rows based on any custom criteria.

The FormViewController example uses this to only show the last name field whenever the first name is not empty, and also shows an "additional fields" section whenever a switch is enabled:

lazy var dataSource: DataSource = {
    DataSource(
        cellDescriptors: [
            TextFieldCell.descriptor
                .isHidden { (field, indexPath) in
                    if field.id == self.lastNameField.id {
                        return self.firstNameField.text?.isEmpty ?? true
                    } else {
                        return false
                    }
                },
            SwitchCell.descriptor,
            TitleCell.descriptor,
        ],
        sectionDescriptors: [
            SectionDescriptor<Void>("section-name")
                .headerHeight { .zero },

            SectionDescriptor<Void>("section-additional")
                .header {
                    .title("Additional Fields")
                }
                .isHidden {
                    !self.switchField.isOn
                }
        ])
    }()

The isHidden closure is evaluated whenever dataSource.reloadData(...) is called.

Delegates and Fallbacks

DataSource provides a convenient way to handle all UITableViewDelegate methods in a type-safe and simple way using closures. In most cases you define those closures on the CellDescriptor or SectionDescriptor. However, sometimes this leads to duplicated code if, for example, you have different cells but the code executed for a selection is the same. In these cases you can set the delegate closures on the DataSource itself:

dataSource.didSelect = { (row, indexPath) in
    print("selected")
    return .deselect
}

These closures will be used as a fallback if no closure for the specific delegate method is defined on CellDescriptor (or SectionDescriptor).

Additionally, you can set a fallback UITableViewDelegate and UITableViewDataSource, which are again used if the matching closure on CellDescriptor or SectionDescriptor is not set.

dataSource.fallbackDelegate = self
dataSource.fallbackDataSource = self

Using these fallback mechanisms you can choose which parts of DataSource you want to use in your specific use case. For example, you could use it to setup and configure all your cells, animate changes between datasets but keep your existing UITableViewDelegate code.

The fallbackDelegate can be used to implement methods that don't belong to DataSource, like e.g. UIScrollViewDelegate methods. You should take extra care that the fallback delegate needs to be set before setting the table view delegate, otherwise certain delegate methods will never be called by UIKit.

// Always set the fallback before setting the table view delegate
dataSource.fallbackDelegate = self
tableView.dataSource = dataSource
tableView.delegate = dataSource

Custom bundles

Cells can be registered from custom bundles. You can specify in the cell descriptor from which bundle the cell should be loaded. The bundle defaults to the main bundle.

let descriptor = CellDescriptor(bundle: customBundle)

Cell Registration

If you define your cell types in a separate xib(outside your tableView definition in a storyboard) or entirely in code your cell needs to be registered with the tableView you want to use it with. You can either register the cell with the tableView manually(see UITableView docs) or let DataSource do that for you by conforming to the AutoRegisterCell protocol.

Version Compatibility

Current Swift compatibility breakdown:

Swift Version Framework Version
5.1 8.x
5.0 7.x
4.2 6.x
4.1 5.x
3.x 3.x, 4.x

Installation

Swift Package Manager (Recommended)

Add the following dependency to your Package.swift file:

.package(url: "https://github.com/allaboutapps/DataSource.git", from: "8.0.0")

Carthage

Add the following line to your Cartfile.

github "allaboutapps/DataSource", ~> 8.0

Then run carthage update.

CocoaPods

For DataSource, use the following entry in your Podfile:

pod 'MBDataSource'

Then run pod install.

In any file you'd like to use DataSource in, don't forget to import the framework with import DataSource.

Manually

Just drag and drop the .swift files in the DataSource folder into your project.

Contributing

  • Create something awesome, make the code better, add some functionality, whatever (this is the hardest part).
  • Fork it
  • Create new branch to make your changes
  • Commit all your changes to your branch
  • Submit a pull request

Contact

Contact me at matthias.buchetics.com or follow me on Twitter.

datasource's People

Contributors

amuehleder avatar aschuch avatar draskovits avatar gunterhager avatar heinzl avatar matthiasbuchetics-aaa avatar mbuchetics avatar michaelpeternell avatar mpoimer avatar mswagner avatar oliverkrakora avatar vuln3r avatar wieweb avatar zettlunic 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

datasource's Issues

Crash in DataSource+UITableViewDelegate.swift line 471

I saw a crash in an app of mine, reported via Firebase, in the implementation of tableView(_:trailingSwipeActionsConfigurationForRowAt:).

Affected OS versions: iOS 14.4.1, iOS 14.4.2, iOS 14.5.1, iOS 14.6.0
Affected DataSource version: 8.1.1
Affected Phones: iPhone 7, iPhone 8, iPhone 11, iPhone 11 Pro, iPhone 12, iPhone XR, iPhone SE2
Xcode version used: 12.4

Bildschirmfoto 2021-06-15 um 11 32 37

In the end, the crash lies in IndexPath.section.getter, and an Apple employee explains that this happens because IndexPath.indices.count != 2.

Unfortunately, I can't reproduce the crash and it happens rarely, and I only know about it because of crash reporting. I don't know if it is an iOS bug, a bug in my app, or a bug in DataSource, but I think it can be fixed by adding a safety check to DataSource.

The problem

DataSource.tableView(_:trailingSwipeActionsConfigurationForRowAt:) is called with a bad indexPath that doesn't have two indices (i.e. indexPath.indices.count != 2. But all IndexPath values that are used for a table view should have exactly 2 indices: the "section" and the "row".)
The line let cellDescriptor = self.cellDescriptor(at: indexPath) as? CellDescriptorTypeiOS11 is therefore called with a bad IndexPath.
That method calls DataSource.visibleRow(at:), which is implemented as visibleSections[indexPath.section].visibleRow(at: indexPath.row). And that crashes because indexPath.section crashes.

Possible solution

I would suggest to add a safety check to DataSource.tableView(_:trailingSwipeActionsConfigurationForRowAt:) and DataSource.tableView(_:leadingSwipeActionsConfigurationForRowAt:) like this:

if indexPath.indices.count != 2 {
    return nil
}

because if indexPath.indices.count is not 2, the next line will crash deterministically.


(I could create a pull request if you want)

Crash when using cells defined in storyboard

When using Cells that were defined in storyboard (not in .xib or programmatically) the DataSource crashes.

We think it has to do with the DataSource trying to register the cell, even though that was already done by the storyboard.

It is likely caused by a change introduced in #24.

Update .podspec with next version number

The .podspec currently specifies version 5.2.0 but the latest tag is 6.x, which leads to some weirdness when I try to run pod update MBDataSource.

My local temporary fix was to write in my Podfile:

pod 'MBDataSource', :git => 'https://github.com/allaboutapps/DataSource.git', :tag => '6.1.2'

But since the podspec is out of date, Cocoapods thinks 5.2 is installed.

Unfortunately I had to investigate the files to confirm that I had the latest code.

Can you update the podspec with the new version, and then move the latest version tag to that commit?

Thanks

Naming-Conflict with Nested Classes or Structs

class TestOne {
    struct Item {
        let value: String
    }
}

class TestTwo {
    struct Item {
        let value: Int
    }
}

dataSource.sections = [Section(items: [TestOne.Item(value: "Hello"), TestTwo.Item(value: 5)])]

This will force a crash because String(describing: type(of: item) will always return Item instead of needed TestOne.Item, TestTwo.Item. So all Items do have the same identifier and cellDescriptor will fail.

Release 1.0

As soon as we feel comfortable with the feature set, we should cut a 1.0 release.

Improve section diffing

Currently, a Section uses its identifier for matching a SectionDescriptor as well as creating the DiffableSection which is used to compute a diff.
These should be separated.
If the content of a section is Diffable, its diffIdentifier should be used for DiffableSection instead.

Add verbose logging

Add a possibility to get more information about the used (or missing) identifiers, results of the diffying etc. to debug problems.

Bug - TrailingSwipeAction + SeparatedSection leads to 'Fatal error: Index out of range'

To reproduce this kind of error, add the following code to the datasource of the SeparatedSectionViewController (Example project) and try to swipe the last cell of the section:

.canEdit { [weak self] (_, _) -> Bool in
    return true
}
.trailingSwipeAction { [weak self] (_, _) -> UISwipeActionsConfiguration? in
    return UISwipeActionsConfiguration(actions: [
        UIContextualAction(style: .destructive, title: "Delete", handler: { [weak self] (_, _, callback) in
            callback(true)
        })
    ])
},

Clarify how to update sections & rows

update(rows:) sets rows and visibleRows and may lead to unexpected behaviour when being used in combination with reloadData. Update documentation on how to update section contents.

Cartfile missing

Please add a Cartfile to your repository.

The project currently doesn't build with carthage, because carthage cannot detect its dependency on the Differ.framework. carthage uses the Cartfile for dependency tracking, it doesn't even look at Cartfile.resolved.

This doesn't matter if you have Differ included for other reasons. It also doesn't matter if you have Differ already in your Carthage/Build folder and do a carthage update, because carthage update doesn't delete obsolete frameworks from the build folder.

To reproduce this isssue, just create a new empty project with just the DataSource dependency built via carthage. The app does not compile.

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.