iOS 7最佳实践:一个天气App案例

suiling 2014-02-24 10:02:33 93389
转自:sjpsega's Blog
iOS7最佳实践:一个天气App案例(一)
 
注:本文译自:raywenderlich ios-7-best-practices-part-1,去除了跟主题无关的寒暄部分。
 
在这个两部分的系列教程中,您将探索如何使用以下工具和技术来创建自己的App:
Manual layout in code(纯代码布局)
 
本教程专为熟悉基本知识的、但还没有接触到太多高级主题的中级开发者而设计。本教程也是想要去探索Objective-C函数编程一个很好的开始。
开始
打开Xcode并执行File\New\Project。选择Application\Empty Application。将项目命名为SimpleWeather,单击下一步,选择一个目录去保存你的项目,然后点击Create。 现在,你的基础项目已经完成。下一步是集成你的第三方工具。但首先你要关闭Xcode,确保他不会影响下一步。
 
Cocoapods
你将要下载Cocoapods的代码,在Xcode项目中添加文件来使用,并配置项目需要的设置。
 
Mantle
Mantle是由于Github团队开发的,目的是去除Objective-C把JSON数据转为NSObject子类的所有样板代码。Mantle也能做数据转换,通过一种神奇的方式把JSON原始数据(strings, ints, floats)转换为复杂数据,比如NSDate, NSURL, 甚至是自定义类。
 
LBBlurredImage
LBBlurredImage是一个继承自UIImageView,轻而易举使图像模糊的项目。你将仅仅用一行代码来创建一个神奇的模糊效果。
 
TSMessages
TSMessages 是另一个非常简单的库,用来显示浮层警告和通知。当出现错误信息而不直接影响用户的时候,最好使用浮层来代替模态窗口(例如UIAlertView),这样你将尽可能减少对用户的影响。
 
你将只用TSMessages,在网络失去连接或API错误的时候。如果发生错误,你将看到类似这样的一个浮层:
ReactiveCocoa
最后,你将使用到ReactiveCocoa,他也来自于GitHub团队。ReactiveCocoa给Objective-C带来了函数编程,类似与.NET的Reactive Extensions。你将在第二部分花费大部分时间去实现ReactiveCocoa。
 
设置你的Cocoapods
 
设置你的Cocoapods,先要确保你已经安装了Cocoapods。为此,打开命令行程序,并输入。
  1. which pod 
 
你将会看到类似这样的输出:
  1. /usr/bin/pod 
这决定于你如何管理Ruby gems,例如你使用rbenvRVM,路径可能有所不同。
 
如果命令行简单的返回提示,或显示pod not found,表示Cocoapods未安装在你的机器上。可以查看我们的Cocoapods教程作为安装说明。这也是一个很好的资源,如果你想更多得了解Cocoapods的话。
 
Podfiles是用来告诉Cocoapods哪些开源项目需要导入。
 
要创建你的第一个Cocoapod,首先在命令行中用cd命令导航到你的XCode项目所在的文件夹,在命令行中启动编辑器,输入
  1. platform :ios, '7.0' 
  2.   
  3. pod 'Mantle' 
  4. pod 'LBBlurredImage' 
  5. pod 'TSMessages' 
  6. pod 'ReactiveCocoa' 
这文件做了两件事情:
1.告诉Cocoapods你的目标平台与版本,这里的你目标是iOS 7.0。
2.列给Cocoapods一个项目所有需要引入和安装的三方库清单。
 
在命令行中输入pod install进行安装。
 
这可能需要花一到两分钟的时间去安装各种包。你的命令行应该输出如下所示:
  1. $ pod install 
  2. Analyzing dependencies 
  3.   
  4. CocoaPods 0.28.0 is available. 
  5.   
  6. Downloading dependencies 
  7. Installing HexColors (2.2.1) 
  8. Installing LBBlurredImage (0.1.0) 
  9. Installing Mantle (1.3.1) 
  10.   
  11. Installing ReactiveCocoa (2.1.7) 
  12. Installing TSMessages (0.9.4) 
  13. Generating Pods project 
  14. Integrating client project 
  15.   
  16. [!] From now on use `SimpleWeather.xcworkspace`. 
 
  1. sjpsega注:若你之前安装过Cocoapods的话,这里安装报错的话,可以看看http://blog.cocoapods.org/Repairing-Our-Broken-Specs-Repository/ 修复问题 
Cocoapods会在你的项目目录中创建一堆新文件,但是,只有一个需要你关心,SimpleWeather.xcworkspace。
 
用Xcode打开SimpleWeather.xcworkspace。看看你的项目设置,现在有一个Pods项目在你的项目工作区,以及在Pods文件夹放着每一个你引入的库,如下所示:
确保你已经选择SimpleWeather项目,如图所示: Select SimpleWeather Project
构建并运行您的App,以确保一切工作正常: 
  1. 提示:您可能会注意到有一些项目生成警告。因为Cocoapods引入的项目,是由不同的开发者开发,并且不同的开发者对生成警告有不同的态度。通常,你应该可以忽略它们。只要确保没有任何编译器错误! 
 
创建你的主视图控制器
 
虽然这个App看起来复杂,但它还会通过一个单一的View Controller完成。现在,你将添加他。
 
选中SimpleWeather项目,单击File\New\File,并且选择Cocoa Touch\Objective-C class. 命名为WXController,并设置为UIViewController的子类。
 
确保Targeted for iPad和With XIB for user interface都没有选中,如下图所示: Create WXController
 
