iOS7 最佳实践:一个天气应用案例(下)

开始

你有两个选择开始本教程:您可以使用在本教程的第1部分你已完成的项目,或者你可以在这里下载第1部分已完成的项目

在前面的教程中你创建了你的App的天气模型 – 现在你需要使用OpenWeatherMap API为你的App来获取一些数据。你将使用两个类抽象数据抓取、分析、存储:WXClientWXManager

WXClient的唯一责任是创建API请求,并解析它们;别人可以不用担心用数据做什么以及如何存储它。划分类的不同工作职责的设计模式被称为关注点分离。这使你的代码更容易理解,扩展和维护。

与ReactiveCocoa工作

确保你使用SimpleWeather.xcworkspace,打开WXClient.h并增加imports

WXClient.h中添加下列四个方法到接口申明:

现在,似乎是一个很好的机会来介绍ReactiveCocoa

ReactiveCocoa(RAC)是一个Objective-C的框架,用于函数式反应型编程,它提供了组合和转化数据流的API。代替专注于编写串行的代码 – 执行有序的代码队列 – 可以响应非确定性事件。

Github上提供的a great overview of the benefits

  • 对未来数据的进行组合操作的能力。
  • 减少状态和可变性。
  • 用声明的形式来定义行为和属性之间的关系。
  • 为异步操作带来一个统一的,高层次的接口。
  • 在KVO的基础上建立一个优雅的API。

例如,你可以监听username属性的变化,用这样的代码:

subscribeNext这个block会在self.username属性变化的时候执行。新的值会传递给这个block。

您还可以合并信号并组合数据到一个组合数据中。下面的示例取自于ReactiveCocoa的Github页面:

RACSignal对象捕捉当前和未来的值。信号可以被观察者链接,组合和反应。信号实际上不会执行,直到它被订阅。

这意味着调用[mySignal fetchCurrentConditionsForLocation:someLocation];不会做什么,但创建并返回一个信号。你将看到之后如何订阅和反应。

打开WXClient.m加入以下imports:

在imports下,添加私有接口:

这个接口用这个属性来管理API请求的URL session。

添加以下init放到到@implementation@end之间:

使用defaultSessionConfiguration为您创建session。

构建信号

你需要一个主方法来建立一个信号从URL中取数据。你已经知道,需要三种方法来获取当前状况,逐时预报及每日预报。

不是写三个独立的方法,你可以遵守DRY(Don’t Repeat Yourself)的软件设计理念,使您的代码容易维护。

第一次看,以下的一些ReactiveCocoa部分可能看起来相当陌生。别担心,你会一块一块理解他。

增加下列方法到WXClient.m:

通过一个一个注释,你会看到代码执行以下操作:

  1. 返回信号。请记住,这将不会执行,直到这个信号被订阅。 - fetchJSONFromURL:创建一个对象给其他方法和对象使用;这种行为有时也被称为工厂模式
  2. 创建一个NSURLSessionDataTask(在iOS7中加入)从URL取数据。你会在以后添加的数据解析。
  3. 一旦订阅了信号,启动网络请求。
  4. 创建并返回RACDisposable对象,它处理当信号摧毁时的清理工作。
  5. 增加了一个“side effect”,以记录发生的任何错误。side effect不订阅信号,相反,他们返回被连接到方法链的信号。你只需添加一个side effect来记录错误。

-fetchJSONFromURL:中找到// TODO: Handle retrieved data ,替换为:

  1. 当JSON数据存在并且没有错误,发送给订阅者序列化后的JSON数组或字典。
  2. 在任一情况下如果有一个错误,通知订阅者。
  3. 无论该请求成功还是失败,通知订阅者请求已经完成。

-fetchJSONFromURL:方法有点长,但它使你的特定的API请求方法变得很简单。

获取当前状况

