How I Wrote Vinylogue for iOS with ReactiveCocoa
Vinylogue is a simple app I designed and developed for iOS that shows your last.fm weekly album charts from previous years. This post will detail the process of creating V1.0 of the app from start to almost finish (is an app ever really finished?).
The full source is available on GitHub.
Warning: this post is super long (10000+ words). I spend a lot of time discussing ReactiveCocoa techniques, a little bit of time on pre-production, and a little bit of time on design.
Contents
Idea
I recently came across an awesome app called TimeHop that compiles your past posts from Facebook, Twitter, etc. and shows you what you posted on that day one year ago. I love reminiscing through my old things, so this concept was right up my alley.
I also love music too though, and have been an avid Last.fm user since 2006 or 2007, scrobbling tracks religiously. I thus have quite a history of my past album listens in their database, and with those listens, a lot of memories connected to those albums. I can remember exactly what I was doing, where I was, and mental snapshots when I see an album or group of albums together.
And so the idea of combining Last.fm and TimeHop was born.
Planning
Getting a feel for the data
The first step was seeing what data was available from the Last.fm API. I could indeed get a weekly album chart (a list of albums, ranked by number of plays) for a specified user and week with user.getWeeklyAlbumChart. It’s also possible to get the same chart grouped by artist or track, but at the current time it seemed like albums would appeal to me most.
One of the great things about this particular API call is that you don’t need a password for the username you’re requesting. One of the bad things was that you can’t just send a particular timestamp and let Last.fm figure out what week that date falls into.
The latter problem is solved by making another API request to user.getWeeklyChartList. This call provides the specific weekly date ranges in Epoch timestamps you can then use as an input to the user.getWeeklyAlbumChart call. The data looks something like this:
user.getWeeklyChartList
Two API calls so far to get most of our data. Not bad. So to document our information flow:
- take the current date
- subtract
n
years (1 year ago to start) - figure out where that date is within the bounds of the Last.fm weeks
- request the charts for the username and date range
- display the data
Pretty simple. Time to dig in a little more.
Features
It’s best to draw a line in the sand about what the constraints of the app will be, at least in the first version. Below are my first set of constraints. They (inevitably) changed later in the project, and we’ll discuss those changes as we make them.
- We’ll support only one last.fm user.
- The user can view charts from the week range exactly
n
years ago (with the lower bound provided by Last.fm). - The user cannot view any charts newer than one year ago.
- The app has a chart view and a settings view (keeping it very simple).
Again, I’ll discuss changes as they happened in the process.
Schema
Next I had to decide how the data would be structured and stored. The initial options were:
Keep everything transient
We should request everything from the network each time. Don’t store anything in Core Data. We’re only looking at old data and we don’t have to worry about realtime feeds so we can use the regular old NSURLCache and set our cache times basically forever (in practice, it’s a little more complicated than this).
(As an aside, I ended up using SDURLCache as a replacement for NSURLCache. From pure observation, NSURLCache was faster from memory, but I could not get it pull from disk between app terminations.)
The weekly charts technically only need to be pulled from the server once a year (I’ll leave that as an exercise to the reader as to why). The chart data for a particular week is only relevant once per year the way the app is set up.
At the end of the day, the URL cache ends up being a dumb key-value store keyed on URLs and returning raw json data.
- Pros
- Less complexity keeping data in sync.
- Cons
- More data transferred.
- Longer waiting for previously requested data.
Incrementally build our own database of the data we request
We should set up a full schema of related objects. Data should be requested locally, and if it doesn’t exist, it should be requested from the server and added to the local database.
- Pros
- Speed of subsequent requests.
- Less API requests.
- May enable future features.
- Cons
- Much greater complexity keeping data in sync and knowing when the cache is invalid.
- More local storage required.
Decision
I was much more inclined to build a local database, but a big goal of this project was a quick ship. I decided to take a few hours building something out with AFIncrementalStore. It didn’t take long to realize doing things this way would slow down development substantially. I decided to keep the local database method as a goal, but leave it until the app idea itself was validated with users. At the current time, it felt like premature optimization.
I went ahead with the two table Core Data schema since I already had it set up and my classes generated with Mogenerator. I added fields for the data I wanted from the user.getWeeklyChartList API call.
Later, I would completely rid Core Data from the project and turn these model objects back into standard NSObject subclasses.
Wireframing
Since I now had an idea of what data I had to work with, it was time to put together wireframes of what the main views would look like.
The aim was simplicity. One main view that shows a single week’s charts. Another modal view for settings and about.
I wanted to go with a more classic look with a standard UINavigationBar. A UITableView as a given based on the nature of the data (unknown number of albums, 0 - ???).
I also needed to show additional metadata including what week and year were being shown, and also what user was being shown. Maybe some sort of table header view?
The next decision was how the user should navigate to past and future years. The first thought was a pull-up-to-refresh-type control to go to the future, and a pull-down to go back. iOS users are pretty used to that control type being for refreshing data, and it probably wouldn’t work with the metadata on a table header view already. Not to mention the amount of scrolling it might take to get to the bottom of the table view.
My solution was to combine the metadata header view and selection into one view/control. I like sliding things, so sliding left and right would request future and past years, respectively. While creating the control later, I also decided that classic arrow buttons would be a good idea so the functionality was immediately discoverable.
And finally, there was the table view cell itself. I wanted to keep this classic. Album image on the left, artist above album name, artist reemphasized and album name emphasized, and play count on the accessory side. I also left a placeholder for the numbered rank above the album image, but decided it was unnecessary later in the process.
I wanted to get a feel for the settings view too, so I sketched that out. When I was getting my first prototype results back, I realized that if you listen to a lot of mixes, you’ll have row after row of albums with 1 play. I personally wanted the option to only see “full” album listens, and decided to add an option to filter rows by play count.
Development
Finally time to dig into some coding (for real)! Well, almost time…
Setup
Up to this project, I still was git cloning all my external libraries into a lib folder and doing the target settings dance with static libraries. It seemed like CocoaPods was nearly complete on all the popular and semi-popular open source libraries out there. I decided to give it a shot, and it turned out to be mostly a boon to development. I ran into a few snags when I was doing my first archive builds, but I’m of the persuasion now that every iOS project should budget at least a full day to dealing with Xcode quirks.
ReactiveCocoa
I first learned about ReactiveCocoa (RAC) almost a year ago (Maybe March of 2012?) and was immediately interested and immediately confused. ReactiveCocoa is an Objective-C framework for Functional Reactive Programming. I spent a lot of time trying to understand the example projects and following development for the next few months. On a (yet to be released as of this writing) project before this one, I was comfortable enough with a few design patterns to use RAC in selected places throughout that app. Another one of my goals for this app was to try to use RAC wherever I could (and it made sense).
Luckily, RAC hit 1.0 right before I started the project and a substantial amount of documentation was added. For a good, brief overview of the library, I also recommend the NSHipster writeup.
I’ll be explaining my RAC code in some detail later in this post. I want to preface it by saying that I am still very much an RAC newbie and still getting a feel for its patterns, limitations, quirks, etc. Keep that in mind when reading through the code.
Other Libraries
I tried to keep the dependencies lower than normal on this project. I did, however, use a few staples.
- AFNetworking - the defacto iOS/OS X networking library.
- SDURLCache - didn’t add this until very close to shipping. I wrestled with NSURLCache during several non-adjacent coding sessions, and eventually decided to replace it with SDURLCache.
- ViewUtils - there’s a bunch of UIView categories out there. I do a lot of manual view layout, so this library was invaluable.
- DrawRectBlock - I don’t like cluttering my file list with single-use view classes, so having access to drawRect when creating a new UIView is often helpful. (Especially with different colored top & bottom borders).
- TestFlightSDK - getting feedback from beta testers has improved my apps and workflow a lot. TestFlight is definitely doing some great work for the iOS community.
- Crashlytics & Flurry - I spent an entire day before App Store submission trying to figure out the best way to do analytics and crash reports (and how all the libraries fit together). The jury is still out on this, but I have to note that the people at Crashlytics were super helpful and responsive in getting me set up.
TCSLastFMAPIClient
We’re going to start development from the back end (data) and move towards the front (views).
There are some older Last.fm Objective-C clients on GitHub, but it made sense to write my own since I was only using a few of the API endpoints and I wanted to use RAC as much as possible.
Interface
I followed the GitHub client RAC example as a template for my client. The initial interface looked like this:
(TCSLastFMAPIClient.h)
A few things to notice:
- We’re subclassing AFHTTPClient.
- A client instance is specific to a last.fm user.
- We can request data from two API endpoints as discussed in the planning section.
- The
fetchWeeklyChartList
method only requires a username. - the
fetchWeeklyAlbumChartForChart:
method requires a username and a WeeklyChart object returned by the former method. A WeeklyChart object simply holds an NSDate forfrom
andto
.
I want to focus on the RACSignal
return types. I say in the comments that the methods “return an NSArray…” when what I mean is that they immediately return an RACSignal
. Subscribing to that signal will (usually) result in an NSArray
being sent to the subscriber in the sendNext
method.
It’s probably not immediately clear why I wouldn’t just return the NSArray
, but bear with me.
enqueueRequest
Feel free to read the full implementation, but I’m going to focus on a few methods in particular to explain the RAC parts. I’ll explain this one inline with comments.
(TCSLastFMAPIClient.m)
So that’s our first taste of RAC in action. RACSubjects are a little different than vanilla RACSignals, but again, they’re very useful in bridging standard Foundation to RAC.
fetchWeeklyChartList
We’re now at the point where an endpoint function can specify a URL and HTTP method and get a response object to process. I’m only going to explain one of the API endpoint functions (the simplest one), but the other ones should be very similar.
(TCSLastFMAPIClient.m)
So it might look a little hairy, but it’s essentially just a few processing steps chained together and all in once place.
We introduced the RACSequence
at the end there to iterate through the array. There are more standard NSArray
ways to do this, but RACSequence
s are a subclass of RACStream
s, which have a bunch of cool operators like map
, flatten
, filter
, etc.
The main point to get out of this is that our API endpoint method defines several processing steps for a stream, then hands the stream off to its assumed subscriber. At the point the API endpoint method is called, none of this work will actually be done. It’s not until the subscriber has called subscribeNext
on the returned signal that the network request and subsequent processing be done. The subscriber doesn’t even have to know that the original signal’s next
values are being modified.
That about does it for the API client. To summarize, the data flow is as follows:
- The client object’s owner (we’ll assume it’s a controller) requests a signal from a public API endpoint method like
fetchWeeklyChartList
. - The API endpoint method asks the
enqueue
method for a new signal for a specific network request. - The
enqueue
method creates a new manually controlled signal, sets up the signal so that it will send responses when the network call completes, and then passes the signal to the API endpoint method. - The API endpoint method sets up a list of processing steps that must be done with responses that are known to be good.
- The API endpoint passes the signal back to the controller.
At this point the signal is completely set up. It knows exactly where and how to get its data, and how to process the data once it has been received. But it is lazy and will only do so once the controller subscribes to it.
We haven’t shown the act of subscribing yet, and we’ll do that in the next section.
TCSWeeklyAlbumChartViewController
Our primary view controller is called TCSWeeklyAlbumChartViewController
. Its function is to request the weekly album charts for a particular user and display them in a table view. It also facilitates moving forward and backward in time to view charts from other years.
It was originally imagined that this would be, for lack of a better term, the root controller of the app. In its original implementation, it had many more responsibilities and had to be more mutable. During the coding process, the app architecture changed, allowing me to assume that the controller would have an immutable user, and simplifying things a bit.
Public Interface
The interface for this controller is pretty simple. Just one designated initializer.
(TCSWeeklyAlbumChartViewController.h)
Our controller needs to know which user it should display data for. It also needs to know which albums will be filtered based on play count. This controller will also handle all delegate and datasource duties from within. As a side note, I sometimes separate table datasources and delegates out to be their own classes. For this particular controller, things haven’t gotten so complex that I’ve needed to refactor them out.
Private Interface
It’s good OO practice to keep your class variables private by default, and that’s what we’ll do by defining our private interface in the source file. I’ll go through all the class variables we’ve defined.
Views
Let’s start with the views.
(TCSWeeklyAlbumChartViewController.m)
The slideSelectView is the special view we use to move to past and future years. It sits on above the tableView and does not scroll.
The tableView displays an album and play count in each row.
The emptyView, errorView, and loadingImageView are used to show state to the user. The empty and error views are added and removed as subviews of the main view when necessary. The loadingImageView is a animated spinning record that is added as a custom view of the right bar button item.
Data
(TCSWeeklyAlbumChartViewController.m)
From a general overview, we’re storing all the data we need to display, including some intermediate states. Why keep the intermediate state? We’ll respond to changes in those intermediates and make the chain of events more malleable. Some variables will be observed by views. Some variables will be observed by RAC processing code to produce new values for other variables. As you’ll see in a moment, we can completely separate our view and data code by using intermediate state variables instead of relying on linear processes.
I’ll reprint our data flow from the planning section above adding some detail and variable names. Variables related to the step are in [brackets].
- take the current date [
now
] - subtract
n
years (1 year ago to start) [displayingDate
&displayingYearsAgo
along with a convenience reference to the Gregoriancalendar
] - figure out where that date is within the bounds of the Last.fm weeks [
weeklyCharts
&displayingWeeklyChart
] - request the charts for the username and date range [
rawAlbumChartsForWeek
] - filter the charts based on play count and display the data [
albumChartsForWeek
] - We also need to calculate the date bounds we can show charts for [
earliestScrobbleDate
&latestScrobbleDate
]
We store a lastFMClient instance to call on as our source of external data.
Controller state
(TCSWeeklyAlbumChartViewController.m)
We have some additional controller state variables set up. I have to admit that I’m not sure my implementation of empty/error views is the best. There was plenty of experimentation, and I ran into some trouble with threading. It works, but will eventually require a refactor.
canMoveForward/BackOneYear depend on which year the user is currently viewing as well as the earliest/latestScrobbleDate. The slideSelectView knows what it should allow based on these bools. Any of our data processes can decide they want to show an error, empty, or loading state. Other RAC processes observe these variables and show the appropriate views. (Again, this took some tweaking and is a little fragile.)
loadView
I’ll annotate loadView inline:
(TCSWeeklyAlbumChartViewController.m)
All view attributes are defined in the view getters section. I’ve taken up this habit to keep my controllers a bit more tidy. The views are created at the first self.[viewname]
call in loadView.
(TCSWeeklyAlbumChartViewController.m)
viewDidLoad & RAC setup
Now for the fun stuff. The majority of the controller’s functionality is set up in viewDidLoad within two helper methods, setUpViewSignals and setUpDataSignals.
(TCSWeeklyAlbumChartViewController.m)
I’m going to start with the view signals to stress that as long as we know the exact meaning of our state variables, we can set up our views to react to them without knowing when or where they will be changed.
But before we can read through the code, we’ll need a quick primer on some more bread-and-butter RAC methods.
@weakify & @strongify
Because RAC is heavily block-based, we can use the EXTScope
preprocessor definitions within the companion libextobjc
library to save our sanity when passing variables into blocks. Simply throw a @weakify(my_variable)
before your RAC blocks to avoid the retain cycles, and then @strongify(my_variable)
within each block to ensure the variable is not released during the block’s execution. See the definitions here.
RACAble(…) & RACAbleWithStart(…)
RACAble is simply magic Key Value Observing. From the documentation:
[RACAble] returns a signal which sends a value every time the value at the given path changes, and sends completed if self is deallocated.
This will be the backbone of our RAC code and is probably the easiest place to get started with RAC. It’s easy to pepper these signals into existing code bases without having to rewrite from scratch. Or just use a few in a project to get started.
setUpViewSignals
Let’s dive right into our first view signal to get a feel for it. I’ve separated it out into several expressions in order to simplify the explanation. In the actual source, it’s a single expression.
(TCSWeeklyAlbumChartViewController.m)
Let’s deconstruct this. RACAbleWithStart(self.userName)
creates an RACSignal
. Again, an RACSignal
can send next
(with a value) and error
or complete
messages to its subscribers.
The WithStart
part sends the current value of self.userName
when it subscribed to. Without this, the block in subscribeNext
would not be executed until self.userName
changes for the first time after this subscription is created. In our case, because self.userName
is only set once in the controller’s init
method (before the signal is created), it would never be called with a normal RACAble
.
(At this point you may be wondering, “Why even observe the userName property if it’s guaranteed to never change within the controller?” That’s a good question. It’s partly vestigial from when the value could change. It would very much be possible to transplant the code from within the subscribeNext
block to viewDidLoad
, but as you’ll see it fits pretty well with the rest of the more dynamic view code.)
The next statement, deliverOn:
, modifies transforms the signal into a new RACSignal on the specified thread to ensure next
values are delivered on the main thread. This is a necessary because UIKit requires user interface changes to be done on the main thread. Like all other signal transformations, it only takes effect if the result is used.
subscribeNext
is where our reaction code goes. Basically saying, “When the value of self.userName
changes, execute the following block with the new value.” In this example, we’re going to change a label to show the userName. If it’s nil
, we’ll throw up the error view.
I could have also used another variation of the subscribeNext
method like subscribeNext:complete:
to also add a block to execute when the signal completes. We don’t really need to do anything on completion, so we’ll just add a block for next:
.
Instead of the if/else
, we could have used two separate subscriptions that first filter
for nil
or empty userName. To keep it simple though, we’ll just mix in the iterative style for now.
Alright, so one signal down. Let’s look at another common pattern: combining multiple signals.
(TCSWeeklyAlbumChartViewController.m)
I’ve separated out the expressions again, and I’ve replaced a bunch of tedious date calculation code and view updating code with comments in order to focus on what’s happening with RAC. You can see everything in the source.
The slideSelectView has a couple components that depend on displayingDate
, earliestScrobbleDate
, and latestScrobbleDate
. I want to update this view when any of these values change. Luckily, there’s an RACSignal constructor for this.
[RACSignal combineLatest:]
allows you to combine several signals into one signal. The new signal sends a next:
message any time one of its component signals sends next:
. There are a few variations of combineLatest:
but the one we’ll use in this example will combine all the latest next:
values of our three component signals into a single RACTuple object. If you haven’t heard of a tuple before, you can think of it like an array for now.
combineLatest
takes an array of signals, which we’ll generate on-the-fly with RACAbleWithStart
.
When we subscribe, we expect a single RACTuple
object to be delivered with the latest values of our component signals in the order we placed them in the original array. We can use the RACTuple
helper methods .first
, .second
, etc. to break out the values we need.
Within the block, we calculate some booleans and set some labels. This could be broken up into multiple methods, but in this case, it made the most sense to do these calculations and assignments in the same place because they depend on the same set of signals.
Let’s do one more view-related signal to show how primitive values are handled.
(TCSWeeklyAlbumChartViewController.m)
Here we’re observing the showingLoading
state variable. This variable will presumably be set by the data subscribers when they’re about to do something with the network or process data.
This time I left the signal creation, modification, and subscription all in one call.
RACAble(self.showingLoading)
creates the signal. distinctUntilChanged
modifies the signal to only send a next:
value to subscribers when that value is different from the last one. For example, let’s assume showingLoading
is YES
. If somewhere in our controller, a method sets showingLoading
to NO
, then it is also set to NO
later, the subscribeNext:
block will only be executed on the first change from YES
to NO
.
We’ve seen deliverOn:
a few times now. No surprises there.
Now for the subscription. You can see that the next:
value is delivered as an NSNumber
object even though self.showingLoading
is a primitive BOOL
. RAC will wrap primitives and structs in objects for us. So before we compare against the value, I’ll use [showingLoading boolValue]
to get the primitive back.
You can check out the rest of the view signals and subscriptions in the source.
setUpDataSignals
We’ll introduce a couple new RAC concepts in this method. But first, here’s an ugly ascii variable dependency graph. We’ll use this to set up our signals.
userName now displayingYearsAgo
| \ /
lastFMClient displayingDate
| /
weeklyCharts /
\ /
displayingWeeklyChart
|
rawAlbumChartsForWeek
|
albumChartsForWeek
|
[tableView reloadData]
When any of these variables change, they trigger a change upstream (downstream?) to the variables that depend on them.
For example, if the user changes displayingDate
(moving to a past year), a change will be triggered in displayingWeeklyChart
, which will trigger a change in rawAlbumChartsForWeek
and so on. If our data was more time sensitive (by-the-minute instead of by-the-week), we could set up a signal for now
that would trigger a change down the line every minute. Or if we decided to reuse our controller with different users, a userName
change would trigger a change down the line.
We’ll start with the userName observer.
(TCSWeeklyAlbumChartViewController.m)
We’ve seen the RACAbleWithStart
pattern. We’re going to use filter:
to act only on non-nil values of userName
. Filter takes a block with an argument of the same type as is sent in next:
. In this case, we don’t need to cast it directly, we just know it shouldn’t be nil. filter
’s block returns a BOOL
that indicates whether it should pass the next
value to subscribers. Returning YES
passes the value. Returning NO
blocks it and the subscribeNext
block will never see it. filter
is a operation defined by RACStream
like map
which we saw earlier.
Next is another RAC pattern. You can automatically assign a property to the latest next
value sent by a signal by using RAC(my_property) = my_signal
. There are actually a couple other ways to accomplish this too. Here’s an example from Vinylogue.
(TCSWeeklyAlbumChartViewController.m)
This one is a little tricky so let’s step through it. First thing we’re doing is setting up the RAC(property)
assignment. self.displayingDate
will be assigned to whatever next
value comes out of our complicated signal on the other side of the equals sign.
We’re creating a signal that combines our self.now
and self.displayingYearsAgo
signalified properties. Remember, they’re not just regular properties. We’ve made them into signals by wrapping them with RACAble
, and they send their values each time they’re changed.
We’re injecting a deliverOn
with the default background scheduler [RACScheduler scheduler]
before doing any work on the next values to make sure the work will be done off the main thread (ensuring a snappy UI).
Edit: Doing this on a background thread is probably overkill and most likely not the best idea since we’re now changing controller properties off the main thread.
Remember that combineLatest
creates an RACTuple
with the next
values of each signal it wraps. We break that tuple out into an NSDate
and an NSNumber
and use those to calculate a date in the past. Remember that map
just modifies next
values. It has to return a value to pass to subscribers. It doesn’t have to be the same type; our input is an RACTuple
and our output is an NSDate
in this case.
Our last operation is a filter for nil. It’s useless to assign nil to our displayingDate. If this returns YES
, then our next
date returned from map
will automatically be assigned to self.displayingDate
.
We’ll do one expression in order to show how we use the Last.fm client functions we wrote earlier.
(TCSWeeklyAlbumChartViewController.m)
Once we have a specific time period to request charts for, we’ll get a signal from the client by supplying the displayingWeeklyChart. We have a signal within a signal in this block. As soon as we subscribeNext
to the signal that was returned from the client, it will request data from the network and do the processing.
We also subscribed with an error block this time so we can pass the error along to the user. By setting showingError
and showingErrorMessage
, the view signal subscriptions we created earlier are triggered. Remember that in this subscription, we’re still on a background thread. Changing these properties on a background thread will still trigger the view updates on the main thread. Pretty cool.
The rest of our setUpDataSignals
method uses similar tricks with RAC, observing properties as outlined in our simple ascii chart. Check out the source to decipher the rest.
You should be at the point now where you can start picking through the signal operations in RACSignal+Operations.h. RACStream.h also has some operations you should be aware of.
In the next section we’ll also briefly cover RACCommand
s.
TCSSlideSelectView
The slideSelectView is the extra special control we dreamed up earlier in the wireframing section.
We should break it out so we know how we should code the view hierarchy.
Let’s implement it!
Interface
From our sketch, we can deconstruct the views we need.
(TCSSlideSelectView.h)
We’ll decide up front that this view will be somewhere between a generic and concrete view. A good future exercise would be figuring out how to make this view generic enough to be used by other controllers and apps. We’ve made it sort of generic by exposing all the subviews as readonly, therefore allowing other objects to change view colors and other properties, but not allowing them to replace views with their own.
We’ll actually redefine these view properties in the private interface in the implementation file.
Although our view will have defaults for the pull offsets and commands, we’ll allow outside classes to change or replace them at will.
The pull offsets are values that answer the question, “How far do I have to pull the scroll view to the right or left before an action is triggered?” The commands are RACSignal
subclasses that are designed to send next
values triggered off of an action, usually from the UI. We’ll use these constructs instead of the delegate pattern to pass messages from our custom view to the controller that owns it.
Implementation
Here’s the private interface:
(TCSSlideSelectView.m)
We’ll define the view hierarchy and create defaults in init
.
(TCSSlideSelectView.m)
Logically, the buttons would be behind the scrollView, but I was having trouble getting taps to get forwarded from the invisible scrollview to the button below it (maybe I should have just made the scrollView narrower?). Instead, the buttons sit above the scrollview and disappear when scrolling begins.
We also define defaults for the pull offsets and set up generic RACCommand
s.
I use layoutSubviews
to resize the views and lay them out. This is mostly self explanatory and tedious to explain. Feel free to read the source to see how I do that.
We’ll move on to the more interesting part: using RACCommand
s to pass messages.
(TCSSlideSelectView.m)
Like I just mentioned before, we’ll hide the buttons when scrolling starts and show them again when scrolling ends.
When scrolling ends, we check the x offset of the scrollview. If it’s past the offsets that were set earlier, we use the execute
method of the RACCommand
. An RACCommand
is just a subclass of an RACSignal
with a few behavior modifications. execute:
sends its argument in a next
message to subscribers. In our example, we don’t need to send any particular object, just the message is enough. We could have alternatively only had one command object and sent the direction as the message object. That’s a little confusing though.
This design pattern works for a few reasons. If the command has no subscribers, the message will be ignored, no harm done. So far though, it isn’t really that much different than creating a protocol and holding a reference to a delegate. I’ll show you the interesting part in a second.
Before we move back to the controller to show how we handle these messages, here’s how we handle the buttons:
(TCSSlideSelectView.m)
The buttons trigger the same action as the scrollView.
Another way to interface UIControl
s with RAC is to use the RACSignalSupport
category:
(UIControl+RACSignalSupport.h)
Let’s quickly jump back to the TCSWeeklyAlbumChartViewController
to show how we interface with this custom control.
(TCSWeeklyAlbumChartViewController.m)
We’re creating signals (commands) and assigning them to the slideSelectView. The slideSelectView will own these signals, but before the controller hands them over, it will add a special “canExecuteSignal” and then subscription instructions for each.
The command will check the latest value of its canExecute signal (which should be a BOOL
) to decide whether it should fire before executing its next
block. In the example, we don’t want to let the user move to the past unless there are weeks to show there. We create a signal from our BOOL
property canMoveBackOneYear
and assign it to the command. Our canMove
properties will now govern the actions of the slideSelectView for us.
When these commands are allowed to fire, they’ll update our displayingYearsAgo
property, and the changes will propagate through our ascii dependency chart.
That’s about it for the slideSelectView. Now that we have the skeleton of our app created, time to start thinking about the design.
Design
Alright, so we’ve now done a fair amount of development. There’s at least enough for a prototype using built in controls. Now it’s time to get a feel for how everything is going to look.
This is approximately what our app looks like at this point:
I am not a designer. I usually work with my friend CJ from Oltman Design, but since this was a low-stakes, unpaid project with a tight-timeline, I decided to give it a shot myself.
Many shops will complete the wireframes, UX, and then do the Photoshop mockups before even involving the developers. Since I’m doing everything, I sometimes pivot back and forth between the two, using new ideas while doing each to iterate in both directions.
You can also see that from my ugly prototype screen shot that I like to multicolor my views initially to make sure everything is being laid out as expected.
Photoshop
I took my ugly mockup and threw it into photoshop, then styled on top of it. I wanted to start with a more colorful mockup and used this color picker to pick out a bunch of colors I liked (again, not a designer!).
Here is my first mock up:
I chose iOS6’s new Avenir Next font in a few weights because it’s pretty, a little more unique, but still very readable and not too opinionated.
As a non-designer, I simply asked myself what the relative order of importance for each element was, and adjusted its relative color and size accordingly. The artist name is not as important as the album name, therefore the artist name is smaller and has less contrast. The album cover is important, so it is nice and big and the first thing you see on the left side (also a standard iOS design pattern). I added padding to the album images so they wouldn’t bump up against each other on the top and bottom, which sometimes looks weird. The number of plays is big and stands out, while the word “plays” can be inferred after seeing it the first time, and can therefore be downplayed heavily. I gave the cells a classic one-line top highlight and bottom shadow.
For my slide control, I wanted to give it depth to imply it being stacked. A darker color and inner shadow accomplished this (although I’ve found Photoshop-style inner shadows much harder to implement with Core Animation and Core Graphics). The user name is mostly understood and is only there as a reminder, so it can be downplayed. The week is important so it should stand out.
I was originally planning on this being the only controller, so I put the logo top and center. The logo is just our normal Avenir Next font with a little extra tracking (not a designer). It looks just hipstery enough I think.
Code the cell design
Now that there’s a map for our cell layout, let’s implement it.
Interface
(TCSAlbumArtistPlayCountCell.h)
We’ll keep a reference to the model object we’re displaying. There’s some debate about the best way to implement the model/view relationship. Sometimes I borrow the concept of a “decorator” object from Rails that owns the model object and transforms its values into something displayable. I decided to keep this app simple this time and have the cell assign its own view attributes directly from the model object. If we didn’t have a one-to-one relationship between views and model objects, I would definitely reconsider this.
Ignore refreshImage
for now. That tackles a problem we’ll run into later.
heightForObject
is our class object that does a height calculation for an object before an instance of the cell is actually allocated. Part of the UITableView protocols is knowing the height of a cell before it is actually laid out. I have yet to figure out a good way to do this without doubling up on some layout calculation code, but alas I’ve gotten used to writing it.
Implementation
Our private instance variables:
(TCSAlbumArtistPlayCountCell.m)
I’m reusing UITableViewCell
’s textLabel
and detailTextLabel
for my artist and album labels, and reusing the imageView
for the album image. Our cells aren’t currently selectable, so the cell only has a normal background view and not a selected one.
I’ll come back to that imageURLCache
string in a moment.
(TCSAlbumArtistPlayCountCell.m)
I create and/or configure my views in custom getters at the bottom of my implementation file. Nothing too interesting here. Moving on…
(TCSAlbumArtistPlayCountCell.m)
Our custom object setter is in charge of properly assigning model object data to our views. The interesting problem we come across is the albumImage. Let’s look at the refreshImage
instance method:
(TCSAlbumArtistPlayCountCell.m)
What do we know about the image at this point? When our WeeklyAlbumChart object is originally created, it does not have an album image URLl The Last.fm API does not return that data with the call we’re using. If we want that image URL, we have to request it using a separate album.getInfo
API call. And it may not even exist for a particular album.
But getting that URL isn’t the cell’s responsibility. We don’t want to create or pass in a copy of the lastFMAPIClient to each cell. That seems like the controller/datasource’s responsibility.
Why don’t we just request all these image URLs when we originally receive the album chart list from the API client? We could, but if that list has 50+ albums in it, that’s 51+ API calls we just made. And if the user only scrolls through a couple, it’s a lot of wasted data. We should strive to do it more lazily. Only request the album image URL if the cell is actually being displayed. Luckily, we have a nice table view delegate method for that.
(TCSWeeklyAlbumChartViewController.m)
In the controller, if the model object does not have an albumImageURL we request it using a new API client method (that returns an RACSignal
of course). We subscribe, so when it’s done we can refresh the cell and load the new image using AFNetworking
’s UIImageView
category that loads images asynchronously from a URL.
While we’re waiting for our response, the user could possibly have scrolled past the cell, and the cell could be reassigned a new object. No problems though, because refreshImage
will just fall through without doing anything and the URL will be saved and loaded the next time the object is assigned to the cell.
The last thing we should do is clear out our imageURLCache
in prepareForReuse
, although technically everything will still work without doing so.
All of this work seems like exactly the thing that RAC would excel at. Unfortunately, I wrestled with several such implementations but could not get things to work out as I wanted. On my to-do list is to try again and see if something unrelated was the problem.
I was considering digging into the view layout calculations, but I think as was with the slideSelectView, explaining the layout code would be tedious and superfluous. Check it out in the source if you’re interested.
Photoshop part 2
We coded up our cell layout and did the same for the slideSelectView and took a step back to see how it looks on the iPhone.
Everything seems to be in the right place. Let’s go ahead and adjust fonts and colors, then tweak the layout to get things as close to our mock up as possible.
Cool. Looking pretty close to what we had in photoshop. We haven’t gotten the inner shadows of the slideSelectView yet though. And the sharp rectangle of the slideSelectView’s top view doesn’t look particularly draggable. Let’s round off the corners. We’ll also add a button for opening up the settings (user name and play count, along with about info).
Settings
Next we need to create a settings controller and a controller for setting the user name.
I’m not going to go in depth for this one. I used a generic static table view datasource class I threw together for another project. You basically give it a specially coded dictionary with titles and optional selectors, then tell it what cell classes to use for headers and cells.
I didn’t do this one in photoshop first. Since it’s only one line of text per line, I simply coded it up and experimented a little with the fonts and colors I already had chosen.
I kept this theme going with the username input controller. I heavily borrowed the styling of the awesome and beautiful app EyeDrop.me.
Iteration
We now have a functional app! But now we can take a step back and really look at what’s going on.
- The color scheme of the root controller is too much. The colors of the album art should draw the most focus not the background of the table cells nor the play count.
- The flow of the settings page kind of works, but it feels a little odd.
- My original thought was that this would be a one-user app, but during testing, I realized that:
- We don’t need to input a user’s password to view their charts.
- During testing I was viewing my friends’ charts and enjoying perusing them.
- Why not just having a landing page controller where we can easily select charts for different users!?
It seems obvious in hindsight, but at the time it was anything but.
So let’s address each of these points.
- I’m kind of liking the settings page. It was sort of an accident, but it’s minimalist, which is easier to pull off for someone with no sense of design. It’s also the way design trends have been going lately. Let’s aim for that on the root controller.
- We can probably address this along with #3.
- The root controller should be a list of users.
- Selecting a user pushes their chart on the nav controller.
- Settings can be viewed and changed from the root controller.
Awesome! A little more complicated, but overall a much more functional app.
Photoshop part 3
Luckily all the layout is still winning our favor. All we have to do is tweak fonts and colors.
That looks a lot cleaner. The slideSelectView looks a little weird, but we’ll play around with those colors on the device/simulator.
Design implementation part 2
Yeah, much better.
Let’s check out the new users controller.
Looks good. UIKit automatically adds those table header view bottom borders and I can’t reliably get rid of them. Weird.
Because we’ve moved the primary user name out of settings, here’s the settings controller one more time.
Back to development
Alright, we totally skipped the implementation of the users controller. Let’s touch on that and see if RAC has anything to do with it.
Data store
We have a new decision to make: how will we set/store users? We have a few options:
- The user adds their friends’ user names manually.
- Pros: simplest, may be what most users want.
- Cons: may be difficult for users to find/enter their friends’ usernames.
- Automatically sync friends with last.fm.
- Pros: users also might find this easiest.
- Cons: may be difficult to keep the user’s personal ordering.
- Manually sync friends with last.fm on user request.
- Pros: probably a good compromise as a “first run” option or default.
- Cons: users may get upset if they sync again and everything is overwritten.
We should probably start with option 1. Later we can add option 3 if the interest is there. Keep an ear open for option 2.
(Update: V1.1 has friend list importing, which solves these problems by only importing friends not already in the list).
The easiest way to implement option 1 is NSUserDefaults. We should probably wrap it in its own datastore class in order to abstract out the implementation details and make option 3 easier to add in the future.
Our new app start up sequence is the following pseudo code in our AppDelegate:
- Pull the play count filter setting from NSUserDefaults.
- Create a user data store class instance that will pull user data from NSUserDefaults.
- Create a UsersController and pass in our userStore and playCountFilter.
Why not have each controller interact with NSUserDefaults as needed? I consider NSUserDefaults a store for global variables. Doing all global IO in one place (the AppDelegate) is better separation of concerns.
At the end of the day it’s a judgement call, and with an app this size there’s only so much anti-pattern spagettification you can even create. With larger apps, it’s a better idea to keep your controllers as functional (in the functional programming sense) as possible.
Moving data around
TCSFavoriteUsersController
has a by-the-book table view delegate and datasource implementation. It includes adding, removing, and rearranging elements in one section only.
An instance of TCSFavoriteUsersController
is a spring board for 3 other controllers.
TCSSettingsViewController
- users can change the value of playCountFilter.TCSUserNameViewController
- used to add a new friend or edit an existing friend.TCSWeeklyAlbumChartViewController
- used to display charts.
We need to keep a connection to the Settings & UserName controllers we create and display because we need to know when they change values. Normally this is accomplished through the delegate pattern. Our parent controller will get a message with the new data and dismiss its child. We’re going to try a different pattern using RAC.
This example is for editing the userName of a friend by tapping the cell during edit mode (this used to be done by tapping the accessory button, but changed before release).
(TCSFavoriteUsersViewController.m)
The child controller has a signal instead of a protocol method. We place what would be the protocol method’s contents in the signal subscription block.
What do I gain by doing it with RAC? Not much in this particular implementation. I can keep the response behavior localized in the controller definition, and expand it to its own method if it happens in multiple places. I also could have done some filtering on the sending or receiving side or sent an error signal.
To make this work on the TCSUserNameController.m
side, I first created a public property for the userSignal
. Then, I created a new RACSubject
in the designated init
method of the controller. Remember, an RACSubject
is capable of sending next, complete, or error values whenever we want.
We’ll use a done button or textField’s return button as our confirm button.
(TCSUserNameViewController.m)
I initially implemented this by simply returning whatever userName the user entered in the box without any checking. As an additional feature, I implemented a network call into this step that checks the userName with Last.fm before returning it. The side effect is that the name gets formatted as it was intended.
Release
We’re skipping a few steps at the end here (testing, analyzing, prepping for the App Store), but this post is long enough as it is.
Improvements
There are a few things I’d like to take a look at in future versions.
More stable threading
I’m doing some strange things with threading, including setting controller properties on background threads. Most of the time it seems to work out, but I need to take a step back and map out the flow of property setting and events.
Better cell image handling
I covered how I handle cell images above, but my method has some code smell and could probably use another look.
Testing
At this point in time, I haven’t explored unit testing or UI automation testing at all, and would like to give those a shot with this project.
V1.1
While the app was processing in the App Store, I worked on several improvement to V1.1. I added friend importing to the Scrobblers controller. I also created an album detail controller which changes background color based on the album image.
I’ll be waiting to hear back from users before implementing any other new features.
Final thoughts
Congrats if you made it this far. Although I didn’t exactly hit all my goals for this post, I didn’t think it’d be nearly this long either. I hope this post will add to the discussion about ReactiveCocoa. Ping me on twitter @twocentstudios if you have comments or questions. Or check out the Hacker News thread.