4

Background:

I designed a TableViewDataSource class that provides an implementation for UITableViewDataSource and UITableViewDelegate. You instantiate TableViewSection objects, which are passed to the TableViewDataSource which are used to configure cells, section headers, handle selection, row insertion, etc.

The TableViewSection object has a property called dataSource: [AnyObject]?, which, when set, is used to calculate the number of rows in the section, and provide an object for the cell configuration block:

// get the section, dequeue a cell for that section, retrieve the item from the dataSource
// ...
tableSection.cellConfigurationBlock?(cell: AnyObject, item: AnyObject?, indexPath: NSIndexPath)
return cell

What I'd like to do is assign a reference to an array from my viewModel to my tableSection.dataSource, having my viewModel update the array, in turn updating the table view. In Swift, you cannot pass an array by reference. The workaround seems to be to use an NSMutableArray, but with that comes a loss of type safety, and greater cognitive load while translating objects back and forth from Swift to Foundation.

Working Example:

let kCellIdentifier = "SomeCellIdentifier"
class MyViewController: UITableViewController {
    // Property declarations
    @IBOutlet var tableDataSource: TableViewDataSource!

    var viewModel: MyViewControllerViewModel = MyViewControllerViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.setupTableView()
        self.refresh()
    }

    func setupTableView() {
        var tableSection = TableViewSection(cellIdentifier: kCellIdentifier)
        tableSection.dataSource = self.viewModel.collection
        // tableSection configuration
        // ...

        self.tableDataSource.addSection(tableSection)
    }

    func refresh() {
        self.viewModel
            .refresh()
            .subscribeNext({ result in
                self.tableView.reloadData()
            }, error: { error in
                self.logger.error(error.localizedDescription)
            })
    }
}

The refresh() method on the viewModel hits my API service, updates it's collection property on response, and provides the result on the next event of an RACSignal (RACSignal is a class provided by Reactive Cocoa and really, besides the point).

I've found one workaround, which involves reassigning the data source each time a single update is made, or after a batch update.

func refresh() {
    self.viewModel
        .refresh()
        .subscribeNext({ result in
            self.updateDataSource()
            self.tableView.reloadData()
        }, error: { error in
            self.logger.error(error.localizedDescription)
        })
}

func updateDataSource() {
    self.tableDataSource.tableSectionForIndex(0)?.dataSource = viewModel.collection
}

This approach works, but only temporarily as a workaround. As a TableViewDataSource grows and becomes more complex, this method becomes increasingly more complex with imperative, procedural code, the opposite of what I set out to achieve when writing the class.

Question

Is there any workaround to stick to native Swift Array's to achieve the equivalent of passing a Foundation NSArray or NSMutableArray by reference?

Bonus Question

Can someone provide me with some class/struct design tips to accomplish the desired goal in pure Swift?

HighFlyingFantasy
  • 3,789
  • 2
  • 26
  • 38
  • 1
    possible duplicate of [Does Swift have something like "ref" keyword that forces parameter to be passed by reference?](http://stackoverflow.com/questions/24004062/does-swift-have-something-like-ref-keyword-that-forces-parameter-to-be-passed) – Antonio Aug 27 '14 at 17:15
  • I added a working example and a few more details. Unless I'm missing something, I don't believe an `inout` parameter is my solution. – HighFlyingFantasy Aug 27 '14 at 18:10
  • 1
    Well... the title may sound misleading, and you say *In Swift, you cannot pass an array by reference*, which, if referred to passing to a function, is incorrect because it can be done using `inout`. If you mean something different, it's better if you update at least the title. – Antonio Aug 27 '14 at 18:20
  • Agreed. Appreciate expressing concerns "vocally" rather than just downvoting. – HighFlyingFantasy Aug 27 '14 at 18:28
  • The new title makes much more sense, and now it's clear what you want to achieve :) – Antonio Aug 27 '14 at 19:10

1 Answers1

5

The simple solution is to wrap the array in a class. The class instance is passed by reference so the problem is effectively solved: a change to the array through any reference to the class instance affects the array as seen through every reference to that class instance.

The class in question can be extremely lightweight - basically, it just serves as a thin wrapper that carries the array along with it, and a client accesses the array directly through the class instance - or, just the opposite, you can design the class to manage the array, i.e. the class deliberately presents an array-like API that shields clients from the underlying implementation. Either approach might be appropriate; I've certainly done both.

Here's an example of the first kind of situation. My model object is an array belonging to a UIDocument subclass. My view controller is a UITableViewController. The user is going to view, add, and edit model entities in the table. Thus, the UITableViewController needs access to the UIDocument's array (which happens to be called people).

  • In Objective-C, my UITableViewController simply held a reference to the array, self.people, which was an NSMutableArray. This was just a pointer, so changes to self.people were also changes to the UIDocument's people - they are one and the same object.

  • In Swift, my UITableViewController holds a reference to the UIDocument object, self.doc. The array, which is now a Swift array, is "inside" it, so I can refer to it as self.doc.people. However, that's too much rewriting! Instead, I've created a calculated variable property self.people which acts as a gateway to self.doc.people:

    var doc : PeopleDocument!
    var people : [Person] { // front end for the document's model object
        get {
            return self.doc.people
        }
        set (val) {
            self.doc.people = val
        }
    }
    

    Hey presto, problem solved. Whenever I say something like self.people.append(newPerson), I'm passed right through to the UIDocument's model object people and I'm actually appending to that. The code thus looks and works just like it did in Objective-C, with no fuss at all.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I've also posted this on /r/swift and got a similar recommendation. I'm somewhat worried about the complexities and confusion that can arise from passing the class around, and may change the structure of my controller, either moving the individual `tableSection`'s to the `viewModel` or the whole data source. – HighFlyingFantasy Aug 27 '14 at 18:27
  • "the complexities and confusion that can arise from passing the class around" Oh, flapdoodle. What on earth "complexities and confusion" would these be, that would not arise from passing the array by reference? – matt Aug 27 '14 at 18:31
  • I'm just sitting here and refactoring my project in my head. It's just the impending code smell that I see coming out of the project. Your solution completely answers my question in a simple and effective way, and any sort of design issues that I may run into are on me. Thank you for the help. – HighFlyingFantasy Aug 27 '14 at 18:36
  • 2
    @HighFlyingFantasy I've added some code to show you how simple this really is. I truly believe you're over-thinking this. – matt Aug 28 '14 at 18:48