打开WXController.m然后用如下所示替换-viewDidLoad方法:
  1. - (void)viewDidLoad { 
  2.     [super viewDidLoad]; 
  3.  
  4.     // Remove this later 
  5.     self.view.backgroundColor = [UIColor redColor]; 
 
现在打开AppDelegate.m,并且引入如下两个class:
  1. #import "WXController.h" 
  2. #import <TSMessage.h> 
眼尖的读者会注意到WXController使用引号引入,TSMessage使用单括号引入。
 
回头看下当你创建Podfile的时候,你使用Cocoapods引入TSMessage。Cocoapods创建TSMessage项目,并把它加入到工作空间。既然你从工作区的其他项目导入,可以使用尖括号代替引号。
 
代替-application:didFinishLaunchingWithOptions的内容:
  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 
  2.     self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; 
  3.     // 1 
  4.     self.window.rootViewController = [[WXController alloc] init]; 
  5.     self.window.backgroundColor = [UIColor whiteColor]; 
  6.     [self.window makeKeyAndVisible]; 
  7.     // 2 
  8.     [TSMessage setDefaultViewController: self.window.rootViewController]; 
  9.     return YES; 
标号注释的解释:
1.初始化并设置WXController实例作为App的根视图控制器。通常这个控制器是一个的UINavigationController或UITabBarController,但在当前情况下,你使用WXController的单个实例。
2.设置默认的视图控制器来显示你的TSMessages。这样做,你将不再需要手动指定要使用的控制器来显示警告。
 
构建并运行,看看你的新视图控制器起作用了。
 
 
在红色背景下,状态栏有点不够清晰。幸运的是,有一个简单的方法,使状态栏更清晰易读。
 
在iOS7,UIViewController有一个新的API,用来控制状态栏的外观。打开WXController,直接添加下面的代码到-viewDidLoad:方法下:
  1. - (UIStatusBarStyle)preferredStatusBarStyle { 
  2.     return UIStatusBarStyleLightContent; 
再次构建并运行,你将看到状态栏如下的变化:
设置你的App视图
 
现在是时候让你的App接近生活。下载这个项目的图片,并解压缩到一个合适的位置。这个压缩包的背景图片出自Flickr用户idleformat之手,天气图片出自Dribbble用户heeyeun之手。
 
切换回Xcode,单击File\Add Files to “SimpleWeather”….定位到你刚刚解压缩的图片文件夹并选择它。选择Copy items into destination group’s folder (if needed),然后单击Add。
 
打开WXController.h, 添加如下委托协议:
  1. <UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate> 
现在打开WXController.m。 小提示:你可以使用Control-Command-Up的快捷键来实现.h和.m文件之间的快速切换。
 
添加如下代码到WXController.m顶部:
  1. #import <LBBlurredImage/UIImageView+LBBlurredImage.h> 
LBBlurredImage.h包含在Cocoapods引入的LBBlurredImage项目,你会使用这个库来模糊背景图片。
 
应该有一个空的私有接口样板在WXController imports的下方。它具有以下属性:
  1. @interface WXController () 
  2.  
  3. @property (nonatomic, strong) UIImageView *backgroundImageView; 
  4. @property (nonatomic, strong) UIImageView *blurredImageView; 
  5. @property (nonatomic, strong) UITableView *tableView; 
  6. @property (nonatomic, assign) CGFloat screenHeight; 
  7.  
  8. @end 
现在,是时候在项目中创建并设置视图。
 
下面是你App的分解图,记住,table view将是透明的:
 
为了实现动态模糊效果,在你的App中,你会根据App的滚动来改变模糊图像的alpha值。
 
打开WXController.m,使用如下代码来,替换掉-viewDidLoad中设置背景色的代码:
  1. // 1 
  2. self.screenHeight = [UIScreen mainScreen].bounds.size.height; 
  3.  
  4. UIImage *background = [UIImage imageNamed:@"bg"]; 
  5.  
  6. // 2 
  7. self.backgroundImageView = [[UIImageView alloc] initWithImage:background]; 
  8. self.backgroundImageView.contentMode = UIViewContentModeScaleAspectFill; 
  9. [self.view addSubview:self.backgroundImageView]; 
  10.  
  11. // 3 
  12. self.blurredImageView = [[UIImageView alloc] init]; 
  13. self.blurredImageView.contentMode = UIViewContentModeScaleAspectFill; 
  14. self.blurredImageView.alpha = 0; 
  15. [self.blurredImageView setImageToBlur:background blurRadius:10 completionBlock:nil]; 
  16. [self.view addSubview:self.blurredImageView]; 
  17.  
  18. // 4 
  19. self.tableView = [[UITableView alloc] init]; 
  20. self.tableView.backgroundColor = [UIColor clearColor]; 
  21. self.tableView.delegate = self; 
  22. self.tableView.dataSource = self; 
  23. self.tableView.separatorColor = [UIColor colorWithWhite:1 alpha:0.2]; 
  24. self.tableView.pagingEnabled = YES; 
  25. [self.view addSubview:self.tableView]; 
这是非常简单的代码:
1.获取并存储屏幕高度。之后,你将在用分页的方式来显示所有天气??数据时,使用它。
2.创建一个静态的背景图,并添加到视图上。
3.使用LBBlurredImage来创建一个模糊的背景图像,并设置alpha为0,使得开始backgroundImageView是可见的。
4.创建tableview来处理所有的数据呈现。 设置WXController为delegate和dataSource,以及滚动视图的delegate。请注意,设置pagingEnabled为YES。
 
添加如下UITableView的delegate和dataSource的代码到WXController.m的@implementation块中:
  1. // 1 
  2. #pragma mark - UITableViewDataSource 
  3.  
  4. // 2 
  5. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 
  6.     return 2; 
  7.  
  8. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 
  9.     // TODO: Return count of forecast 
  10.     return 0; 
  11.  
  12. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
  13.     static NSString *CellIdentifier = @"CellIdentifier"
  14.     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
  15.  
  16.     if (! cell) { 
  17.         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier]; 
  18.     } 
  19.  
  20.     // 3 
  21.     cell.selectionStyle = UITableViewCellSelectionStyleNone; 
  22.     cell.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; 
  23.     cell.textLabel.textColor = [UIColor whiteColor]; 
  24.     cell.detailTextLabel.textColor = [UIColor whiteColor]; 
  25.  
  26.     // TODO: Setup the cell 
  27.  
  28.     return cell; 
  29.  
  30. #pragma mark - UITableViewDelegate 
  31.  
  32. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 
  33.     // TODO: Determine cell height based on screen 
  34.     return 44; 
Pragma mark是组织代码的很好的一种方式。
 
你的table view有两个部分,一个是每小时的天气预报,另一个用于每日播报。table view的section数目,设置为2。
 
天气预报的cell是不可选择的。给他们一个半透明的黑色背景和白色文字。
  1. 注意:使用格式化的注释 // TODO:可以帮助Xcode找到需要以后完成的代码。你还可以使用 Show Document Items(Control-6)来查看TODO项。 
最后,添加如下代码到WXController.m:
  1. - (void)viewWillLayoutSubviews { 
  2.     [super viewWillLayoutSubviews]; 
  3.  
  4.     CGRect bounds = self.view.bounds; 
  5.  
  6.     self.backgroundImageView.frame = bounds; 
  7.     self.blurredImageView.frame = bounds; 
  8.     self.tableView.frame = bounds; 
 
在WXController.m中,你的视图控制器调用该方法来编排其子视图。
 
构建并运行你的App,看看你的视图如何堆叠。
 
 
仔细看,你会看到所有空的table cell的cell分隔线。
 
仍然在-viewDidLoad中,添加下面的代码来设置你的布局框架和边距:
  1. // 1 
  2. CGRect headerFrame = [UIScreen mainScreen].bounds; 
  3. // 2 
  4. CGFloat inset = 20; 
  5. // 3 
  6. CGFloat temperatureHeight = 110; 
  7. CGFloat hiloHeight = 40; 
  8. CGFloat iconHeight = 30; 
  9. // 4 
  10. CGRect hiloFrame = CGRectMake(inset, 
  11.                               headerFrame.size.height - hiloHeight, 
  12.                               headerFrame.size.width - (2 * inset), 
  13.                               hiloHeight); 
  14.  
  15. CGRect temperatureFrame = CGRectMake(inset, 
  16.                                      headerFrame.size.height - (temperatureHeight + hiloHeight), 
  17.                                      headerFrame.size.width - (2 * inset), 
  18.                                      temperatureHeight); 
  19.  
  20. CGRect iconFrame = CGRectMake(inset, 
  21.                               temperatureFrame.origin.y - iconHeight, 
  22.                               iconHeight, 
  23.                               iconHeight); 
  24. // 5 
  25. CGRect conditionsFrame = iconFrame; 
  26. conditionsFrame.size.width = self.view.bounds.size.width - (((2 * inset) + iconHeight) + 10); 
  27. conditionsFrame.origin.x = iconFrame.origin.x + (iconHeight + 10); 
 
这是相当常规设置代码,但这里是怎么回事:
1.设置table的header大小与屏幕相同。你将利用的UITableView的分页来分隔页面页头和每日每时的天气预报部分。
2.创建inset(或padding)变量,以便您的所有标签均匀分布并居中。
3.创建并初始化为各种视图创建的高度变量。设置这些值作为常量,使得可以很容易地在需要的时候,配置和更改您的视图设置。
4.使用常量和inset变量,为label和view创建框架。
5.复制图标框,调整它,使文本具有一定的扩展空间,并将其移动到该图标的右侧。当我们把标签添加到视图,你会看到布局的效果。
 
添加如下代码到-viewDidLoad:
  1. // 1 
  2. UIView *header = [[UIView alloc] initWithFrame:headerFrame]; 
  3. header.backgroundColor = [UIColor clearColor]; 
  4. self.tableView.tableHeaderView = header; 
  5.  
  6. // 2 
  7. // bottom left 
  8. UILabel *temperatureLabel = [[UILabel alloc] initWithFrame:temperatureFrame]; 
  9. temperatureLabel.backgroundColor = [UIColor clearColor]; 
  10. temperatureLabel.textColor = [UIColor whiteColor]; 
  11. temperatureLabel.text = @"0°"
  12. temperatureLabel.font = [UIFont fontWithName:@"HelveticaNeue-UltraLight" size:120]; 
  13. [header addSubview:temperatureLabel]; 
  14.  
  15. // bottom left 
  16. UILabel *hiloLabel = [[UILabel alloc] initWithFrame:hiloFrame]; 
  17. hiloLabel.backgroundColor = [UIColor clearColor]; 
  18. hiloLabel.textColor = [UIColor whiteColor]; 
  19. hiloLabel.text = @"0° / 0°"
  20. hiloLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:28]; 
  21. [header addSubview:hiloLabel]; 
  22.  
  23. // top 
  24. UILabel *cityLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, self.view.bounds.size.width, 30)]; 
  25. cityLabel.backgroundColor = [UIColor clearColor]; 
  26. cityLabel.textColor = [UIColor whiteColor]; 
  27. cityLabel.text = @"Loading..."
  28. cityLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18]; 
  29. cityLabel.textAlignment = NSTextAlignmentCenter; 
  30. [header addSubview:cityLabel]; 
  31.  
  32. UILabel *conditionsLabel = [[UILabel alloc] initWithFrame:conditionsFrame]; 
  33. conditionsLabel.backgroundColor = [UIColor clearColor]; 
  34. conditionsLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18]; 
  35. conditionsLabel.textColor = [UIColor whiteColor]; 
  36. [header addSubview:conditionsLabel]; 
  37.  
  38. // 3 
  39. // bottom left 
  40. UIImageView *iconView = [[UIImageView alloc] initWithFrame:iconFrame]; 
  41. iconView.contentMode = UIViewContentModeScaleAspectFit; 
  42. iconView.backgroundColor = [UIColor clearColor]; 
  43. [header addSubview:iconView]; 
 