还在WXClient.m中,添加如下方法:

  1. 使用CLLocationCoordinate2D对象的经纬度数据来格式化URL。
  2. 用你刚刚建立的创建信号的方法。由于返回值是一个信号,你可以调用其他ReactiveCocoa的方法。 在这里,您将返回值映射到一个不同的值 – 一个NSDictionary实例。
  3. 使用MTLJSONAdapter来转换JSON到WXCondition对象 – 使用MTLJSONSerializing协议创建的WXCondition

获取逐时预报

现在添加根据坐标获取逐时预报的方法到WXClient.m:

 

  1. 再次使用-fetchJSONFromUR方法,映射JSON。注意:重复使用该方法节省了多少代码!
  2. 使用JSON的”list”key创建RACSequence。 RACSequences让你对列表进行ReactiveCocoa操作。
  3. 映射新的对象列表。调用-map:方法,针对列表中的每个对象,返回新对象的列表。
  4. 再次使用MTLJSONAdapter来转换JSON到WXCondition对象。
  5. 使用RACSequence-map方法,返回另一个RACSequence,所以用这个简便的方法来获得一个NSArray数据。

获取每日预报

最后,添加如下方法到WXClient.m:

是不是看起来很熟悉?是的,这个方法与-fetchHourlyForecastForLocation:方法非常像。除了它使用WXDailyForecast代替WXCondition,并获取每日预报。

构建并运行您的App,现在你不会看到任何新的东西,但这是一个很好机会松一口气,并确保没有任何错误或警告。

builregergerggrret-layout

管理并存储你的数据

现在是时间来充实WXManager,这个类会把所有东西结合到一起。这个类实现您App的一些关键功能:

  • 它使用单例设计模式
  • 它试图找到设备的位置。
  • 找到位置后,它获取相应的气象数据。

打开WXManager.h使用以下代码来替换其内容:

  1. 请注意,你没有引入WXDailyForecast.h,你会始终使用WXCondition作为预报的类。 WXDailyForecast的存在是为了帮助Mantle转换JSON到Objective-C。
  2. 使用instancetype而不是WXManager,子类将返回适当的类型。
  3. 这些属性将存储您的数据。由于WXManager是一个单例,这些属性可以任意访问。设置公共属性为只读,因为只有管理者能更改这些值。
  4. 这个方法启动或刷新整个位置和天气的查找过程。

现在打开WXManager.m并添加如下imports到文件顶部:

在imports下方,粘贴如下私有接口:

  1. 声明你在公共接口中添加的相同的属性,但是这一次把他们定义为可读写,因此您可以在后台更改他们。
  2. 为查找定位和数据抓取声明一些私有变量。

添加如下通用的单例构造器到@implementation@endå中间:

然后,你需要设置你的属性和观察者。

添加如下方法到WXManager.m:

你正使用更多的ReactiveCocoa方法来观察和反应数值的变化。上面这些你做了:

  1. 创建一个位置管理器,并设置它的delegate为self
  2. 为管理器创建WXClient对象。这里处理所有的网络请求和数据分析,这是关注点分离的最佳实践。
  3. 管理器使用一个返回信号的ReactiveCocoa脚本来观察自身的currentLocation。这与KVO类似,但更为强大。
  4. 为了继续执行方法链,currentLocation必须不为nil
  5. - flattenMap:非常类似于-map:,但不是映射每一个值,它把数据变得扁平,并返回包含三个信号中的一个对象。通过这种方式,你可以考虑将三个进程作为单个工作单元。
  6. 将信号传递给主线程上的观察者。
  7. 这不是很好的做法,在你的模型中进行UI交互,但出于演示的目的,每当发生错误时,会显示一个banner。

接下来,为了显示准确的天气预报,我们需要确定设备的位置。

查找你的位置

下一步,你要添加当位置查找到,触发抓取天气数据的代码。

添加如下代码到WXManager.m的实现块中:

  1. 忽略第一个位置更新,因为它一般是缓存值。
  2. 一旦你获得一定精度的位置,停止进一步的更新。
  3. 设置currentLocation,将触发您之前在init中设置的RACObservable。

获取气象数据

