Reuse 1.0.0

Reuse 1.0.0

Maintained by Oren Farhan.



Reuse 1.0.0

Reuse

Version License Platform

About Reuse

Reuse was created in an attempt to avoid the tedious, repetitive work required to populate UITableViews/UICollectionViews, in a simple to use, unified way.

It uses the concept of InstanceReusers, a single configurator to manage all similar items. Once created, all data will be provided from the InstanceReuser to drive the UI. It will also handle interaction and database updates.

Example

Say we need to display a list of people. Let's follow the most common pattern.

First, we create some structure to hold our data:

class PeopleDatabase {
    
    var people: [Person]
    
    init(people: [Person]) {
        self.people = people
    }
}

Second, we have our view controller:

class PeopleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    private let cellIdentifier: String = "person.cell.id"

    @IBOutlet weak var tableView: UITableView!

    var database: PeopleDatabase!

    func viewDidLoad() {
        super.viewDidLoad()

        tableView.delegate = self
        tableView.dataSource = self
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return database.people.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let person = database.people[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! PersonCell
        cell.nameLabel.text = person.name
        cell.emailLabel.text = person.email
        // ...
        return cell
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 120.0
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let person = database.people[indexPath.row]
        let viewController = PersonViewController(person: person)
        navigationController?.push(viewController, animated: true)
        tableView.deselectRow(at: indexPath, animated: true)
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
        guard editingStyle == .delete else { return }
        // delete entry from database
        // update table
    }
    
    func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
        // update database
    }

}

It's okay when you have a single table, but it's not scalable and will require some code duplications when you have more screens.

Personal note:

For me, I found that I lacked consistency in structuring this approach, sometime in the same project. That means theres a learning process each time I revisited code.

This is what Reuse is for. Having a reusable component system that could be applied to instance and can be shared across the app.

Let's modify the project to Reuse.

First, we implement the database protocols:

extension PeopleDatabase: Section, DataProvider {
    
    // MARK: DataProvider protocol

    var objects: [Usable] {
        get { return people }
        set { people = newValue }
    }
    
    func canDeleteObject(at index: ObjectIndex) -> Bool {
        return true
    }
    
    func canMoveObject(at index: ObjectIndex) -> Bool {
        return true
    }
}

By making our database both a DataProvider and a Section, we basically say it only has one section.

Then we create a reusable instance to configure our cells.

struct PersonReuser: InstanceReuser {
    
    var viewIdentifier: String { return "person.cell.id" }
    var height: CGFloat { return 120.0 }
    
    private var person: Person?
    private weak var navigationController: UINavigationController?
    
    // We inject a navigation controller to handle our navigation.
    // I would prefer injecting some `Navigator` protocol, which will allow easier testing and abstraction, but for simplicity we'll just use this.
    init(navigationController: UINavigationController?) {
        self.navigationController = navigationController
    }
    
    // MARK: InstanceReuser protocol

    mutating func setObject(_ object: Usable) {
        person = object as? Person
    }
 
    func configure(_ reusable: Reusable) -> Bool {
        guard let person = person, let cell = reusable as? PersonCell else { return false }        
        cell.nameLabel?.text = person.name
        cell.emailLabel?.text = person.email
        return true
    }
    
    func select() {
        guard let person = person else { return }
        let viewController = PersonViewController(person: person)
        navigationController?.push(viewController, animated: true)
    }
}

Now all that's left is to refactor our view controller to create a Reuser and let it handle everything for us.

class PeopleViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    var database: PeopleDatabase!
    private var reuser: Reuser!

    func viewDidLoad() {
        super.viewDidLoad()
        setupReuser()
        tableView.passHandling(to: reuser)
    }

    private func setupReuser() {
        reuser = Reuser(dataProvider: database)
        let instanceReuser = PersonReuser(navigationController: navigationController)
        reuser.register(instanceReuser, forObject: Person.self)
    }
}

And you're done. Reuse will handle everything else. You can add logic to the PersonReuser or you could avoid writing it at all and just use the generic provided one. If you need more control, you could implement the UITableViewDataSource and UITableViewDelegate functions and just access your reuser using a subscript. For example:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    reuser[indexPath].select()
    tableView.deselectRow(at: indexPath, animated: true)
}

Now, if you need it elsewhere, just Reuse it. No code duplications. It easy to maintain, update or replace.

There's more you could do with it, but I'll let you explore.

Hope it will help you the same as it helps me.

Installation

Reuse is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Reuse'

Author

Oren Farhan

License

Reuse is available under the MIT license. See the LICENSE file for more info.