作为一个没有时间观看太多比赛的热心的足球迷,我想获得一种了解最新比分的直观方式。大多数相关的应用程序似乎都很臃肿和平淡无奇,所以我决定自己构建一个。
在本教程中,我将介绍我如何构建 “Occer” 应用程序并将它部署在 IBM 的 Bluemix 平台上。后端 API 层是使用 Ruby、Sinatra 和 MongoDB 构建的。它使用免费的 football-data.co.uk 数据提要来获取最新结果。该应用程序本身是一个使用 Swift 编写的 iOS 应用程序,使用了 IBM Bluemix Mobile SDK 来处理推送通知。
“ 本教程将介绍如何构建一个 iOS 应用程序来获取英格兰足球超级联赛的最新足球比分,并在有新比分时发送推送通知。 ”
您可以从 IBM DevOps Services 的 “moccer” 项目中获取下面的说明中提及的代码。
获取代码
构建此应用程序的第一步是创建一个后端,该后端将执行以下操作:
我们首先会创建一个基本的 Sinatra 应用程序。
source 'https://rubygems.org' gem 'mongo' gem 'bson' gem 'bson_ext' gem 'sinatra' gem 'thin'
$ bundle install
创建一个名为 app.rb 的文件并向其中添加以下代码。
require 'sinatra' get '/' do 'Hello, world!' end
此代码定义一个非常基本的 “Hello, World” Sinatra 应用程序,该应用程序在用户导航到它的根目录时显示此消息。
require './app' run Sinatra::Application
$ rackup -p 4567
您现在应该能够打开您选择的 Web 浏览器,访问 http://localhost:4567 并看到显示了熟悉的 “Hello, World!” 消息。
我们将整个 app.rb 文件替换为将从 football-data.co.uk 文件抓取数据,并将这些数据存储在 MongoDB 数据库中的代码。确保您有一个本地 MongoDB 数据库正在运行,否则该应用程序不会启动。要进行核实,可以在命令行上运行 mongo
。如果看到一条类似 “connect failed” 的消息,则表明 MongoDB 未在本地运行。
require 'net/http' require 'uri' require 'csv' require 'mongo' require 'sinatra' require 'json' include Mongo
configure :development do host = 'localhost' port = MongoClient::DEFAULT_PORT client = MongoClient.new(host, port) db = client.db('occer-data') set :coll, db.collection('scores') || db.create_collection('scores') end configure do set :csvUrl, "http://www.football-data.co.uk/mmz4281/1415/E0.csv" CSV::Converters[:date_to_iso] = lambda do |s| begin Date.strptime(s, '%d/%m/%y').strftime('%Y-%m-%d') rescue s end end end
在第一个代码块中,您定义了 MongoDB 连接参数、数据库名称和集合(或者如果该集合还不存在,则创建它)。在第二个代码块中,将会定义用于查找足球比赛结果的 URL。请注意, 1415
表示足球赛季 2014/2015。要抓取另一个赛季的数据,可将 1415
替换为相关的值。您还需要定义一个 CSV 转换器,它将获取日期并将该日期格式化为 ISO 数据标准 (YYYY-MM-DD)。
定义一个基本的 API 端点,它将抓取该 CSV 文件的最新版本,检查其中是否包含新记录,以及更新 MongoDB 数据库中的数据。将此代码添加到紧挨前一个清单的代码后面。
get '/update' do count = settings.coll.find.count uri = URI.parse(settings.csvUrl) response = Net::HTTP.get_response uri csv = CSV.new(response.body, { :headers => :first_row, :header_converters => :symbol, :converters => [:all, :date_to_iso] }) keep = [ :date, :hometeam, :awayteam, :fthg, :ftag, :ftr, :hthg, :htag, :htr, :referee ] data = csv.to_a.map { |row| row.to_hash.select { |k, v| keep.include?(k) } } data.delete_if { |v| v[:ftr] == nil } records = data.length - count if records > 0 # Push notification! settings.coll.remove settings.coll.insert(data) end { :success => true, :records => records }.to_json end
此代码将获取数据库中比分统计,抓取最新的数据,并进行比较,以查看是否有新比分。我们放置了一个占位符来发送推送通知。稍后将返回这里。为了简便起见,我们删除了数据库中的所有记录,将它们替换为来自 CSV 文件的最新数据。实际上,您可能希望使用增量更新。
$ rackup -p 4567
在您浏览器中导航到 http://localhost:4567/update。您会看到一个类似这样的响应:
{"success":true,"records":110}
请注意,找到的记录数量取决于您阅读本教程的时间,所以绝不应该是 110。
该 iOS 应用程序的第一个视图是一个比赛日期列表,允许用户下钻到某个比赛日期来查看那天举行的比赛的比分。为了实现此目的,我们需要提供一个返回比赛日期列表的 API。
将以下代码添加到 app.rb 文件中。
get '/dates' do dates = settings.coll.distinct('date').to_a.sort! { |x, y| y <=> x } dates.to_json end
此代码从 Mongo 集合获取不同日期的列表,将它们存储在一个数组中,然后按逆序排序该数组,并以 JSON 格式返回它。
如果重新加载 Rack 服务器并导航到 http://localhost:4567/dates,您会看到与以下内容类似的输出:
点击查看代码清单
关闭 [x]
["2014-11-09","2014-11-08","2014-11-03","2014-11-02","2014-11-01","2014-10-27","2014-10-26","2014-10-25","2014-10-20","2014-10-19","2014-10-18","2014-10-05","2014-10-04","2014-09-29","2014-09-28","2014-09-27","2014-09-21","2014-09-20","2014-09-15","2014-09-14","2014-09-13","2014-08-31","2014-08-30","2014-08-25","2014-08-24","2014-08-23","2014-08-18","2014-08-17","2014-08-16"]
同样地,请记住您可能会看到比我多得多的日期,因为到您阅读本文时举行了更多比赛。
最后的 API 端点将抓取一个给定比赛日期的比分数组。它接受一个查询参数,该参数告诉该 API 要查找的日期。如果未提供任何参数,则会获取并使用最大的比赛日期。
将以下代码附加到 app.rb 文件中并保存。
点击查看代码清单
关闭 [x]
get '/' do date_str = params[:date] date = Date.strptime(date_str, '%Y-%m-%d') rescue nil if date.nil? maxdate = settings.coll.find({}, :fields => ["date"], :limit => 1, :sort => ['date','desc']).to_a date_str = maxdate.length > 0 ? maxdate[0]["date"] : '' date = Date.strptime(date_str, '%Y-%m-%d') rescue nil end unless date.nil? # Get scores that match the given date scores = settings.coll.find({"date" => date_str}).to_a scores.to_json else [].to_json end end
此 API 在 URI 中寻找一个日期查询参数。如果它未找到,则使用限制和排序指令个从数据库获取最新的比赛日期。它然后在 Mongo 集合中查找所有与给定日期匹配的比分,并以 JSON 格式返回。
同样地,重新启动服务器进程,这次导航到 http://localhost:4567/。您会看到与下面类似的内容:
点击查看代码清单
关闭 [x]
[{"_id":{"$oid": "5464efb0f10cf604dd0000d9"},"date":"2014-11-09","hometeam":"Sunderland","awayteam":"Everton","fthg":1,"ftag":1,"ftr":"D","hthg":0,"htag":0,"htr":"D","referee":"L Mason"},{"_id":{"$oid": "5464efb0f10cf604dd0000da"},"date":"2014-11-09","hometeam":"Swansea","awayteam":"Arsenal","fthg":2,"ftag":1,"ftr":"H","hthg":0,"htag":0,"htr":"D","referee":"P Dowd"},{"_id":{"$oid": "5464efb0f10cf604dd0000db"},"date":"2014-11-09","hometeam":"Tottenham","awayteam":"Stoke","fthg":1,"ftag":2,"ftr":"A","hthg":0,"htag":2,"htr":"A","referee":"M Jones"},{"_id":{"$oid": "5464efb0f10cf604dd0000dc"},"date":"2014-11-09","hometeam":"West Brom","awayteam":"Newcastle","fthg":0,"ftag":2,"ftr":"A","hthg":0,"htag":1,"htr":"A","referee":"C Pawson"}]
请记住,您看到的数据可能会有所不同,但键值应该是类似的。
后端现在已完成。现在,让我们将后端部署到 Bluemix。
要将应用程序推送到 Bluemix,首先需要向项目目录添加一个 manifest.yml 文件,告诉 Bluuemix 您想要部署的应用程序的一些信息。
applications: - name: occer host: occer disk: 1024M path: . domain: mybluemix.net mem: 128M instances: 1
备注:您需要将名称和主机值更改为您应用程序独有的值。
$ cf api https://api.ng.bluemix.net $ cf login $ cf push
occer
的引用是您在 manifest.yml 文件中创建的应用程序名称。 $ cf create-service mongodb 100 occer-db $ cf bind-service occer occer-db
configure :production do # Get the credentials for the MongoDB Bluemix service env = JSON.parse(ENV["VCAP_SERVICES"])["mongodb-2.4"].first["credentials"] # Connect to MongoDB and authenticate using environment variables conn = MongoClient.new(env["hostname"], env["port"]) db = conn.db(env["db"]) db.authenticate(env["username"], env["password"]) # Store the DB connection in a setting for easy retrieval later set :coll, db.collection('scores') || db.create_collection('scores') end
$ cf push
{"success":true,"records":110}
根据您阅读本教程的时间,您获得的数量可能与 110 不同。请注意,如果您已经填充了该数据库,就会看到一个记录数量 0。这是因为未找到新记录。
接下来,您将构建前端 iOS 应用程序来显示这些比分。
启动 Xcode 并创建一个新的大纲-细节 iOS 应用程序项目。将它命名为 Occer
,将 Organization Name 和 Bundler Identifier 设置为 Demo 。将 Language 设置为 Swift ,将 Devices 设置为 iPhone 。保留 Use Core Data 未选择并按下 Next 。选择一个位置来存储您的项目,然后将创建该项目。
Occer 2014/15
。 Detail View Controller
。 Style
更改为 Basic
。 Cell
。 完成这些操作后,故事板应该类似于:
点击查看大图
关闭 [x]
UI 工作现在已完成,您已准备好将应用程序连接到后端。
DateAPIController
。 import Foundation protocol DateAPIControllerProtocol { func didReceiveDates(dates: NSArray) } class DateAPIController { var delegate: DateAPIControllerProtocol? init() { } func getDates() { let urlPath = "http://occer.mybluemix.net/dates" let url = NSURL(string: urlPath) let session = NSURLSession.sharedSession() let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in if(error != nil) { println(error.localizedDescription) } var err: NSError? var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &err) as NSArray self.delegate?.didReceiveDates(jsonResult) if(error != nil) { println("JSON Error /(err!.localizedDescription)") } }) task.resume() } }此代码定义了一个协议,允许我们将特定于视图的函数放在该类外,在从服务器收到响应时使用委托来更新 UI。
getDates()
方法在 Bluemix 上查找该 API 并以 JSON 格式抓取数据,然后再在协议中调用委托函数 didReceiveDates()
。 备注:一定要将 urlPath
常量更改为您自己的 Bluemix 托管的 Ruby 应用程序的 URL。
import Foundation protocol ScoreAPIControllerProtocol { func didReceiveScores(scores: NSArray) } class ScoreAPIController { var delegate: ScoreAPIControllerProtocol? init() { } func getScores(date: NSString) { let urlPath = "http://occer.mybluemix.net/?date=/(date)" let url = NSURL(string: urlPath) let session = NSURLSession.sharedSession() let task = session.dataTaskWithURL(url!, completionHandler: {data, response, error -> Void in if(error != nil) { println(error.localizedDescription) } var err: NSError? var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: &err) as NSArray self.delegate?.didReceiveScores(jsonResult) if(error != nil) { println("JSON Error /(err!.localizedDescription)") } }) task.resume() } }
此文件与 DateAPIController 文件的主要区别在于, getScores()
方法接受一个日期字符串作为参数,而且这个字符串会作为查询字符串参数附加到 API URL。除此之外,它们的工作原理完全相同。再次声明,确保将 urlPath
常量更改为您自己的 Bluemix 托管的 API 应用程序的实际 URL。
要让所有功能正常运行,需要将 API 类连接到视图控制器。
MasterViewController
类: class MasterViewController: UITableViewController {
DateAPIControllerProtocol
协议: class MasterViewController: UITableViewController, DateAPIControllerProtocol {
NSMutableArray
的行替换为以下代码: var objects = NSArray() var api = DateAPIController()您不需要在对象数组中添加或删除内容,所以应将它定义为一个常规
NSArray
,而不是一个 NSMutableArray
。还要初始化 DateAPIController
类。 viewDidLoad()
方法更改为以下形式: 点击查看代码清单
关闭 [x]
override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged) tableView.addSubview(refreshControl!) }
这会向表格视图添加一个 pull-to-refresh
控件子视图。执行此操作时,会调用 refresh()
方法。
viewDidLoad()
方法下,添加以下代码: override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) self.refresh(self) } func refresh(sender: AnyObject) { self.api.delegate = self self.api.getDates() } func didReceiveDates(dates: NSArray) { dispatch_async(dispatch_get_main_queue(), { self.objects = dates self.tableView.reloadData() self.refreshControl?.endRefreshing() }) }只要出现该表格视图,
viewWillAppear()
方法就会刷新该表格。视图出现时或使用表格的 pull-to-refresh 控件时,将调用 refresh()
方法。最后, didReceiveDates()
方法是早先定义的 DateAPIControllerProtocol 的实现。它将对象数组设置为 API 调用的结果,重新加载表格视图数据,并告诉 pull-to-refresh 控件停止刷新。 insertNewObject()
方法。此应用程序不需要它。 prepareForSegue()
方法中,找到下面这行: let object = objects[indexPath.row] as NSDate将此行更改为以下代码:
let object = objects[indexPath.row] as NSString
点击查看代码清单
关闭 [x]
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let object = objects[indexPath.row] as NSDate将此行更改为以下代码:
let object = objects[indexPath.row] as NSString
canEditRowAtIndexPath
和 commitEditingStyle
表格视图方法。 接下来,您将连接 DetailViewController。
class DetailViewController: UIViewController {
ScoreAPIControllerProtocol
协议: class DetailViewController: UITableViewController, ScoreAPIControllerProtocol {
@IBOutlet weak var detailDescriptionLabel: UILabel!将它替换为以下代码:
var objects = NSArray() var api = ScoreAPIController()
detailItem
的定义,使它成为 NSString?
类型,而不是 AnyObject?
类型: var detailItem: NSString? { didSet { // Update the view self.configureView() } }
configureView()
方法内容替换为对一个名为 refresh
的新方法的调用,我们稍后将创建该方法。 func configureView() { self.refresh(self) }
viewDidLoad()
方法中,将 self.configure()
行替换为以下代码,以实现 pull-to-refresh: 点击查看代码清单
关闭 [x]
refreshControl = UIRefreshControl() refreshControl!.addTarget(self, action: "refresh:", forControlEvents: UIControlEvents.ValueChanged) tableView.addSubview(refreshControl!)
override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) self.refresh(self) } func refresh(sender: AnyObject) { if let detail:NSString = self.detailItem { self.title = detail self.api.delegate = self self.api.getScores(detail) } } func didReceiveScores(scores: NSArray) { dispatch_async(dispatch_get_main_queue(), { self.objects = scores self.tableView.reloadData() self.refreshControl?.endRefreshing() }) }
didReceiveMemoryWarning()
方法不变,添加以下 3 个方法来完成该类: // MARK: - Table View override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return 1 } override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return objects.count } override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell let object = objects[indexPath.row] as NSDictionary let homeTeam = object["hometeam"] as NSString let awayTeam = object["awayteam"] as NSString let homeScore = object["fthg"] as NSInteger let awayScore = object["ftag"] as NSInteger cell.textLabel.text = "/(homeTeam) /(homeScore) - /(awayScore) /(awayTeam)" return cell }
提示:如果应用程序成功构建但在启动时崩溃了,应用程序可能无法连接到 Bluemix API。确保您在 API 地址中拥有正确的 URL,而且拥有互联网连接。
要让应用程序在真实设备上运行,需要执行一些步骤,尤其在您想让推送通知在下一节中生效的时候。
点击查看大图
关闭 [x]
Occer App ID
),并确保选择了 Explicit App ID 。Bundle ID 值应该与您应用程序的 Bundle Identifier 匹配,该标识符可在 Project Properties 中的 General 表格中找到(如果已按照早先的指南进行操作,该值应该类似于 Demo.Occer
)。 Configurable
。 现在我们向应用程序添加推送通知功能,完成该应用程序。
occer-mas
(应该是惟一名称),然后创建它。 occer-push
。 Occer-Header-Bridge.h
。删除该文件的内容,将其替换为以下代码。 #import <IBMBluemix/IBMBluemix.h> #import <IBMPush/IBMPush.h>
Occer-Header-Bridge.h
。 备注:如果该文件已添加到您应用程序结构中的 Occer 子文件夹中,您可能需要值 Occer/Occer-Header-Bridge.h
。
didFinishLaunchingWithOptions
方法。将此方法替换为以下代码。 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Override point for customization after application launch. IBMBluemix.initializeWithApplicationId("yourappid", andApplicationSecret: "yourappsecret", andApplicationRoute: "yourapproute.mybluemix.net") IBMPush.initializeService() var type = UIUserNotificationType.Badge | UIUserNotificationType.Alert | UIUserNotificationType.Sound var settings: UIUserNotificationSettings = UIUserNotificationSettings(forTypes: type, categories: nil) application.registerUserNotificationSettings(settings) application.registerForRemoteNotifications() return true }
确保已将 yourappid 、 yourappsecret 和 yourapproute 值替换为您应用程序的实际值。此代码初始化 IBM SDK 并尝试向应用程序注册远程通知。在用户第一次启动该应用程序时,他会被要求允许发送该应用程序的推送通知。以后不会再收到此提示,但应用程序每次启动时都会尝试注册。如果用户选择不允许发送推送通知,但在以后通过 iOS 设置手动授予访问权,应用程序将在下次启动时应用此更改。
} 的前面:
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) { var pushService: IBMPush = IBMPush.service() pushService.registerDevice("occeralias", withConsumerId: "occerconsumerid", withDeviceToken: deviceToken.description).continueWithBlock({ (task: BFTask!) -> BFTask in if task.error() != nil { println(task.error().description) } else { println(task.result().description) } return task }) } func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) { println("Push Registration Failed. error: /(error.debugDescription)") }
如果用户向应用程序授予了向设备发送推送通知的权限,此代码会向 IBM 推送服务注册该设备。 occeralias
和 occerconsumerid
值无关紧要。本例中的惟一 ID 是设备令牌。
现在设备已注册了推送通知,您可以向设备发送一个测试通知。
Testing push notifications for Occer
,按下 Next 。 您看到的通知类型取决于您的设备目前是否锁定。如果设备是锁定的,那么您会看到与下面这个屏幕类似的屏幕。
如果您点击该提醒,它会直接启动 Occer 应用程序。
从 Bluemix 仪表板发送推送通知不是很有用。我们更改一下 Ruby 后端,以便它会在每次向数据库添加新记录的时候发送通知。
# Push notification!
app_id = 'yourappid' app_secret = 'yourappsecret' push_url = URI('https://mobile.ng.bluemix.net/push/v1/apps/' + app_id + '/messages') https = Net::HTTP.new(push_url.host, push_url.port) https.use_ssl = true push_body = { :message => { :alert => 'New English Premier League soccer scores are available' } }.to_json req = Net::HTTP::Post.new(push_url.path, initheader = {'Content-Type' => 'application/json'}) req['IBM-Application-Secret'] = app_secret req.body = push_body res = https.request(req)
$ cf push
当然,要实际测试此功能的运行情况,需要触发该数据库的一次更新,以便从 football-data.co.uk 服务查找新比分。自您上次更新 Bluemix 数据库中的比分以来,不太可能添加了新比分。幸运的是,您可以在本地强制添加新比分,方法是使用 mongo CLI 客户端登录 MongoDB 并从数据库中删除所有比分。完成这些操作之后,使用以下命令再次启动您的本地应用程序:
$ rackup -p 4567
您现在可以返回到 http://localhost:4567/update 下载更新的比分。这会导致发送推送通知。
最后一步是确保您的应用程序每天检查比分更新。这通常使用 cron 作业来完成。不幸的是,IBM Bluemix 没有 cron 作业的概念。但是,它有一个名为 Workload Scheduler 的服务可用来实现同样的目的。您可以轻松地构建一个应用程序,使用 Workload Scheduler 每天自动向您的 Bluemix 有一些的 /update API 端点发送 HTTP GET
请求。
也可以使用免费的 Web 服务(比如 SetCronJob )来实现同样的目的,无需构建另一个应用程序。
本教程介绍了大量技术,从 Ruby 和 Sinatra 到 iOS 应用程序和推送通知。作为开发人员,Bluemix 平台使您能够将精力集中在构建优秀的功能上,无需担忧服务器和设备管理。本教程所描述的应用程序只是您可以使用 Bluemix 实现的功能的一个小示例。我很期待看到您提出新的想法。