最后,是时候添加在客户端上调用并保存数据的三个获取方法。将三个方法捆绑起来,被之前在init方法中添加的RACObservable订阅。您将返回客户端返回的,能被订阅的,相同的信号。

所有的属性设置发生在-doNext:中。

添加如下代码到WXManager.m:

它看起来像将一切都连接起来,并蓄势待发。别急!这App实际上并没有告诉管理者做任何事情。 打开WXController.m并导入这管理者到文件的顶部,如下所示:

添加如下代码到-viewDidLoad:的最后:

这告诉管理类,开始寻找设备的当前位置。

构建并运行您的App,系统会提示您是否允许使用位置服务。你仍然不会看到任何UI的更新,但检查控制台日志,你会看到类似以下内容:

这些输出代表你的代码工作正常,网络请求正常执行。

连接接口

这是最后一次展示所有获取,映射和存储的数据。您将使用ReactiveCocoa来观察WXManager单例的变化和当新数据到达时更新界面。

还在WXController.m,到- viewDidLoad的底部,并添加下面的代码到[[WXManager sharedManager] findCurrentLocation];之前:

  1. 观察WXManager单例的currentCondition。
  2. 传递在主线程上的任何变化,因为你正在更新UI。
  3. 使用气象数据更新文本标签;你为文本标签使用newCondition的数据,而不是单例。订阅者的参数保证是最新值。
  4. 使用映射的图像文件名来创建一个图像,并将其设置为视图的图标。

构建并运行您的App,你会看到当前温度,当前状况和表示当前状况的图标。所有的数据都是实时的。但是,如果你的位置是旧金山,它似乎总是约65度。Lucky San Franciscans! :]

ureferfergfi-wiring

ReactiveCocoa的绑定

ReactiveCocoa为iOS带来了自己的Cocoa绑定的形式。

不知道是什么绑定?简而言之,他们是一种提供了保持模型和视图的数据同步而无需编写大量”胶水代码”的手段,它们允许你建立一个视图和数据块之间的连接, “结合”它们,使得一方的变化反映到另一个中的技术。

这是一个非常强大的概念,不是吗?

添加如下代码到你上一步添加的代码后面:

上面的代码结合高温、低温的值到hiloLabel的text属性。看看你完成了什么:

  1. RAC(…)宏有助于保持语法整洁。从该信号的返回值将被分配给hiloLabel对象的text
  2. 观察currentCondition的高温和低温。合并信号,并使用两者最新的值。当任一数据变化时,信号就会触发。
  3. 从合并的信号中,减少数值,转换成一个单一的数据,注意参数的顺序与信号的顺序相匹配。
  4. 同样,因为你正在处理UI界面,所以把所有东西都传递到主线程。

构建并运行你的App。你应该看到在左下方的高/低温度label更新了:

ui-wregerggriring-hilo

在Table View中显示数据

现在,你已经获取所有的数据,你可以在table view中整齐地显示出来。你会在分页的table view中显示最近6小时的每时播报和每日预报。该App会显示三个页面:一个是当前状况,一个是逐时预报,以及一个每日预报。

之前,你可以添加单元格到table view,你需要初始化和配置一些日期格式化。

WXController.m最顶端的私有接口处,添加下列两个属性

由于创建日期格式化非常昂贵,我们将在init方法中实例化他们,并使用这些变量去存储他们的引用。

还在WXController.m中,添加如下代码到@implementation中:

你可能想知道为什么在-init中初始化这些日期格式化,而不是在-viewDidLoad中初始化他们。好问题!

实际上-viewDidLoad可以在一个视图控制器的生命周期中多次调用。 NSDateFormatter对象的初始化是昂贵的,而将它们放置在你的-init,会确保被你的视图控制器初始化一次。

WXController.m中,寻找tableView:numberOfRowsInSection:,并用如下代码更换TODOreturn

  1. 第一部分是对的逐时预报。使用最近6小时的预预报,并添加了一个作为页眉的单元格。
  2. 接下来的部分是每日预报。使用最近6天的每日预报,并添加了一个作为页眉的单元格。

