iOS 最佳实践 part2
在系列的第一部分,你通过Cocoapods设置了你的工程,为控制器添加了视图并且实际了它们,最终我们通过创建模型来反应天气的变化我们就可以完成一个吸引人的应用了。
在第二部分,我们会最终完成这个应用,在第二部分,我们会通过weatherAPI来完成接通UI来刷新数据。你将会学到ReactiveCocoa的使用语法,和非常依赖数据获取的和UI 更新事件。
让我们开始:
你有两个选择来开始这一部分的,你可以选择通过看第一部分来一步一步完成前面的代码,或者来这里http://cdn4.raywenderlich.com/wp-content/uploads/2013/11/SimpleWeather-Part-1.zip下载代码。
在这个辅导开始之前,我们需要创建为你的app创建天气模型,现在你需要通过OpenWeatherMapAPI来让你的app接收到数据。你将通过WXClient和WXManager这两个类来保存,你获取到并解析的数据。 WXClient的唯一责任就是创建API请求并且解析它们。
Working with ReactiveCocoa
确保你正在使用SimpleWeather.xcworkspace文件。打开WXClient.h文件,添加下面的导入语句
@import CoreLocation;
#import<ReactiveCocoa/ReactiveCocoa/ReactiveCocoa.h>
提示:也许你之前从来没有看到过@import指令,你还可以看看这个 What’s New in Objective-C and Foundation in iOS 7.
在WXClient.h: 中添加下面的四个公共借口声明方法
@import Foundation
- (RACSignal *)fetchJSONFormURL:(NSURL *)url;
- (RACSignal *)fetchCurrentConditionsForLocation:(CLLocationCoordinate2D)coordinate;
- (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate;
- (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate;
是的,在这上面也许有一个微小的事情你没有认出来
也许现在是介绍ReactiveCocoa一个很好的时间段。
ReactiveCocoa(RAC)是一个Objective-c的框架,用于响应事编程,用于组合和转换值序列的框架
在https://github.com/blog/1107-reactivecocoa-for-a-better-world这里提供了一份关于ReactiveCocoa非常好的介绍,也就是:
-
组成对未来数据的操作的能力.
-
一种以尽量减少转台.
-
声明的形式来定义行为和属性之间的关系.
-
统一的高级异步操作接口.
-
建立在KVO之上的一个可爱的API.
例如,你可以观察username属性的变化,像这样:
[RACAble(self.username) subscribeNext:^(NSString *newName){ NSLog(@“%@“,newName); }]
这个subscribeNext代码块被称为self.username变化时的值。新的值被传递到代码块中。
下面时ReactiveCocoa在github上的示例
[[RACSignal
combineLatest:@[ RACAble(self.password), RACAble(self.passwordConfirmation) ]
reduce:^(NSString *currentPassword, NSString *currentConfirmPassword) {
return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]]; }]
subscribeNext:^(NSNumber *passwordsMatch) {
self.createEnabled = [passwordsMatch boolValue]; }];
这个RACSignal
对象捕捉当前和未来值。信号可以被链接,结合反应给观察者。信号会不会执行将取决于它是否被同意执行。
就是说[mySignal fetchCurrentConditionsForLocation:someLocation];会被创建和返回一个信号,但是不会做其他事情。你将在稍后明白怎样订阅和反应
打开WXClient.m添加下面的导入头文件:
#import “WXCondition.h”
#import “WXDailyForecast.h”
在导入部分,添加私有接口:
@interface WXClient()
@property (nonatomic, strong)NSURLSession *session;
@end
找个接口有管理你的API请求的URL session单个属性。
在@implementation
和@end
:之间添加 init 方法
-(id)init {
if (self = [super init]){
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config];
}
return self;
}
这个为defaultSessionConfiguration
.简单的创建session
注意:如果你在这之前没有接触过NSURLSession你可以看看 NSURLSession tutorial
Building the Signals
你需要创建一个主方法来建立信号从而获取RUL 中的数据。你已经知道三种方法来返回信息,一个时当前的信息,一个时小时天气预报,一个时每天的天气预报。
用ReactiveCocoa代替三个独立的方法,首次使用也许会有些陌生,但是我们会一步一步来完成。
在WXClient.m添加下面的方法
- (RACSignal *)fetchJSONFromURL:(NSURL *)url{
NSLog(@“Fetching:%@”,url.absoluteString);
//1
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber>subscriber) {
//2
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHander:^(NSData *data,NSURLResponse *response,NSError *error)
{
//TODO:Handle retrieved data
}];
//3
[dataTask resume];
//4
return [RACDisposable disposableWithBlock:^{[dataTask cancel]; }];
}] doError:^(NSError *error){
//5
NSLog(“%@“,error);}];
}
通过一步步介绍,你会看到代码完成下面的事情
1.返回信号,但是记住,直到该信号被订阅才会执行。-fetchJSONFromURL:利用其他方法创建对象和使用对象,这种行为也被称为factory pattern(工场模式)
2.创建NSURLSessionDataTask(iOS7中新的东西)从URL中获取数据。你将随后添加解析数据。
3.一旦有人预定信号就启动网络请求
4.创建并返回RACDisposable
对象来清理已经被销毁的信号
5.添加一个“副作用”,来添加错误日志。
注意:可以通过找个网站来更好的理解ReactiveCocoa
找到// TODO: Handle retrieved data中的-fetchJSONFromURL:替换称下面的代码:
if (!error){
NSError *jsonError = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
if(!jsonError){
//1
[subscriber sendNext:json];
}
else{
// 2
[subscriber sendError:jsonError];
}
}
else {
//2
[subscriber sendError:error];
}
//3
[subscriber sendCompleted];
下面显示上面的每一段代码时干啥的
1.当JSON数据存在并且没有错误,将JSON序列作为一个数据或者字典发送给用户
2.如果有错误就通知用户
3.无论请求成功与否都让用户知道请求已经完成。
也许-fetchJSONFromURL:
这个方法有点长,但是它使得你的特定的API请求在最后会很简单。
Fetching Current Conditions
仍然时在WXClient.m方法中,添加下面的方法:
- (RACSignal *)fetchCurrentConditionsForLocation: (CLLocationCoordinate2D)coordinate{
//1
NSString *urlString = [NSString stringWithFormat:@“http://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&units=imperial”,coordinate.latitude,coordinate.longitude];
NSURL *url = [NSURL URLWithString:urlString];
//2
return [[self fetchJSONFormURL:url] map:^(NSDictionary *json){
//3
return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:json error:nil];
}];
}
依次给每段代码解释:
1. 利用CLLocationCoordinate2D
对象规定URL为纬度和经度格式
2. 用你刚才创建信号的方法。由于返回值是一个信号,所以你可以调用它的其他ReactiveCocoa 方法。NSDictionary中的一个实例—在这里,你将映射返回值到另一个不同的值中。
3.使用MTLJSONAdapter来转换成JSON的WXCondition对象,使用你的WXCondition创建 MTLJSONSerializing协议
Fetching the Hourly Forecast
现在,在WXClient.m中添加下面的犯法,来给特定坐标显示实时预报:
- (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];
//1
return [[self fetchJSONFormURL:url] map^(NSDictionary *json){
//2
RACSequence *list = [json[@“list”] rac_sequence];
//3
return [[list map:^(NSDictionary *item) {
//4
return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:item error:nil];
//
}] array];
}];
}
这是一个很短的方法,但是有很多细节:
1.使用-fetchJSONFromURL再次格式化为JSON格式作为映射。注意有多少代码来使用这个方法来保存
2.从JSON的”list”键来创建 RACSequence 。RACSequence
s让你通过执行ReactiveCocoa来操作列表。
3.映射对象到新的列表,这里被称为-map
在这个列表中的每个对象,都将返回新的对象到列表。
4.再次使用MTLJSONAdapterr将WXCondition对象转换成JSON
5.使用RACSequence中的 -map: 返回另一个 RACSequence,利用这个方法可以简单的获取所谓NSArray
.返回的数据
Fetching the Daily Forecast
仍然在WXClient.m:中添加下面的方法
- (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate{
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];
NSURL *url = [NSURL URLWithString:urlString];
// use the generic fetch method and map results to convert into an array of mantle objects
return [[self fetchJSONFormURL:url] map:^(NSDictionary *json) {
//从JSON 列表中选择一个列创建sequence
RACSequence *list = [json[@“list”] rac_sequence];
//use a function to map results from JSON to mantle objects
return [[list map:^(NSDictionary *item){
return [MTLJSONAdapter modelOfClass:[WXDailyForecast class] fromJSONDictionary:item error:nil;]
}]]
}]
}
到这里你是否感觉很熟悉?是啊, 这里的方法完全一样 -fetchHourlyForecastForLocation:
除了它使用WXDailyForecast而不是WXCondition和获取daily预报。
运行你的程序,这时候你不会看到任何新加入的东西,但是这是让你休息一下并确保美欧任何错误或者警告的好机会。
Managing & Storing Your Data
是时候来完成WXManager这个类了,这是将一切融合的一个类。这个类将实现你的应用程序中的一些关键功能:
-
它遵循单例设计模式singleton design pattern
-
它视图找到设备的位置
-
找到位置后,获取响应的气象数据
打开WXManager.h并且用下面的代码替换内容
@import Foundation;
@import CoreLocation;
#import <ReactiveCocoa/ReactiveCocoa/ReactiveCocoa.h>
//1
#import “WXCondition.h”
@interface WXManager:NSObject <CLLocationManagerDelegate>
//2
+ (instancetype)sharedManager;
//3
@property (nonatomic, strong, readonly) CLLocation *currentLocation;
@property (nonatomic, strong, readonly) WXCondition *currentCondition;
@property (nonatomic, strong, readonly) NSArray *hourlyForecast;
@property (nonatomic, strong, readonly) NSArray *dailyForecast;
//4
- (void)findCurrentLocation;
@end
这里没有什么非常重要的知识点,但是上面注释的地方需要注意
1.注意如果你没有导入WXDailyForecast.h,你将总是使用WXCondition作为预报类。WXDailyForecast.h只是帮助覆盖的JSON转换成Objective-c
2.利用instancetype替换WXManager,所以子类会返回适当的类型。
3.这些属性将储存你的数据,由于WXManager是一个单例,所以这些属性可以访问任何地方。设置公共属性为只读(readonly
)是因为只有管理者才可以更改。
4.这种方法启动或者刷新将获取到整个位置和天气的数据
现在打开WXManager.m添加下面的倒入文件
#import “WXClient.h”
#import <TSMessages/TSMessage.h>
右侧下方的进口,粘贴在专用接口,如下所示
@interface WXManager () 下面是关于上面属性的解释: 1.声明你在公共接口中加入相同的属性,但是这一次把他们定义为读写(readwrite),因此您可以在后台更改值 2.声明为定位的发现和数据抓取其他一些私人位置,添加@implementation和@end:之间的下列通用单例构造函数:
- (id)init { |
你使用更ReactiveCocoa方法来观察和反应值的变化。下面是上面的方法的作用
1.创建一个位置管理器并且以self
.来设置它的委托
2.为管理者创建WXClient对象。这将处理所有的网络和数据请求,根据我们的最佳实践的这个主张。
3.类似KVO但是比KVO更加强大,不明白KVO你可以看这里Key-Value Observing
4.为了继续, currentLocation不能为零
5.-flattenMap:非常类似于-map:: ,将值扁平化代替映射值,并返回包含所有三个信号中的一个对象。通过这种方式,你可以考虑所有三个进程作为单个工作单元。
6.将信号传递到主线程上的用户。
7.这不是很好的做法,从你的模型中的UI交互,但出于演示的目的,每当发生错误时你会显示一个标语。
下一步,以显示准确的天气预测,我们需要确定设备的位置
Finding Your Location
接下来,您将添加时的位置,发现触发的天气抓取的代码。下面的代码添加到WXManager.m:中
- (void)findCurrentLocation {
self.isFirstUpdate = YES; [self.locationManager startUpdatingLocation];}
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
// 1
if (self.isFirstUpdate) {
self.isFirstUpdate = NO; return; }
CLLocation *location = [locations lastObject];
// 2
if (location.horizontalAccuracy > 0) {
// 3
self.currentLocation = location; [self.locationManager stopUpdatingLocation]; }}
上面的方法是相当简单的:
1.总是忽略第一个位置更新,因为它几乎总是缓存
2.一旦你有适当的精度的位置,停止更新
3.设置currentLocation键将触发你在init执行前设置的RACObservable
Retrieve the Weather Data
最后,现在是时候添加三个取其中调用客户端上的方法的方法和保存价值的manager。所有这三种方法都捆绑起来,由RACObservable订阅创建的init方法之前添加。您将返回客户端返回相同的信号,这也可以订阅。
将下面的代码添加到WXManager.m:中
- (RACSignal *)updateCurrentConditions {
return [[self.client fetchCurrentConditionsForLocation:self.currentLocation.coordinate] doNext:^(WXCondition *condition) {
self.currentCondition = condition; }];}
- (RACSignal *)updateHourlyForecast {
return [[self.client fetchHourlyForecastForLocation:self.currentLocation.coordinate] doNext:^(NSArray *conditions) {
self.hourlyForecast = conditions; }];}
- (RACSignal *)updateDailyForecast {
return [[self.client fetchDailyForecastForLocation:self.currentLocation.coordinate] doNext:^(NSArray *conditions) {
self.dailyForecast = conditions; }];}
它看起来像一切都连接起来,蓄势待发。别急!该应用程序实际上并没有告诉管理者做任何事情
打开 WXController.m并导入管理文件的顶部,如下所示
#import “WXManager.h”
在- viewDidload最后添加下面的代码
[[WXManager shareManager] findCurrentLocation];
这只是要求管理类,开始寻找设备的当前位置。
生成并运行您的应用程序,系统会提示您是否允许使用位置服务。你仍然不会看到任何UI的更新,但检查控制台日志,你会看到类似以下内容
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=imperial2013-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=72013-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
它看起来有点迟钝,但输出是指所有的代码是否工作正常,网络请求发火正常。
Wiring the Interface
这是最后一次显示所有你获取,映射和存储的数据。你将利用ReactiveCocoa观察新数据到达WXManager和界面时的更新。
还在WXController.m,去的 - viewDidLoad中的底部,并添加下面的代码只是上面的
[ [ WXManager sharedManager ] findCurrentLocation ] ;行
1.在WXManager单例中观察currentCondition key
2.因为你更新UI所以要提供在主线程上的变化
3.更新与气象数据的文本标签;你使用newCondition的文字,而不是单例。以保证用户参数是新值
4.使用映射的图像文件名创建一个图像,并将其作为该视图的图标
生成并运行您的应用程序,你会看到当前温度,当前条件,并表示当前条件下的图标。所有的数据都是实时的,所以你的价值观可能不会匹配那些下文。但是,如果你的位置是旧金山,它似乎总是约65度。幸运的旧金山!:]
// 1[[RACObserve([WXManager sharedManager], currentCondition) |
ReactiveCocoa Bindings
ReactiveCocoa带来了自己的Cocoa Bindings的形式为iOS
不知道是什么绑定?概括地说,他们是一个技术,它提供了保持模型和视图的值而无需编写大量的同步的一种手段“胶水代码”。它们允许你建立一个视图和数据块之间的间接连接, “结合”它们,使得在一方的变化反映在另一个中
这是一个非常强大的概念,不是吗
好了,拿起你的下巴掉在地上。现在是时候继续前进。
注意:对于强大的绑定更多的例子,你可以看看 ReactiveCocoa Readme.。
添加下面你在上一步中添加的代码下面的代码:
// 1RAC(hiloLabel, text) = [[RACSignal combineLatest:@[
// 2
RACObserve([WXManager sharedManager], currentCondition.tempHigh),
RACObserve([WXManager sharedManager], currentCondition.tempLow)]
// 3
reduce:^(NSNumber *hi, NSNumber *low) {
return [NSString stringWithFormat:@"%.0f° / %.0f°",hi.floatValue,low.floatValue]; }]
// 4
deliverOn:RACScheduler.mainThreadScheduler];
1.在RAC ( ... )宏有助于保持语法干净。从该信号的返回值被分配给 hiloLabel对象的文本项。
2.观察了高温和低温的currentCondition key。
3.降低您的组合信号的值转换成一个单一的值,注意该参数的顺序信号的顺序相匹配。
4。同样,因为你正在处理的用户界面,提供一切都在主线程。
生成并运行您的应用程序,你应该看到的高/低标号沿着与UI的其余部分,像这样的左下方更新时间
// 1RAC(hiloLabel, text) = [[RACSignal combineLatest:@[ |
上面的代码结合高低温值到hiloLabel的text属性。这里有一个详细的看看你完成这件事情:
Displaying Data in the Table View
现在,你已经获取所有的数据,你可以在表视图整齐地显示出来。你会在标题单元格(如适用)分页表视图中显示六个最新的每小时和每天的预测。该应用程序会出现有三个页面:一个是目前的条件,一个是逐时预报,以及一个用于每日预报。
之前,你可以添加单元格的表视图,你需要初始化和配置一些日期格式化
在WXController.m顶部添加下面的两个属性
@property (nonatomic, strong) NSDateFormatter *hourlyFormatter;
@property (nonatomic, strong) NSDataFormatter *dailyFormatter;
由于日期格式化是昂贵的创建,我们将实例化他们在我们的init方法和存储使用这些属性对它们的引用
还是在同一个文件中,添加直属@implementation
下面的代码
- (id)init { |
你可能想知道为什么你初始化这些日期格式化在-init和而不是viewDidLoad中其他事物一样。好问题!
- viewDidLoad中实际上可以在一个视图控制器的生命周期多次调用。 NSDateFormatter对象是昂贵的初始化,而是将它们放置在你的init会确保它们是由你的视图控制器初始化一次
寻找实现代码如下: numberOfRowsInSection :在WXController.m并更换TODO并以下列返回行
// 1
if (section == 0) {
return MIN([[WXManager sharedManager].hourlyForecast count], 6) + 1;
}
// 2
return MIN([[WXManager sharedManager].dailyForecast count], 6) + 1;
相对短的代码块,但这里是它的作用:
1.第一部分是对的逐时预报。使用六小时最新预测,并添加了页眉多一个单元格
2.接下来的部分是每日预报。使用六种最新的每日预报,并添加了页眉多一个单元格
Note: You’re using table cells for headers here instead of the built-in section headers which have sticky-scrolling behavior. The table view is set up with paging enabled and sticky-scrolling behavior would look odd in this context.
寻找实现代码如下:的cellForRowAtIndexPath :在WXController.m ,并以下列取代TODO部分
if (indexPath.section == 0) { 同样,这段代码是相当简单的
1.每个部分的第一行是标题单元格 2.获取每小时的天气和使用自定义配置方法配置单元 3.获取每天的天气,并使用另一个自定义配置方法配置单元 最后,下面的三种方法添加到WXController.m :
|
Adding Polish to Your App
本页面为每小时和每天的预测不占用整个屏幕。幸运的是,这原来是一个真正简单的办法。在本教程前面您捕获在
-viewDidLoad
.中屏幕高度。
查找表视图的委托方法,实现代码如下:-tableView:heightForRowAtIndexPath:在WXController.m and replace the TODO
and return
lines with the following:
NSInteger cellCount = [self tableView:tableView numberOfRowsInSection:indexPath.section]; 这将屏幕高度由cell中各部分的数量,以便所有单元的总高度等于屏幕的高度.
生成并运行您的应用程序;表视图现在填满整个屏幕,如下面的截图: 最后要做的是把我在本教程的Part 1开头提到的模糊。模糊应填写为动态滚动过去预测的第一页
3.所得到的值赋给模糊图像的alpha属性来更改模糊图像的多,你会看到当你滚动 生成并运行您的应用程序,滚动你的表视图,并检查了真棒模糊效果 ![]() Where To Go From Here
你已经完成了很多在本教程中:您使用CocoaPods创建了一个项目,建立一个视图结构完全代码,创建的数据模型和管理人员,并利用函数式编程连接到一起! 有很多很酷的地方,你可以把这个应用程序。一个整洁的开始是使用Flickr的API来查找基于设备的位置,背景图片 还有,你的应用程序只处理温度和条件;什么其他的天气信息,你能融入你的应用程序
|