00:06 Today we'll take a look at the architecture of our Ledger
GUI Mac app. More specifically, we'll look
at two things: how we get the data to the view controllers, and how we hook up
the balance view controller on the right with the register view controller on
the left. When you select an account on the left-hand side, the transactions on
the right should be filtered so that you only see the ones that contain the
selected account:

00:58 First, we'll look at how we get the data to the view controllers.
There are two basic ways of doing this: either the view controller retrieves the
data from somewhere, or we set the data on the view controller from the outside.
In our example, we'll use the latter approach, since we want the view
controllers to know as little as possible about their context.
Setting Data on the View Controllers
01:18 We start with a skeleton of the app that doesn't yet display any
data:

01:37 The location where we're going to set the data on the view
controllers and later establish the communication between the two view
controllers is the LedgerDocumentController. This class is very similar to the
App class we showed in the episode about Connecting View
Controllers
on iOS, in that it has the responsibility of hooking everything up. In our Mac
app, an instance of this class gets created for us, so we'll just deal with its
implementation. To start with, the class looks like this:
final class LedgerDocumentController {
var ledger: Ledger = Ledger()
var windowController: LedgerWindowController?
}
02:08 Both properties, ledger and windowController, get set from the
outside. ledger is a simple struct that contains all the data of our ledger
file, i.e. all the transactions, all the accounts, and a few other things. The
window controller is specific to the Mac; you can think of a root view
controller as an iOS equivalent. Once we have a window controller, we can use it
to get a reference to the view controllers in order to configure them. We use a
property observer for that:
final class LedgerDocumentController {
var windowController: LedgerWindowController? {
didSet {
windowController?.balanceViewController?.balanceTree = ledger.balanceTree
windowController?.registerViewController?.transactions = ledger.evaluatedTransactions
}
}
}
03:58 Both view controllers are already implemented in a way that
they'll display the data we set on the balanceTree and transactions
properties, so at this point, we already see our data in the UI. However,
selecting an account on the left-hand side still doesn't do anything.
04:24 Before we make this work, we still have to deal with another
problem in our code. Currently, we're lucky that our code works as expected,
because the ledger property gets set before the windowController property
gets set. But this hidden API contract can easily bite us later. Additionally,
the data on screen wouldn't update if we would re-read the file from disk and
set the ledger property again. So we'll add a didSet on ledger as well and
pull out the configuration of the view controllers into an update method:
final class LedgerDocumentController {
var ledger: Ledger {
didSet {
update()
}
}
var windowController: LedgerWindowController? {
didSet {
update()
}
}
func update() {
windowController?.balanceViewController?.balanceTree = ledger.balanceTree
windowController?.registerViewController?.transactions = ledger.evaluatedTransactions
}
}
05:39 Another subtle detail we should take care of: before we set new
data on the view controllers, we have to be sure to be on the main queue. Since
reading the file might happen on a background queue, and the ledger property
might be set on a background queue as a result, we dispatch the call to update
onto the main queue:
final class LedgerDocumentController {
var ledger: Ledger {
didSet {
DispatchQueue.main.async {
self.update()
}
}
}
}
Filtering Transactions by Account
06:27 Now we can make the selection in the view controller on the left
work. For this, we use an approach similar to the one we used in the Connecting
View
Controllers
episode: we define a callback property — didSelect — on the left view
controller, which gets called when the user selects or deselects a row. The type
of this property is an optional function with a single (optional) string
argument:
class BalanceViewController: NSViewController {
var didSelect: ((String?) -> ())? {
}
}
07:45 Since we've factored out the data source and delegate methods of
the table view into a separate class, we have to forward this callback to the
data source/delegate object. To do this, we use a didSet on the didSelect
property and set the didSelect callback of our dataSourceAndDelegate object:
class BalanceViewController: NSViewController {
didSet {
dataSourceAndDelegate.didSelect = { node in
}
}
}
08:20 didSelect on the delegate object has an account tree node as
parameter, whereas the didSelect callback we defined on
BalanceViewController just takes an optional string parameter. So we extract
the account name from the tree node and pass it on to the view controller's
callback:
class BalanceViewController: NSViewController {
didSet {
dataSourceAndDelegate.didSelect = { node in
self.didSelect?(node?.accountName)
}
}
}
08:50 Now we can configure the BalanceViewController's didSelect
callback in the LedgerDocumentController. Once we have a window controller, we
add the callback and just print out the name of the selected account to check if
it works:
var windowController: LedgerWindowController? {
didSet {
windowController?.balanceViewController?.didSelect = { account in
print(account)
}
update()
}
}
10:14 Now that the callback on selection works, we can replace the print
statement with the actual implementation that causes the view controller on the
right to filter the transactions according to the selected account. One approach
we could take is to forward the account selection to the
RegisterViewController on the right — for example, by setting an
accountFilter property on the view controller:
var windowController: LedgerWindowController? {
didSet {
windowController?.balanceViewController?.didSelect = { account in
self.windowController?.registerViewController?.accountFilter = account
}
update()
}
}
11:05 However, this would complicate the RegisterViewController quite
a bit. We'd have to keep the original contents of its transactions property
around to handle deselection and add a new property that contains only the
transactions that should be displayed according to the accountFilter property.
11:28 Instead, we'll handle the filtering of the transactions in the
LedgerDocumentController so that the RegisterViewController doesn't have to
know anything about filtering itself.
11:46 We add a property, accountFilter, to LedgerDocumentController,
which stores the currently selected account:
var accountFilter: String?
var windowController: LedgerWindowController? {
didSet {
windowController?.balanceViewController?.didSelect = { account in
self.accountFilter = account
}
update()
}
}
12:02 Then we add a property observer on accountFilter, which calls
update:
var accountFilter: String? {
didSet {
update()
}
}
12:07 Lastly, we change the update method to set the filtered
transactions on the RegisterViewController. In the filter function, we use an
existing method on transactions, matches, which checks whether an account
appears in one of the transaction's postings:
func update() {
windowController?.balanceViewController?.balanceTree = ledger.balanceTree
windowController?.registerViewController?.transactions = ledger.evaluatedTransactions.filter {
$0.matches(accountFilter)
}
}
Using a Struct to Encapsulate Application State
12:56 In the next step, let's improve the design of our
LedgerDocumentController class. Our current approach requires implementing a
didSet with a call to update on any property that would affect the UI.
Currently, this only pertains to the ledger and the accountFilter
properties, but in a more mature app, this would get very complicated.
13:20 We'll try to pull out all those properties into a separate struct,
LedgerDocumentState:
struct LedgerDocumentState {
var ledger: Ledger = Ledger()
var accountFilter: String?
}
14:06 Now we have to add a state property to the
LedgerDocumentController and make the ledger property a simple proxy for the
ledger property on the state struct:
final class LedgerDocumentController {
var state = LedgerDocumentState() {
didSet {
update()
}
}
var ledger: Ledger {
get { return state.ledger }
set { state.ledger = newValue }
}
}
15:07 After we've changed our code to make use of the new state
property, the complete LedgerDocumentController looks like this:
final class LedgerDocumentController {
var state = LedgerDocumentState() {
didSet {
update()
}
}
var ledger: Ledger {
get { return state.ledger }
set { state.ledger = newValue }
}
var windowController: LedgerWindowController? {
didSet {
windowController?.balanceViewController?.didSelect = { account in
self.state.accountFilter = account
}
update()
}
}
func update() {
windowController?.balanceViewController?.balanceTree = ledger.balanceTree
windowController?.registerViewController?.transactions = ledger.evaluatedTransactions.filter {
$0.matches(state.accountFilter)
}
}
}
15:45 Since we've defined the LedgerDocumentState as a struct, the
didSet on the state property gets triggered each time anything in the state
changes. That's a key feature of structs, compared to classes: whenever you
change something in a struct, it results in a new value.
16:10 Now that we have this LedgerDocumentState struct, there's one
more refactoring we'll do — we'll add a filteredTransactions property on the
state struct to pull out the filtering we currently do in the update method:
struct LedgerDocumentState {
var filteredTransactions: [EvaluatedTransaction] {
return ledger.evaluatedTransactions.filter { transaction in transaction.matches(accountFilter) }
}
}
final class LedgerDocumentController {
func update() {
windowController?.balanceViewController?.balanceTree = ledger.balanceTree
windowController?.registerViewController?.transactions = state.filteredTransactions
}
}
17:28 It's nice to have things like filteredTransactions encapsulated
in a struct, because the struct is much easier to test in isolation compared to
the LedgerDocumentController.
17:53 In a future episode, we'll look at how to implement the search
field. Hopefully this will integrate well with our existing pattern and we won't
even need to touch our view controllers; we should be able to just add the
search logic to the LedgerDocumentState.