这是相当长的一块代码,但它真的只是在做设置各种控件的繁重工作。简单的说:
1.设置当前view为你的table header。
2.构建每一个显示气象数据的标签。
3.添加一个天气图标的图像视图。
 
构建并运行你的App,你应该可以看到你之前布局的所有所有view。下面的屏幕截图显示了使用手工布局的、所有标签框在视觉上的显示。
用手指轻轻推动table,当你滚动它的时候,应该会反弹。
 
获取气象数据
你会注意到,App显示“Loading…”,但它不是真正地在工作。是时候获取一些真正的天气数据。
 
你会从OpenWeatherMap的API拉取数据。 OpenWeatherMap是一个非常棒的服务,旨在提供实时,准确,免费的天气数据给任何人。虽然有很多天气API,但他们大多要么使用较旧的数据格式,如XML,或是有偿服务 – 并且有时还相当昂贵。
 
你会遵循以下基本步骤,来获你设备的位置的气象数据:
1.找到设备的位置
2.从API端下载JSON数据
3.映射JSON到WXConditions和WXDailyForecasts
4.告诉UI有新数据了
开始创建你的天气模型和数据管理类。单击File\New\File…并选择Cocoa Touch\Objective-C class。命名为WXClient并使其为NSObject的子类。
 
这样再做三次创建以下类:
1.WXManager作为NSObject的子类
2.WXCondition作为MTLModel的子类
3.WXDailyForecast作为WXCondition的子类
 
全部完成?现在,你可以开始下一节,其中涉及映射和转换您的天气数据。
 
创建你的天气模型
你的模型将使用Mantle,这使得数据映射和转型非常简单。
 
打开WXCondition.h如下列代码,修改接口:
  1. // 1 
  2. @interface WXCondition : MTLModel <MTLJSONSerializing> 
  3.  
  4. // 2 
  5. @property (nonatomic, strong) NSDate *date; 
  6. @property (nonatomic, strong) NSNumber *humidity; 
  7. @property (nonatomic, strong) NSNumber *temperature; 
  8. @property (nonatomic, strong) NSNumber *tempHigh; 
  9. @property (nonatomic, strong) NSNumber *tempLow; 
  10. @property (nonatomic, strong) NSString *locationName; 
  11. @property (nonatomic, strong) NSDate *sunrise; 
  12. @property (nonatomic, strong) NSDate *sunset; 
  13. @property (nonatomic, strong) NSString *conditionDescription; 
  14. @property (nonatomic, strong) NSString *condition; 
  15. @property (nonatomic, strong) NSNumber *windBearing; 
  16. @property (nonatomic, strong) NSNumber *windSpeed; 
  17. @property (nonatomic, strong) NSString *icon; 
  18.  
  19. // 3 
  20. - (NSString *)imageName; 
  21.  
  22. @end 
MTLJSONSerializing协议告诉Mantle序列化该对象如何从JSON映射到Objective-C的属性。
 
这些都是你的天气数据的属性。你将会使用这些属性的get set方法,但是当你要扩展App,这是一种很好的方法来访问数据。
 
这是一个简单的辅助方法,从天气状况映射到图像文件。
 
构建并运行App。失败了……
 
原因是你没有从你的Cocoapods项目中引入Mantle。解决方法是,在WXCondition.h中,你需要把MTLModel.h替换为#import <Mantle.h>。
 
现在构建并运行App。成功了。你会看到一些新的警告,但你可以忽略他们。
 
首先,你需要处理未实现的-imageName方法。
 