WXController.m找到tableView:cellForRowAtIndexPath:,并用如下代码更换TODO

  1. 每个部分的第一行是标题单元格。
  2. 获取每小时的天气和使用自定义配置方法配置cell。
  3. 获取每天的天气,并使用另一个自定义配置方法配置cell。

最后,添加如下代码到WXController.m:

  1. 配置和添加文本到作为section页眉单元格。你会重用此为每日每时的预测部分。
  2. 格式化逐时预报的单元格。
  3. 格式化每日预报的单元格。

构建并运行您的App,尝试滚动你的table view,并…等一下。什么都没显示!怎么办?

如果你已经使用过的UITableView,可能你之前遇到过问题。这个table没有重新加载!

为了解决这个问题,你需要添加另一个针对每时预报和每日预报属性的ReactiveCocoa观察。

WXController.m-viewDidLoad中,添加下列代码到其他ReactiveCocoa观察代码中:

构建并运行App;滚动table view,你将看到填充的所有预报数据。

unalifwefewfegned-heights

给你的App添加效果

本页面为每时和每日预报不会占满整个屏幕。幸运的是,有一个非常简单的修复办法。在本教程前期,您在-viewDidLoad中获得屏幕高度。

WXController.m中,查找table view的委托方法-tableView:heightForRowAtIndexPath:,并且替换TODOreturn的代码:

屏幕高度由一定数量的cell所分割,所以所有cell的总高度等于屏幕的高度。

构建并运行你的App;table view填满了整个屏幕,如下所示:

alignrefewfed-heights1

最后要做的是把我在本教程的第一部分开头提到的模糊效果引入。当你滚动预报页面,模糊效果应该动态显示。

添加下列scroll delegate到WXController.m最底部:

  1. 获取滚动视图的高度和内容偏移量。与0偏移量做比较,因此试图滚动table低于初始位置将不会影响模糊效果。
  2. 偏移量除以高度,并且最大值为1,所以alpha上限为1。
  3. 当你滚动的时候,把结果值赋给模糊图像的alpha属性,来更改模糊图像。

构建并运行App,滚动你的table view,并查看这令人惊异的模糊效果:

with-efewfweffblur

何去何从?

在本教程中你已经完成了很多内容:您使用CocoaPods创建了一个项目,完全用代码书写了一个视图结构,创建数据模型和管理类,并使用函数式编程将他们连接到一起!

您可以从这里下载该项目的完成版本。

这个App还有很多酷的东西可以去做。一个好的开始是使用Flickr API来查找基于设备位置的背景图像。

还有,你的应用程序只处理温度和状态;有什么其他的天气信息能融入你的App?

收藏 2 评论

相关文章

可能感兴趣的话题



直接登录
最新评论
  • 完整的项目下载下来运行,没有任何数据,是不是必须要在真机上面才能运行?

  • Glass heart ios开发 2015/12/16

    获取逐时预报和每日预报的方法

    //逐时预报
    - (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate {
    NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&units=imperial&cnt=12",coordinate.latitude, coordinate.longitude];
    NSURL *url = [NSURL URLWithString:urlString];
    return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
    RACSequence *list = [json[@"list"] rac_sequence];
    return [[list map:^(NSDictionary *item) {
    return [MTLJSONAdapter modelOfClass:[WXDailyForecast class] fromJSONDictionary:item error:nil];
    }] array];
    }];
    }

    //每日预报
    - (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate {
    NSString *urlString = [NSString stringWithFormat:@"http://api.openweathermap.org/data/2.5/forecast?lat=%f&lon=%f&units=imperial&cnt=12",coordinate.latitude, coordinate.longitude];
    NSURL *url = [NSURL URLWithString:urlString];

    return [[self fetchJSONFromURL:url] map:^(NSDictionary *json) {
    RACSequence *list = [json[@"list"] rac_sequence];

    return [[list map:^(NSDictionary *item) {
    return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:item error:nil];
    }] array];
    }];
    }

跳到底部
返回顶部