May 09, 2015
Today we'll build a cute little stock ticker that automatically updates every second. Here's what it'll look like when we're done:
Most of the app is the same as our pull to refresh project with a tableview that displays stock info from the Yahoo financial API. Originally we set it up to update using pull to refresh. Today we'll switch it to automatically update and use NSNotifications to make the data available to the tableview.
This tutorial has been updated for Swift 2.0, Xcode 7, and Alamofire 3.0.0.
Project Setup
Just like last time, we need to set up a project to display the data before we implement updating it:
- Create a new single view project in Xcode
- In the storyboard, drag & drop a tableview into the main view for the view controller (or remove the UIViewController and replace it with a UITableViewController)
- Add a prototype cell to the tableview with the subtitle style and set the reuse identifier to "Cell"
- Add an [ATS Exception for the API](https://grokswift.com/swift2/)
- Create a QuoteItem class in a new file with 4 string properties: symbol, yearHigh, yearLow, and ask
- Add Alamofire & SwiftyJSON to the project
- Use Alamofire & SwiftyJSON to extend Alamofire.Request and create a function to pull the data from the API and populate an array of CurrentItems (Follow along with Hooking Up a REST API to a UITableView in Swift but adjust the class & variable names as well as the endpoint and JSON parsing or check out the code on GitHub)
- Fill out the ViewController so with the boilerplate to display an array of items. Don't hook up the call to the Alamofire request, we'll use a notification instead.
If you just want the code, here it is: SwiftNotifications on GitHub or zip.
Sending Notifications
Instead of having the tableview cause the request for new data to be sent, we're going to set up a timer so it automatically requests new data every second. Then it'll send out a notification with the new data. So first, let's set up a class that's responsible for asking for the updates and distributing them, called QuoteUpdater. When it gets created it'll create a timer that calls the getUpdatedQuotes:
function every second.
class QuoteUpdater {
var timer:NSTimer?
let INTERVAL_SECONDS = 1.0
required init() {
self.timer = nil // Swift requires values to be set before doing anything
// else with self in init, so we can't set the target in the
// next line without this one first
self.timer = NSTimer.scheduledTimerWithTimeInterval(INTERVAL_SECONDS, target: self, selector: "getUpdatedQuotes:", userInfo: nil, repeats: true)
}
If you look closely there's an oddity here. We're setting self.timer
to nil then immediately setting it to our timer. That's because we're working in an init function. Swift does init in 2 parts (though you'd never guess it to look at the code). First you have to provide a value for all of the properties. Then you can do other stuff with self
. So why can't we just do self.timer = NSTimer…
? Because target: self
can't be used until all of the properties have values. So we just set the timer to nil, ending the first phase of init
, then we can use target: self
. Still looks funny to me so I added to a comment to keep myself from deleting self.timer = nil
in the future.
For convenience (and to avoid spelling errors), we'll use a class method to hold the identifier for the notification:
class func updateNotificationName() -> String {
return "UpdatedQuotes";
}
Then we need to implement the getUpdatedQuotes:
function. It needs to request the new stock quotes then create a notification to send them out:
dynamic func getUpdatedQuotes(timer: NSTimer!) {
StockQuoteItem.getFeedItems({ (items, error) in
if error != nil
{
// TODO: add error handling
}
else
{
let notification = NSNotification(name: QuoteUpdater.updateNotificationName(), object: items)
NSNotificationCenter.defaultCenter().postNotification(notification)
}
})
}
So each time the timer fires off, we get new data asynchronously by calling StockQuoteItem.getFeedItems()
and create a notification that includes the data (object: items
) and is identified by our class method QuoteUpdater.updateNotificationName()
:
let notification = NSNotification(name: QuoteUpdater.updateNotificationName(), object: items)
Then we use the default notification center for the app to broadcast the notification to anyone who is listening:
NSNotificationCenter.defaultCenter().postNotification(notification)
If you're sharp you might have noticed the dynamic
keyword in the function declaration. If you leave it out you'll get an error like this:
*** NSForwarding: warning: object 0x7f9662d427e0 of class SwiftNotificationsDemo.QuoteUpdater' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector -[SwiftNotificationsDemo.QuoteUpdater getUpdatedQuotes:]
That's because methodSignatureForSelector:
is an Objective-C method. By default, Swift objects don't have it. Adding dynamic
to the function signature makes calls to it get dispatched using the Objective-C runtime which avoids our error. The real issue is that NSTimer is still very much an Objective-C class. Hopefully we'll get something more Swift-y soon. Another way to fix this error is to tag the class with @objc
:
@objc class QuoteUpdater {
...
}
Which exposes the class to the Objective-C runtime. Then it'll effectively insert dynamic
if it's needed. Personally, I'd rather have dynamic
near the spot that requires it than labelling the whole class. If I use @objc
I probably won't remember why I did it next time I look at this code.
Initializing Notification Sender
So now that's we've got our QuoteUpdater class set up, we need to create an instance so that init
method gets called to start the updates. We can do this when the app starts up in the AppDelegate:
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
// create QuoteUpdater to start timer
var quoteUpdater = QuoteUpdater()
}
That's nice and tidy. So we're getting updates and broadcasting the data but no one is listening :(
Receiving Notifications
To receive notifications we need to register as an observer with the default notification center using the same name as when we send the notifications (QuoteUpdater.updateNotificationName()
). We can set that up in our view controller's viewDidLoad()
function:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "processNewQuoteItems:", name: QuoteUpdater.updateNotificationName(), object: nil)
}
So now whenever there's new data processNewQuoteItems:
will be called with the notification as it's argument. Then we can pull out the array of stock quotes that we stuffed in it when we created the notification:
func processNewQuoteItems(notification: NSNotification?) {
if let newQuotes = notification?.object as? Array
{
self.itemsArray = newQuotes
self.tableView?.reloadData()
}
}
Within processNewQuoteItems:
we extract the stock data:
if let newQuotes = notification?.object as? Array
Save the data:
self.itemsArray = newQuotes
And reload the tableview to reflect the new data:
self.tableView?.reloadData()
And That's All
Save & run. You'll see the stock numbers appear after about a second and then update every second as the timer fires and the notifications get dispatched. If you got tired of typing, here's the final code: SwiftNotifications on GitHub or zip. If you really want to see the power of NSNotifications, add a detail view for each stock that also updates every second. You won't need to make any connections directly between Alamofire and your new detail view controller or create another QuoteUpdater. In fact, neither the table view nor your detail view has any idea where the stock data is coming from. You could swap it out for pre-recorded data or a different API without touching any of your views & view controllers at all :)