打开WXCondition.m,添加如下方法:
  1. + (NSDictionary *)imageMap { 
  2.     // 1 
  3.     static NSDictionary *_imageMap = nil; 
  4.     if (! _imageMap) { 
  5.         // 2 
  6.         _imageMap = @{ 
  7.                       @"01d" : @"weather-clear"
  8.                       @"02d" : @"weather-few"
  9.                       @"03d" : @"weather-few"
  10.                       @"04d" : @"weather-broken"
  11.                       @"09d" : @"weather-shower"
  12.                       @"10d" : @"weather-rain"
  13.                       @"11d" : @"weather-tstorm"
  14.                       @"13d" : @"weather-snow"
  15.                       @"50d" : @"weather-mist"
  16.                       @"01n" : @"weather-moon"
  17.                       @"02n" : @"weather-few-night"
  18.                       @"03n" : @"weather-few-night"
  19.                       @"04n" : @"weather-broken"
  20.                       @"09n" : @"weather-shower"
  21.                       @"10n" : @"weather-rain-night"
  22.                       @"11n" : @"weather-tstorm"
  23.                       @"13n" : @"weather-snow"
  24.                       @"50n" : @"weather-mist"
  25.                       }; 
  26.     } 
  27.     return _imageMap; 
  28.  
  29. // 3 
  30. - (NSString *)imageName { 
  31.     return [WXCondition imageMap][self.icon]; 
创建一个静态的NSDictionary,因为WXCondition的每个实例都将使用相同的数据映射。
 
天气状况与图像文件的关系(例如“01d”代表“weather-clear.png”)。
 
声明获取图像文件名的公有方法。
 
看一看从OpenWeatherMap返回的JSON响应数据:
  1.     "dt": 1384279857, 
  2.     "id": 5391959, 
  3.     "main": { 
  4.         "humidity": 69, 
  5.         "pressure": 1025, 
  6.         "temp": 62.29, 
  7.         "temp_max": 69.01, 
  8.         "temp_min": 57.2 
  9.     }, 
  10.     "name""San Francisco"
  11.     "weather": [ 
  12.         { 
  13.             "description""haze"
  14.             "icon""50d"
  15.             "id": 721, 
  16.             "main""Haze" 
  17.         } 
  18.     ] 
你需要把嵌套的JSON值映射到Objective-C的属性。嵌套的JSON值是元素,如温度,即上面看到的main节点。
 
要做到这一点,你将利用的Objective-C的Key-Value Coding和Mantle的MTLJSONAdapter
 
还在WXCondition.m,通过添加+JSONKeyPathsByPropertyKey方法,“JSON到模型属性”的映射,且该方法是MTLJSONSerializing协议的require
  1. + (NSDictionary *)JSONKeyPathsByPropertyKey { 
  2.     return @{ 
  3.              @"date": @"dt"
  4.              @"locationName": @"name"
  5.              @"humidity": @"main.humidity"
  6.              @"temperature": @"main.temp"
  7.              @"tempHigh": @"main.temp_max"
  8.              @"tempLow": @"main.temp_min"
  9.              @"sunrise": @"sys.sunrise"
  10.              @"sunset": @"sys.sunset"
  11.              @"conditionDescription": @"weather.description"
  12.              @"condition": @"weather.main"
  13.              @"icon": @"weather.icon"
  14.              @"windBearing": @"wind.deg"
  15.              @"windSpeed": @"wind.speed" 
  16.              }; 
在这个方法里,dictionary的key是WXCondition的属性名称,而dictionary的value是JSON的路径。
 
您可能已经注意到,这里有一个从JSON数据映射到Objective-C属性的问题。属性date是NSDate类型的,但JSON有一个Unix时间类型(sjpsega注:即从1970年1月1日0时0分0秒起至现在的总秒数)的NSInteger值。你需要完成两者之间的转换。
 
Mantle正好有一个功能来为你解决这个问题:MTLValueTransformer。这个类允许你声明一个block,详细说明值的相互转换。
 
Mantle的转换器语法有点怪。要创建一个为一个特定属性的转换器,,您可以添加一个以属性名开头和JSONTransformer结尾的类方法。 可能看实际代码比试图解释它更容易理解,所以在WXCondition.m中添加以下为NSDate属性设置的转换器。
  1. + (NSValueTransformer *)dateJSONTransformer { 
  2.     // 1 
  3.     return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSString *str) { 
  4.         return [NSDate dateWithTimeIntervalSince1970:str.floatValue]; 
  5.     } reverseBlock:^(NSDate *date) { 
  6.         return [NSString stringWithFormat:@"%f",[date timeIntervalSince1970]]; 
  7.     }]; 
  8.  
  9. // 2 
  10. + (NSValueTransformer *)sunriseJSONTransformer { 
  11.     return [self dateJSONTransformer]; 
  12.  
  13. + (NSValueTransformer *)sunsetJSONTransformer { 
  14.     return [self dateJSONTransformer]; 
使用blocks做属性的转换的工作,并返回一个MTLValueTransformer返回值。
 
您只需要详细说明Unix时间和NSDate之间进行转换一次,就可以重用-dateJSONTransformer方法为sunrise和sunset属性做转换。
 
下一个值转型有点讨厌,但它只是使用OpenWeatherMap的API,并自己的格式化JSON响应方式的结果。weather键对应的值是一个JSON数组,但你只关注单一的天气状况。
 
在WXCondition.m中,使用dateJSONTransformer相同的结构,您可以创建一个NSArray和NSString的之间的转换。该解决方案提供如下:
  1. + (NSValueTransformer *)conditionDescriptionJSONTransformer { 
  2.     return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSArray *values) { 
  3.         return [values firstObject]; 
  4.     } reverseBlock:^(NSString *str) { 
  5.         return @[str]; 
  6.     }]; 
  7.  
  8. + (NSValueTransformer *)conditionJSONTransformer { 
  9.     return [self conditionDescriptionJSONTransformer]; 
  10.  
  11. + (NSValueTransformer *)iconJSONTransformer { 
  12.     return [self conditionDescriptionJSONTransformer]; 
最后的转换器只是为了格式化。 OpenWeatherAPI使用每秒/米的风速。由于您的App使用英制系统,你需要将其转换为每小时/英里。
 
在WXCondition.m的实现中添加以下转换器的方法和宏定义。
  1. #define MPS_TO_MPH 2.23694f 
  2.  
  3. + (NSValueTransformer *)windSpeedJSONTransformer { 
  4.     return [MTLValueTransformer reversibleTransformerWithForwardBlock:^(NSNumber *num) { 
  5.         return @(num.floatValue*MPS_TO_MPH); 
  6.     } reverseBlock:^(NSNumber *speed) { 
  7.         return @(speed.floatValue/MPS_TO_MPH); 
  8.     }]; 
 在OpenWeatherMap的API中有一个小的差异,你必须处理。看一看在位于当前状况的响应和每日预测反应之间的温度:
  1. // current 
  2. "main": { 
  3.     "grnd_level": 1021.87, 
  4.     "humidity": 64, 
  5.     "pressure": 1021.87, 
  6.     "sea_level": 1030.6, 
  7.     "temp": 58.09, 
  8.     "temp_max": 58.09, 
  9.     "temp_min": 58.09 
  10.  
  11. // daily forecast 
  12. "temp": { 
  13.     "day": 58.14, 
  14.     "eve": 58.14, 
  15.     "max": 58.14, 
  16.     "min": 57.18, 
  17.     "morn": 58.14, 
  18.     "night": 57.18 
current的第一个key是main,最高温度存储在key temp_max中,而daily forecast的第一个key是temp,最高温度存储在key max中。
 
key Temperature的差异放在一边,其他都一样。所以,你真正需要做的是修改daily forecasts的键映射。
 
打开WXDailyForecast.m重写+JSONKeyPathsByPropertyKey方法:
  1. + (NSDictionary *)JSONKeyPathsByPropertyKey { 
  2.     // 1 
  3.     NSMutableDictionary *paths = [[super JSONKeyPathsByPropertyKey] mutableCopy]; 
  4.     // 2 
  5.     paths[@"tempHigh"] = @"temp.max"
  6.     paths[@"tempLow"] = @"temp.min"
  7.     // 3 
  8.     return paths; 
获取WXCondition的映射,并创建它的可变副本。
 
你需要为daily forecast做的是改变max和min键映射。
 
返回新的映射。
 
构建并运行您的App,看起来和上次运行没什么改变,但好的一点是,App编译和运行没有任何错误。
 
 
何去何从?
 
你可以从这里这里完整程序。
 
在这部分教程中,您使用Cocoapods设置项目,增加视图到控制器,编排视图,并建立模型来反映你抓取的气象数据。该App还没有充分发挥作用,但是你成功用纯代码创建视图,并学习了如何使用Mantle映射和转换JSON数据。
 
接下来看看教程的第二部分,你将充实你的App,从weather API获取数据,并在UI上显示。您将使用新的iOS7 NSURLSession去下载数据,以及使用ReactiveCocoa把位置查找,天气数据抓取和UI更新事件绑在一起。
 
iOS 7最佳实践:一个天气App案例
#p#副标题#e#
iOS 7最佳实践:一个天气App案例(二)
注:本文译自:raywenderlich ios-7-best-practices-part-2,去除了跟主题无关的寒暄部分。
 
开始
你有两个选择开始本教程:您可以使用在本教程的第1部分你已完成的项目,或者你可以在这里下载第1部分已完成的项目
 
在前面的教程中你创建了你的App的天气模型 – 现在你需要使用OpenWeatherMap API为你的App来获取一些数据。你将使用两个类抽象数据抓取、分析、存储:WXClient和WXManager。
 
WXClient的唯一责任是创建API请求,并解析它们;别人可以不用担心用数据做什么以及如何存储它。划分类的不同工作职责的设计模式被称为关注点分离。这使你的代码更容易理解,扩展和维护。
 
与ReactiveCocoa工作
 
确保你使用SimpleWeather.xcworkspace,打开WXClient.h并增加imports
  1. @import CoreLocation; 
  2. #import <ReactiveCocoa/ReactiveCocoa/ReactiveCocoa.h> 
 
  1. 注意:您可能之前没有见过的@import指令,它在Xcode5中被引入,是由苹果公司看作是一个现代的,更高效的替代 #import。有一个非常好的教程,涵盖了最新的Objective-C特性-[What’s New in Objective-C and Foundation in iOS 7](http://www.raywenderlich.com/49850/whats-new-in-objective-c-and-foundation-in-ios-7)。 
在WXClient.h中添加下列四个方法到接口申明:
  1. @import Foundation; 
  2. - (RACSignal *)fetchJSONFromURL:(NSURL *)url; 
  3. - (RACSignal *)fetchCurrentConditionsForLocation:(CLLocationCoordinate2D)coordinate; 
  4. - (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate; 
  5. - (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate; 
 
现在,似乎是一个很好的机会来介绍ReactiveCocoa
 
ReactiveCocoa(RAC)是一个Objective-C的框架,用于函数式反应型编程,它提供了组合和转化数据流的API。代替专注于编写串行的代码 – 执行有序的代码队列 – 可以响应非确定性事件。
 
1.对未来数据的进行组合操作的能力。
2.减少状态和可变性。
3.用声明的形式来定义行为和属性之间的关系。
4.为异步操作带来一个统一的,高层次的接口。
5.在KVO的基础上建立一个优雅的API。
 
例如,你可以监听username属性的变化,用这样的代码:
  1. [RACAble(self.username) subscribeNext:^(NSString *newName) { 
  2.     NSLog(@"%@", newName); 
  3. }]; 
subscribeNext这个block会在self.username属性变化的时候执行。新的值会传递给这个block。
 
您还可以合并信号并组合数据到一个组合数据中。下面的示例取自于ReactiveCocoa的Github页面:
  1. [[RACSignal 
  2.     combineLatest:@[ RACAble(self.password), RACAble(self.passwordConfirmation) ] 
  3.            reduce:^(NSString *currentPassword, NSString *currentConfirmPassword) { 
  4.                return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]]; 
  5.            }] 
  6.     subscribeNext:^(NSNumber *passwordsMatch) { 
  7.         self.createEnabled = [passwordsMatch boolValue]; 
  8.     }]; 
 
RACSignal对象捕捉当前和未来的值。信号可以被观察者链接,组合和反应。信号实际上不会执行,直到它被订阅。
 
这意味着调用[mySignal fetchCurrentConditionsForLocation:someLocation];不会做什么,但创建并返回一个信号。你将看到之后如何订阅和反应。
 
打开WXClient.m加入以下imports:
  1. #import "WXCondition.h" 
  2. #import "WXDailyForecast.h" 
 
在imports下,添加私有接口:
  1. @interface WXClient () 
  2.  
  3. @property (nonatomic, strong) NSURLSession *session; 
  4.  
  5. @end 
 
这个接口用这个属性来管理API请求的URL session。
 
添加以下init放到到@implementation和@end之间:
  1. - (id)init { 
  2.     if (self = [super init]) { 
  3.         NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; 
  4.         _session = [NSURLSession sessionWithConfiguration:config]; 
  5.     } 
  6.     return self; 
 
使用defaultSessionConfiguration为您创建session。
  1. 注意:如果你以前没有了解过NSURLSession,看看我们的[NSURLSession教程](http://www.raywenderlich.com/51127/nsurlsession-tutorial),了解更多信息。 
 
构建信号
你需要一个主方法来建立一个信号从URL中取数据。你已经知道,需要三种方法来获取当前状况,逐时预报及每日预报。
 
不是写三个独立的方法,你可以遵守DRY(Don’t Repeat Yourself)的软件设计理念,使您的代码容易维护。
 
第一次看,以下的一些ReactiveCocoa部分可能看起来相当陌生。别担心,你会一块一块理解他。
 
增加下列方法到WXClient.m:
  1. - (RACSignal *)fetchJSONFromURL:(NSURL *)url { 
  2.     NSLog(@"Fetching: %@",url.absoluteString); 
  3.  
  4.     // 1 
  5.     return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  6.         // 2 
  7.         NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 
  8.             // TODO: Handle retrieved data 
  9.         }]; 
  10.  
  11.         // 3 
  12.         [dataTask resume]; 
  13.  
  14.         // 4 
  15.         return [RACDisposable disposableWithBlock:^{ 
  16.             [dataTask cancel]; 
  17.         }]; 
  18.     }] doError:^(NSError *error) { 
  19.         // 5 
  20.         NSLog(@"%@",error); 
  21.     }]; 
 
通过一个一个注释,你会看到代码执行以下操作:
1.返回信号。请记住,这将不会执行,直到这个信号被订阅。 - fetchJSONFromURL:创建一个对象给其他方法和对象使用;这种行为有时也被称为工厂模式
2.创建一个NSURLSessionDataTask(在iOS7中加入)从URL取数据。你会在以后添加的数据解析。
3.一旦订阅了信号,启动网络请求。
4.创建并返回RACDisposable对象,它处理当信号摧毁时的清理工作。
5.增加了一个“side effect”,以记录发生的任何错误。side effect不订阅信号,相反,他们返回被连接到方法链的信号。你只需添加一个side effect来记录错误。
 
  1. 如果你觉得需要更多一些背景知识,看看由Ash Furrow编写的[这篇文章](http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/),以便更好地了解ReactiveCocoa的核心概念。 
在-fetchJSONFromURL:中找到// TODO: Handle retrieved data ,替换为:
  1. if (! error) { 
  2.     NSError *jsonError = nil; 
  3.     id json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError]; 
  4.     if (! jsonError) { 
  5.         // 1 
  6.         [subscriber sendNext:json]; 
  7.     } 
  8.     else { 
  9.         // 2 
  10.         [subscriber sendError:jsonError]; 
  11.     } 
  12. else { 
  13.     // 2 
  14.     [subscriber sendError:error]; 
  15.  
  16. // 3 
  17. [subscriber sendCompleted]; 
 
1.当JSON数据存在并且没有错误,发送给订阅者序列化后的JSON数组或字典。
2.在任一情况下如果有一个错误,通知订阅者。
3.无论该请求成功还是失败,通知订阅者请求已经完成。
 
-fetchJSONFromURL:方法有点长,但它使你的特定的API请求方法变得很简单。
 
获取当前状况
还在WXClient.m中,添加如下方法:
  1. - (RACSignal *)fetchCurrentConditionsForLocation:(CLLocationCoordinate2D)coordinate { 
  2.     // 1 
  3.     NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&units=imperial",coordinate.latitude, coordinate.longitude]; 
  4.     NSURL *url = [NSURL URLWithString:urlString]; 
  5.  
  6.     // 2 
  7.     return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) { 
  8.         // 3 
  9.         return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:json error:nil]; 
  10.     }]; 
 1.使用CLLocationCoordinate2D对象的经纬度数据来格式化URL。
2.用你刚刚建立的创建信号的方法。由于返回值是一个信号,你可以调用其他ReactiveCocoa的方法。 在这里,您将返回值映射到一个不同的值 – 一个NSDictionary实例。
3.使用MTLJSONAdapter来转换JSON到WXCondition对象 – 使用MTLJSONSerializing协议创建的WXCondition。

获取逐时预报
现在添加根据坐标获取逐时预报的方法到WXClient.m:
  1. - (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate { 
  2.     NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&units=imperial&cnt=12",coordinate.latitude, coordinate.longitude]; 
  3.     NSURL *url = [NSURL URLWithString:urlString]; 
  4.  
  5.     // 1 
  6.     return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) { 
  7.         // 2 
  8.         RACSequence *list = [json[@"list"] rac_sequence]; 
  9.  
  10.         // 3 
  11.         return [[list map:^(NSDictionary *item) { 
  12.             // 4 
  13.             return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:item error:nil]; 
  14.         // 5 
  15.         }] array]; 
  16.     }]; 
 
1.再次使用-fetchJSONFromUR方法,映射JSON。注意:重复使用该方法节省了多少代码!
2.使用JSON的”list”key创建RACSequence。 RACSequences让你对列表进行ReactiveCocoa操作。
3.映射新的对象列表。调用-map:方法,针对列表中的每个对象,返回新对象的列表。
4.再次使用MTLJSONAdapter来转换JSON到WXCondition对象。
 
使用RACSequence的-map方法,返回另一个RACSequence,所以用这个简便的方法来获得一个NSArray数据。
 
获取每日预报
最后,添加如下方法到WXClient.m:
  1. - (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate { 
  2.     NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/forecast/daily?lat=%f&lon=%f&units=imperial&cnt=7",coordinate.latitude, coordinate.longitude]; 
  3.     NSURL *url = [NSURL URLWithString:urlString]; 
  4.  
  5.     // Use the generic fetch method and map results to convert into an array of Mantle objects 
  6.     return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) { 
  7.         // Build a sequence from the list of raw JSON 
  8.         RACSequence *list = [json[@"list"] rac_sequence]; 
  9.  
  10.         // Use a function to map results from JSON to Mantle objects 
  11.         return [[list map:^(NSDictionary *item) { 
  12.             return [MTLJSONAdapter modelOfClass:[WXDailyForecast class] fromJSONDictionary:item error:nil]; 
  13.         }] array]; 
  14.     }]; 
 
是不是看起来很熟悉?是的,这个方法与-fetchHourlyForecastForLocation:方法非常像。除了它使用WXDailyForecast代替WXCondition,并获取每日预报。
 
构建并运行您的App,现在你不会看到任何新的东西,但这是一个很好机会松一口气,并确保没有任何错误或警告。
 
 
管理并存储你的数据
现在是时间来充实WXManager,这个类会把所有东西结合到一起。这个类实现您App的一些关键功能:
1.它使用单例设计模式
2.它试图找到设备的位置。
3.找到位置后,它获取相应的气象数据。
 
打开WXManager.h使用以下代码来替换其内容:
  1. @import Foundation; 
  2. @import CoreLocation; 
  3. #import <ReactiveCocoa/ReactiveCocoa/ReactiveCocoa.h> 
  4. // 1 
  5. #import "WXCondition.h" 
  6.  
  7. @interface WXManager : NSObject 
  8. <CLLocationManagerDelegate> 
  9.  
  10. // 2 
  11. + (instancetype)sharedManager; 
  12.  
  13. // 3 
  14. @property (nonatomic, strong, readonly) CLLocation *currentLocation; 
  15. @property (nonatomic, strong, readonly) WXCondition *currentCondition; 
  16. @property (nonatomic, strong, readonly) NSArray *hourlyForecast; 
  17. @property (nonatomic, strong, readonly) NSArray *dailyForecast; 
  18.  
  19. // 4 
  20. - (void)findCurrentLocation; 
  21.  
  22. @end 
 
1.请注意,你没有引入WXDailyForecast.h,你会始终使用WXCondition作为预报的类。 WXDailyForecast的存在是为了帮助Mantle转换JSON到Objective-C。
2.使用instancetype而不是WXManager,子类将返回适当的类型。
3.这些属性将存储您的数据。由于WXManager是一个单例,这些属性可以任意访问。设置公共属性为只读,因为只有管理者能更改这些值。
4.这个方法启动或刷新整个位置和天气的查找过程。
 
现在打开WXManager.m并添加如下imports到文件顶部:
  1. #import "WXClient.h" 
  2. #import <TSMessages/TSMessage.h> 
在imports下方,粘贴如下私有接口:
  1. @interface WXManager () 
  2.  
  3. // 1 
  4. @property (nonatomic, strong, readwrite) WXCondition *currentCondition; 
  5. @property (nonatomic, strong, readwrite) CLLocation *currentLocation; 
  6. @property (nonatomic, strong, readwrite) NSArray *hourlyForecast; 
  7. @property (nonatomic, strong, readwrite) NSArray *dailyForecast; 
  8.  
  9. // 2 
  10. @property (nonatomic, strong) CLLocationManager *locationManager; 
  11. @property (nonatomic, assign) BOOL isFirstUpdate; 
  12. @property (nonatomic, strong) WXClient *client; 
  13.  
  14. @end 
 
1.声明你在公共接口中添加的相同的属性,但是这一次把他们定义为可读写,因此您可以在后台更改他们。
2.为查找定位和数据抓取声明一些私有变量。
 
添加如下通用的单例构造器到@implementation与@endå中间:
  1. + (instancetype)sharedManager { 
  2.     static id _sharedManager = nil; 
  3.     static dispatch_once_t onceToken; 
  4.     dispatch_once(&onceToken, ^{ 
  5.         _sharedManager = [[self alloc] init]; 
  6.     }); 
  7.  
  8.     return _sharedManager; 
 
然后,你需要设置你的属性和观察者。
 
添加如下方法到WXManager.m:
  1. - (id)init { 
  2.     if (self = [super init]) { 
  3.         // 1 
  4.         _locationManager = [[CLLocationManager alloc] init]; 
  5.         _locationManager.delegate = self; 
  6.  
  7.         // 2 
  8.         _client = [[WXClient alloc] init]; 
  9.  
  10.         // 3 
  11.         [[[[RACObserve(self, currentLocation) 
  12.             // 4 
  13.             ignore:nil] 
  14.             // 5 
  15.            // Flatten and subscribe to all 3 signals when currentLocation updates 
  16.            flattenMap:^(CLLocation *newLocation) { 
  17.                return [RACSignal merge:@[ 
  18.                                          [self updateCurrentConditions], 
  19.                                          [self updateDailyForecast], 
  20.                                          [self updateHourlyForecast] 
  21.                                          ]]; 
  22.             // 6 
  23.            }] deliverOn:RACScheduler.mainThreadScheduler] 
  24.            // 7 
  25.          subscribeError:^(NSError *error) { 
  26.              [TSMessage showNotificationWithTitle:@"Error" 
  27.                                          subtitle:@"There was a problem fetching the latest weather." 
  28.                                              type:TSMessageNotificationTypeError]; 
  29.          }]; 
  30.     } 
  31.     return self; 
 
你正使用更多的ReactiveCocoa方法来观察和反应数值的变化。上面这些你做了:
 
1.创建一个位置管理器,并设置它的delegate为self。
2.为管理器创建WXClient对象。这里处理所有的网络请求和数据分析,这是关注点分离的最佳实践。
3.管理器使用一个返回信号的ReactiveCocoa脚本来观察自身的currentLocation。这与KVO类似,但更为强大。
4.为了继续执行方法链,currentLocation必须不为nil。
5.- flattenMap:非常类似于-map:,但不是映射每一个值,它把数据变得扁平,并返回包含三个信号中的一个对象。通过这种方式,你可以考虑将三个进程作为单个工作单元。
6.将信号传递给主线程上的观察者。
7.这不是很好的做法,在你的模型中进行UI交互,但出于演示的目的,每当发生错误时,会显示一个banner。
 
接下来,为了显示准确的天气预报,我们需要确定设备的位置。
 
查找你的位置
下一步,你要添加当位置查找到,触发抓取天气数据的代码。
 
添加如下代码到WXManager.m的实现块中:
  1. - (void)findCurrentLocation { 
  2.     self.isFirstUpdate = YES; 
  3.     [self.locationManager startUpdatingLocation]; 
  4.  
  5. - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations { 
  6.     // 1 
  7.     if (self.isFirstUpdate) { 
  8.         self.isFirstUpdate = NO; 
  9.         return
  10.     } 
  11.  
  12.     CLLocation *location = [locations lastObject]; 
  13.  
  14.     // 2 
  15.     if (location.horizontalAccuracy > 0) { 
  16.         // 3 
  17.         self.currentLocation = location; 
  18.         [self.locationManager stopUpdatingLocation]; 
  19.     } 
1.忽略第一个位置更新,因为它一般是缓存值。
2.一旦你获得一定精度的位置,停止进一步的更新。
3.设置currentLocation,将触发您之前在init中设置的RACObservable。
 
获取气象数据
最后,是时候添加在客户端上调用并保存数据的三个获取方法。将三个方法捆绑起来,被之前在init方法中添加的RACObservable订阅。您将返回客户端返回的,能被订阅的,相同的信号。
 
所有的属性设置发生在-doNext:中。
 
添加如下代码到WXManager.m:
  1. - (RACSignal *)updateCurrentConditions { 
  2.     return [[self.client fetchCurrentConditionsForLocation:self.currentLocation.coordinate] doNext:^(WXCondition *condition) { 
  3.         self.currentCondition = condition; 
  4.     }]; 
  5.  
  6. - (RACSignal *)updateHourlyForecast { 
  7.     return [[self.client fetchHourlyForecastForLocation:self.currentLocation.coordinate] doNext:^(NSArray *conditions) { 
  8.         self.hourlyForecast = conditions; 
  9.     }]; 
  10.  
  11. - (RACSignal *)updateDailyForecast { 
  12.     return [[self.client fetchDailyForecastForLocation:self.currentLocation.coordinate] doNext:^(NSArray *conditions) { 
  13.         self.dailyForecast = conditions; 
  14.     }]; 
 
它看起来像将一切都连接起来,并蓄势待发。别急!这App实际上并没有告诉管理者做任何事情。 打开WXController.m并导入这管理者到文件的顶部,如下所示:
  1. #import "WXManager.h" 
 
添加如下代码到-viewDidLoad:的最后:
  1. [[WXManager sharedManager] findCurrentLocation]; 
 
这告诉管理类,开始寻找设备的当前位置。
 
构建并运行您的App,系统会提示您是否允许使用位置服务。你仍然不会看到任何UI的更新,但检查控制台日志,你会看到类似以下内容:
  1. 2013-11-05 08:38:48.886 WeatherTutorial[17097:70b] Fetching: http://api.openweathermap.org/data/2.5/weather?lat=37.785834&lon=-122.406417&units=imperial 
  2. 2013-11-05 08:38:48.886 WeatherTutorial[17097:70b] Fetching: http://api.openweathermap.org/data/2.5/forecast/daily?lat=37.785834&lon=-122.406417&units=imperial&cnt=7 
  3. 2013-11-05 08:38:48.886 WeatherTutorial[17097:70b] Fetching: http://api.openweathermap.org/data/2.5/forecast?lat=37.785834&lon=-122.406417&units=imperial&cnt=12 
 
这些输出代表你的代码工作正常,网络请求正常执行。

连接接口
这是最后一次展示所有获取,映射和存储的数据。您将使用ReactiveCocoa来观察WXManager单例的变化和当新数据到达时更新界面。
 
还在WXController.m,到- viewDidLoad的底部,并添加下面的代码到[[WXManager sharedManager] findCurrentLocation];之前:
  1. // 1 
  2. [[RACObserve([WXManager sharedManager], currentCondition) 
  3.   // 2 
  4.   deliverOn:RACScheduler.mainThreadScheduler] 
  5.  subscribeNext:^(WXCondition *newCondition) { 
  6.      // 3 
  7.      temperatureLabel.text = [NSString stringWithFormat:@"%.0f°",newCondition.temperature.floatValue]; 
  8.      conditionsLabel.text = [newCondition.condition capitalizedString]; 
  9.      cityLabel.text = [newCondition.locationName capitalizedString]; 
  10.  
  11.      // 4 
  12.      iconView.image = [UIImage imageNamed:[newCondition imageName]]; 
  13.  }]; 
 
1.观察WXManager单例的currentCondition。
2.传递在主线程上的任何变化,因为你正在更新UI。
3.使用气象数据更新文本标签;你为文本标签使用newCondition的数据,而不是单例。订阅者的参数保证是最新值。
4.使用映射的图像文件名来创建一个图像,并将其设置为视图的图标。
 
构建并运行您的App,你会看到当前温度,当前状况和表示当前状况的图标。所有的数据都是实时的。但是,如果你的位置是旧金山,它似乎总是约65度。Lucky San Franciscans! :]
ReactiveCocoa的绑定
ReactiveCocoa为iOS带来了自己的Cocoa绑定的形式。
 
不知道是什么绑定?简而言之,他们是一种提供了保持模型和视图的数据同步而无需编写大量”胶水代码”的手段,它们允许你建立一个视图和数据块之间的连接, “结合”它们,使得一方的变化反映到另一个中的技术。
 
这是一个非常强大的概念,不是吗?
  1. 注意:要获得更多的绑定实例代码,请查看[ReactiveCocoa Readme](https://github.com/ReactiveCocoa/ReactiveCocoa)。 
 
添加如下代码到你上一步添加的代码后面:
  1. // 1 
  2. RAC(hiloLabel, text) = [[RACSignal combineLatest:@[ 
  3.                         // 2 
  4.                         RACObserve([WXManager sharedManager], currentCondition.tempHigh), 
  5.                         RACObserve([WXManager sharedManager], currentCondition.tempLow)] 
  6.                         // 3 
  7.                         reduce:^(NSNumber *hi, NSNumber *low) { 
  8.                             return [NSString  stringWithFormat:@"%.0f° / %.0f°",hi.floatValue,low.floatValue]; 
  9.                         }] 
  10.                         // 4 
  11.                         deliverOn:RACScheduler.mainThreadScheduler]; 
上面的代码结合高温、低温的值到hiloLabel的text属性。看看你完成了什么:
 
1.RAC(…)宏有助于保持语法整洁。从该信号的返回值将被分配给hiloLabel对象的text。
2.观察currentCondition的高温和低温。合并信号,并使用两者最新的值。当任一数据变化时,信号就会触发。
3.从合并的信号中,减少数值,转换成一个单一的数据,注意参数的顺序与信号的顺序相匹配。
4.同样,因为你正在处理UI界面,所以把所有东西都传递到主线程。
 
构建并运行你的App。你应该看到在左下方的高/低温度label更新了:
在Table View中显示数据
现在,你已经获取所有的数据,你可以在table view中整齐地显示出来。你会在分页的table view中显示最近6小时的每时播报和每日预报。该App会显示三个页面:一个是当前状况,一个是逐时预报,以及一个每日预报。
 
之前,你可以添加单元格到table view,你需要初始化和配置一些日期格式化。
 
到WXController.m最顶端的私有接口处,添加下列两个属性
  1. @property (nonatomic, strong) NSDateFormatter *hourlyFormatter; 
  2. @property (nonatomic, strong) NSDateFormatter *dailyFormatter; 
 
由于创建日期格式化非常昂贵,我们将在init方法中实例化他们,并使用这些变量去存储他们的引用。
 
还在WXController.m中,添加如下代码到@implementation中:
  1. - (id)init { 
  2.     if (self = [super init]) { 
  3.         _hourlyFormatter = [[NSDateFormatter alloc] init]; 
  4.         _hourlyFormatter.dateFormat = @"h a"
  5.  
  6.         _dailyFormatter = [[NSDateFormatter alloc] init]; 
  7.         _dailyFormatter.dateFormat = @"EEEE"
  8.     } 
  9.     return self; 
 
你可能想知道为什么在-init中初始化这些日期格式化,而不是在-viewDidLoad中初始化他们。好问题!
 
实际上-viewDidLoad可以在一个视图控制器的生命周期中多次调用。 NSDateFormatter对象的初始化是昂贵的,而将它们放置在你的-init,会确保被你的视图控制器初始化一次。
 
在WXController.m中,寻找tableView:numberOfRowsInSection:,并用如下代码更换TODO到return:
  1. // 1 
  2. if (section == 0) { 
  3.     return MIN([[WXManager sharedManager].hourlyForecast count], 6) + 1; 
  4. // 2 
  5. return MIN([[WXManager sharedManager].dailyForecast count], 6) + 1; 
 
1.第一部分是对的逐时预报。使用最近6小时的预预报,并添加了一个作为页眉的单元格。
2.接下来的部分是每日预报。使用最近6天的每日预报,并添加了一个作为页眉的单元格。
 
  1. 注意:您使用表格单元格作为标题,而不是内置的、具有粘性的滚动行为的标题。这个table view设置了分页,粘性滚动行为看起来会很奇怪。 
在WXController.m找到tableView:cellForRowAtIndexPath:,并用如下代码更换TODO:
  1. if (indexPath.section == 0) { 
  2.     // 1 
  3.     if (indexPath.row == 0) { 
  4.         [self configureHeaderCell:cell title:@"Hourly Forecast"]; 
  5.     } 
  6.     else { 
  7.         // 2 
  8.         WXCondition *weather = [WXManager sharedManager].hourlyForecast[indexPath.row - 1]; 
  9.         [self configureHourlyCell:cell weather:weather]; 
  10.     } 
  11. else if (indexPath.section == 1) { 
  12.     // 1 
  13.     if (indexPath.row == 0) { 
  14.         [self configureHeaderCell:cell title:@"Daily Forecast"]; 
  15.     } 
  16.     else { 
  17.         // 3 
  18.         WXCondition *weather = [WXManager sharedManager].dailyForecast[indexPath.row - 1]; 
  19.         [self configureDailyCell:cell weather:weather]; 
  20.     } 
 
1.每个部分的第一行是标题单元格。
2.获取每小时的天气和使用自定义配置方法配置cell。
3.获取每天的天气,并使用另一个自定义配置方法配置cell。
 
最后,添加如下代码到WXController.m:
  1. // 1 
  2. - (void)configureHeaderCell:(UITableViewCell *)cell title:(NSString *)title { 
  3.     cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18]; 
  4.     cell.textLabel.text = title; 
  5.     cell.detailTextLabel.text = @""
  6.     cell.imageView.image = nil; 
  7.  
  8. // 2 
  9. - (void)configureHourlyCell:(UITableViewCell *)cell weather:(WXCondition *)weather { 
  10.     cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18]; 
  11.     cell.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18]; 
  12.     cell.textLabel.text = [self.hourlyFormatter stringFromDate:weather.date]; 
  13.     cell.detailTextLabel.text = [NSString stringWithFormat:@"%.0f°",weather.temperature.floatValue]; 
  14.     cell.imageView.image = [UIImage imageNamed:[weather imageName]]; 
  15.     cell.imageView.contentMode = UIViewContentModeScaleAspectFit; 
  16.  
  17. // 3 
  18. - (void)configureDailyCell:(UITableViewCell *)cell weather:(WXCondition *)weather { 
  19.     cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18]; 
  20.     cell.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18]; 
  21.     cell.textLabel.text = [self.dailyFormatter stringFromDate:weather.date]; 
  22.     cell.detailTextLabel.text = [NSString stringWithFormat:@"%.0f° / %.0f°"
  23.                                   weather.tempHigh.floatValue, 
  24.                                   weather.tempLow.floatValue]; 
  25.     cell.imageView.image = [UIImage imageNamed:[weather imageName]]; 
  26.     cell.imageView.contentMode = UIViewContentModeScaleAspectFit; 
 
1.配置和添加文本到作为section页眉单元格。你会重用此为每日每时的预测部分。
2.格式化逐时预报的单元格。
3.格式化每日预报的单元格。
 
构建并运行您的App,尝试滚动你的table view,并…等一下。什么都没显示!怎么办?
 
如果你已经使用过的UITableView,可能你之前遇到过问题。这个table没有重新加载!
 
为了解决这个问题,你需要添加另一个针对每时预报和每日预报属性的ReactiveCocoa观察。
 
在WXController.m的-viewDidLoad中,添加下列代码到其他ReactiveCocoa观察代码中:
  1. [[RACObserve([WXManager sharedManager], hourlyForecast) 
  2.        deliverOn:RACScheduler.mainThreadScheduler] 
  3.    subscribeNext:^(NSArray *newForecast) { 
  4.        [self.tableView reloadData]; 
  5.    }]; 
  6.  
  7. [[RACObserve([WXManager sharedManager], dailyForecast) 
  8.        deliverOn:RACScheduler.mainThreadScheduler] 
  9.    subscribeNext:^(NSArray *newForecast) { 
  10.        [self.tableView reloadData]; 
  11.    }]; 
 
构建并运行App;滚动table view,你将看到填充的所有预报数据。
 
给你的App添加效果
本页面为每时和每日预报不会占满整个屏幕。幸运的是,有一个非常简单的修复办法。在本教程前期,您在-viewDidLoad中获得屏幕高度。
 
在WXController.m中,查找table view的委托方法-tableView:heightForRowAtIndexPath:,并且替换TODO到return的代码:
  1. NSInteger cellCount = [self tableView:tableView numberOfRowsInSection:indexPath.section]; 
  2. return self.screenHeight / (CGFloat)cellCount; 
屏幕高度由一定数量的cell所分割,所以所有cell的总高度等于屏幕的高度。
 
构建并运行你的App;table view填满了整个屏幕,如下所示:
最后要做的是把我在本教程的第一部分开头提到的模糊效果引入。当你滚动预报页面,模糊效果应该动态显示。
 
添加下列scroll delegate到WXController.m最底部:
  1. #pragma mark - UIScrollViewDelegate 
  2.  
  3. - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 
  4.     // 1 
  5.     CGFloat height = scrollView.bounds.size.height; 
  6.     CGFloat position = MAX(scrollView.contentOffset.y, 0.0); 
  7.     // 2 
  8.     CGFloat percent = MIN(position / height, 1.0); 
  9.     // 3 
  10.     self.blurredImageView.alpha = percent; 
 
1.获取滚动视图的高度和内容偏移量。与0偏移量做比较,因此试图滚动table低于初始位置将不会影响模糊效果。
2.偏移量除以高度,并且最大值为1,所以alpha上限为1。
3.当你滚动的时候,把结果值赋给模糊图像的alpha属性,来更改模糊图像。
 
构建并运行App,滚动你的table view,并查看这令人惊异的模糊效果: 
何去何从?
在本教程中你已经完成了很多内容:您使用CocoaPods创建了一个项目,完全用代码书写了一个视图结构,创建数据模型和管理类,并使用函数式编程将他们连接到一起!
 
您可以从这里下载该项目的完成版本。
 
这个App还有很多酷的东西可以去做。一个好的开始是使用Flickr API来查找基于设备位置的背景图像。
 
还有,你的应用程序只处理温度和状态;有什么其他的天气信息能融入你的App?