注:本文譯自:raywenderlich ios-7-best-practices-part-2,去除了跟主題無關的寒暄部分。 歡迎轉載,保持署名
開始
你有兩個選擇開始本教程:您可以使用在本教程的第1部分你已完成的項目,或者你可以在這裏下載第1部分已完成的項目。
在前面的教程中你創建了你的App的天氣模型 – 現在你需要使用OpenWeatherMap API爲你的App來獲取一些數據。你將使用兩個類抽象數據抓取、分析、存儲:WXClient
和WXManager
。
WXClient
的唯一責任是創建API請求,並解析它們;別人可以不用擔心用數據做什麼以及如何存儲它。劃分類的不同工作職責的設計模式被稱爲關注點分離。這使你的代碼更容易理解,擴展和維護。
與ReactiveCocoa工作
確保你使用SimpleWeather.xcworkspace
,打開WXClient.h
並增加imports
1
2
|
@import CoreLocation;
#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
2
3
4
5
|
@import Foundation;
- (RACSignal *)fetchJSONFromURL:(NSURL *)url;
- (RACSignal *)fetchCurrentConditionsForLocation:(CLLocationCoordinate2D)coordinate;
- (RACSignal *)fetchHourlyForecastForLocation:(CLLocationCoordinate2D)coordinate;
- (RACSignal *)fetchDailyForecastForLocation:(CLLocationCoordinate2D)coordinate;
|
現在,似乎是一個很好的機會來介紹ReactiveCocoa!
ReactiveCocoa(RAC)是一個Objective-C的框架,用於函數式反應型編程,它提供了組合和轉化數據流的API。代替專注於編寫串行的代碼 – 執行有序的代碼隊列 – 可以響應非確定性事件。
Github上提供的a great overview of the benefits:
-
對未來數據的進行組合操作的能力。
-
減少狀態和可變性。
-
用聲明的形式來定義行爲和屬性之間的關係。
-
爲異步操作帶來一個統一的,高層次的接口。
-
在KVO的基礎上建立一個優雅的API。
例如,你可以監聽username
屬性的變化,用這樣的代碼:
1
2
3
|
[RACAble(self.username) subscribeNext:^(NSString *newName) {
NSLog(@"%@", newName);
}];
|
subscribeNext
這個block會在self.username
屬性變化的時候執行。新的值會傳遞給這個block。
您還可以合併信號並組合數據到一個組合數據中。下面的示例取自於ReactiveCocoa的Github頁面:
1
2
3
4
5
6
7
8
|
[[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
加入以下imports:
1
2
|
#import "WXCondition.h"
#import "WXDailyForecast.h"
|
在imports下,添加私有接口:
1
2
3
4
5
|
@interface WXClient ()
@property (nonatomic, strong) NSURLSession *session;
@end
|
這個接口用這個屬性來管理API請求的URL session。
添加以下init
放到到@implementation
和@end
之間:
1
2
3
4
5
6
7
|
- (id)init {
if (self = [super init]) {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config];
}
return self;
}
|
使用defaultSessionConfiguration
爲您創建session。
1
|
注意:如果你以前沒有了解過NSURLSession,看看我們的[NSURLSession教程](http://www.raywenderlich.com/51127/nsurlsession-tutorial),瞭解更多信息。
|
構建信號
你需要一個主方法來建立一個信號從URL中取數據。你已經知道,需要三種方法來獲取當前狀況,逐時預報及每日預報。
不是寫三個獨立的方法,你可以遵守DRY(Don’t Repeat Yourself)的軟件設計理念,使您的代碼容易維護。
第一次看,以下的一些ReactiveCocoa部分可能看起來相當陌生。別擔心,你會一塊一塊理解他。
增加下列方法到WXClient.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- (RACSignal *)fetchJSONFromURL:(NSURL *)url {
NSLog(@"Fetching: %@",url.absoluteString);
// 1
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 2
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(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);
}];
}
|
通過一個一個註釋,你會看到代碼執行以下操作:
-
返回信號。請記住,這將不會執行,直到這個信號被訂閱。
-
fetchJSONFromURL:
創建一個對象給其他方法和對象使用;這種行爲有時也被稱爲工廠模式。 -
創建一個NSURLSessionDataTask(在iOS7中加入)從URL取數據。你會在以後添加的數據解析。
-
一旦訂閱了信號,啓動網絡請求。
-
創建並返回RACDisposable對象,它處理當信號摧毀時的清理工作。
-
增加了一個“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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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];
|
-
當JSON數據存在並且沒有錯誤,發送給訂閱者序列化後的JSON數組或字典。
-
在任一情況下如果有一個錯誤,通知訂閱者。
-
無論該請求成功還是失敗,通知訂閱者請求已經完成。
-fetchJSONFromURL:
方法有點長,但它使你的特定的API請求方法變得很簡單。
獲取當前狀況
還在WXClient.m
中,添加如下方法:
1
2
3
4
5
6
7
8
9
10
11
|
- (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 fetchJSONFromURL:url] map:^(NSDictionary *json) {
// 3
return [MTLJSONAdapter modelOfClass:[WXCondition class] fromJSONDictionary:json error:nil];
}];
}
|
-
使用
CLLocationCoordinate2D
對象的經緯度數據來格式化URL。 -
用你剛剛建立的創建信號的方法。由於返回值是一個信號,你可以調用其他ReactiveCocoa的方法。 在這裏,您將返回值映射到一個不同的值 – 一個NSDictionary實例。
-
使用
MTLJSONAdapter
來轉換JSON到WXCondition
對象
– 使用MTLJSONSerializing
協議創建的WXCondition
。
獲取逐時預報
現在添加根據座標獲取逐時預報的方法到WXClient.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
- (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 fetchJSONFromURL: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];
// 5
}] array];
}];
}
|
-
再次使用
-fetchJSONFromUR
方法,映射JSON。注意:重複使用該方法節省了多少代碼! -
使用JSON的”list”key創建
RACSequence
。 RACSequences
讓你對列表進行ReactiveCocoa操作。 -
映射新的對象列表。調用
-map:
方法,針對列表中的每個對象,返回新對象的列表。 -
再次使用
MTLJSONAdapter
來轉換JSON到WXCondition
對象。 -
使用
RACSequence
的-map
方法,返回另一個RACSequence
,所以用這個簡便的方法來獲得一個NSArray
數據。
獲取每日預報
最後,添加如下方法到WXClient.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- (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 fetchJSONFromURL:url] map:^(NSDictionary *json) {
// Build a sequence from the list of raw JSON
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];
}] array];
}];
}
|
是不是看起來很熟悉?是的,這個方法與-fetchHourlyForecastForLocation:
方法非常像。除了它使用WXDailyForecast
代替WXCondition
,並獲取每日預報。
構建並運行您的App,現在你不會看到任何新的東西,但這是一個很好機會鬆一口氣,並確保沒有任何錯誤或警告。
管理並存儲你的數據
現在是時間來充實WXManager
,這個類會把所有東西結合到一起。這個類實現您App的一些關鍵功能:
-
它使用單例設計模式。
-
它試圖找到設備的位置。
-
找到位置後,它獲取相應的氣象數據。
打開WXManager.h
使用以下代碼來替換其內容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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
|
-
請注意,你沒有引入
WXDailyForecast.h
,你會始終使用WXCondition
作爲預報的類。 WXDailyForecast
的存在是爲了幫助Mantle轉換JSON到Objective-C。 -
使用
instancetype
而不是WXManager
,子類將返回適當的類型。 -
這些屬性將存儲您的數據。由於
WXManager
是一個單例,這些屬性可以任意訪問。設置公共屬性爲只讀,因爲只有管理者能更改這些值。 -
這個方法啓動或刷新整個位置和天氣的查找過程。
現在打開WXManager.m
並添加如下imports到文件頂部:
#import "WXClient.h"
#import <TSMessages/TSMessage.h>
在imports下方,粘貼如下私有接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@interface WXManager ()
// 1
@property (nonatomic, strong, readwrite) WXCondition *currentCondition;
@property (nonatomic, strong, readwrite) CLLocation *currentLocation;
@property (nonatomic, strong, readwrite) NSArray *hourlyForecast;
@property (nonatomic, strong, readwrite) NSArray *dailyForecast;
// 2
@property (nonatomic, strong) CLLocationManager *locationManager;
@property (nonatomic, assign) BOOL isFirstUpdate;
@property (nonatomic, strong) WXClient *client;
@end
|
-
聲明你在公共接口中添加的相同的屬性,但是這一次把他們定義爲
可讀寫
,因此您可以在後臺更改他們。 -
爲查找定位和數據抓取聲明一些私有變量。
添加如下通用的單例構造器到@implementation
與@end
å中間:
1
2
3
4
5
6
7
8
9
|
+ (instancetype)sharedManager {
static id _sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
}
|
然後,你需要設置你的屬性和觀察者。
添加如下方法到WXManager.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
- (id)init {
if (self = [super init]) {
// 1
_locationManager = [[CLLocationManager alloc] init];
_locationManager.delegate = self;
// 2
_client = [[WXClient alloc] init];
// 3
[[[[RACObserve(self, currentLocation)
// 4
ignore:nil]
// 5
// Flatten and subscribe to all 3 signals when currentLocation updates
flattenMap:^(CLLocation *newLocation) {
return [RACSignal merge:@[
[self updateCurrentConditions],
[self updateDailyForecast],
[self updateHourlyForecast]
]];
// 6
}] deliverOn:RACScheduler.mainThreadScheduler]
// 7
subscribeError:^(NSError *error) {
[TSMessage showNotificationWithTitle:@"Error"
subtitle:@"There was a problem fetching the latest weather."
type:TSMessageNotificationTypeError];
}];
}
return self;
}
|
你正使用更多的ReactiveCocoa方法來觀察和反應數值的變化。上面這些你做了:
-
創建一個位置管理器,並設置它的delegate爲
self
。 -
爲管理器創建
WXClient
對象。這裏處理所有的網絡請求和數據分析,這是關注點分離的最佳實踐。 -
管理器使用一個返回信號的ReactiveCocoa腳本來觀察自身的
currentLocation
。這與KVO類似,但更爲強大。 -
爲了繼續執行方法鏈,
currentLocation
必須不爲nil
。 -
-
flattenMap:
非常類似於-map:
,但不是映射每一個值,它把數據變得扁平,並返回包含三個信號中的一個對象。通過這種方式,你可以考慮將三個進程作爲單個工作單元。 -
將信號傳遞給主線程上的觀察者。
-
這不是很好的做法,在你的模型中進行UI交互,但出於演示的目的,每當發生錯誤時,會顯示一個banner。
接下來,爲了顯示準確的天氣預報,我們需要確定設備的位置。
查找你的位置
下一步,你要添加當位置查找到,觸發抓取天氣數據的代碼。
添加如下代碼到WXManager.m
的實現塊中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
- (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];
}
}
|
-
忽略第一個位置更新,因爲它一般是緩存值。
-
一旦你獲得一定精度的位置,停止進一步的更新。
-
設置
currentLocation
,將觸發您之前在init中設置的RACObservable。
獲取氣象數據
最後,是時候添加在客戶端上調用並保存數據的三個獲取方法。將三個方法捆綁起來,被之前在init
方法中添加的RACObservable訂閱。您將返回客戶端返回的,能被訂閱的,相同的信號。
所有的屬性設置發生在-doNext:
中。
添加如下代碼到WXManager.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
- (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;
}];
}
|
它看起來像將一切都連接起來,並蓄勢待發。別急!這App實際上並沒有告訴管理者做任何事情。 打開WXController.m
並導入這管理者到文件的頂部,如下所示:
添加如下代碼到-viewDidLoad:
的最後:
1
|
[[WXManager sharedManager] findCurrentLocation];
|
這告訴管理類,開始尋找設備的當前位置。
構建並運行您的App,系統會提示您是否允許使用位置服務。你仍然不會看到任何UI的更新,但檢查控制檯日誌,你會看到類似以下內容:
1
2
3
|
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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
|
// 1
[[RACObserve([WXManager sharedManager], currentCondition)
// 2
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(WXCondition *newCondition) {
// 3
temperatureLabel.text = [NSString stringWithFormat:@"%.0f°",newCondition.temperature.floatValue];
conditionsLabel.text = [newCondition.condition capitalizedString];
cityLabel.text = [newCondition.locationName capitalizedString];
// 4
iconView.image = [UIImage imageNamed:[newCondition imageName]];
}];
|
-
觀察
WXManager
單例的currentCondition。 -
傳遞在主線程上的任何變化,因爲你正在更新UI。
-
使用氣象數據更新文本標籤;你爲文本標籤使用
newCondition
的數據,而不是單例。訂閱者的參數保證是最新值。 -
使用映射的圖像文件名來創建一個圖像,並將其設置爲視圖的圖標。
構建並運行您的App,你會看到當前溫度,當前狀況和表示當前狀況的圖標。所有的數據都是實時的。但是,如果你的位置是舊金山,它似乎總是約65度。Lucky San Franciscans! :]
ReactiveCocoa的綁定
ReactiveCocoa爲iOS帶來了自己的Cocoa綁定的形式。
不知道是什麼綁定?簡而言之,他們是一種提供了保持模型和視圖的數據同步而無需編寫大量”膠水代碼”的手段,它們允許你建立一個視圖和數據塊之間的連接, “結合”它們,使得一方的變化反映到另一箇中的技術。
這是一個非常強大的概念,不是嗎?
1
|
注意:要獲得更多的綁定實例代碼,請查看[ReactiveCocoa Readme](https://github.com/ReactiveCocoa/ReactiveCocoa)。
|
添加如下代碼到你上一步添加的代碼後面:
1
2
3
4
5
6
7
8
9
10
11
|
// 1
RAC(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];
|
上面的代碼結合高溫、低溫的值到hiloLabel的text屬性。看看你完成了什麼:
-
RAC(…)宏有助於保持語法整潔。從該信號的返回值將被分配給
hiloLabel
對象的text
。 -
觀察
currentCondition
的高溫和低溫。合併信號,並使用兩者最新的值。當任一數據變化時,信號就會觸發。 -
從合併的信號中,減少數值,轉換成一個單一的數據,注意參數的順序與信號的順序相匹配。
-
同樣,因爲你正在處理UI界面,所以把所有東西都傳遞到主線程。
構建並運行你的App。你應該看到在左下方的高/低溫度label更新了:
在Table View中顯示數據
現在,你已經獲取所有的數據,你可以在table view中整齊地顯示出來。你會在分頁的table view中顯示最近6小時的每時播報和每日預報。該App會顯示三個頁面:一個是當前狀況,一個是逐時預報,以及一個每日預報。
之前,你可以添加單元格到table view,你需要初始化和配置一些日期格式化。
到WXController.m
最頂端的私有接口處,添加下列兩個屬性
1
2
|
@property (nonatomic, strong) NSDateFormatter *hourlyFormatter;
@property (nonatomic, strong) NSDateFormatter *dailyFormatter;
|
由於創建日期格式化非常昂貴,我們將在init方法中實例化他們,並使用這些變量去存儲他們的引用。
還在WXController.m
中,添加如下代碼到@implementation
中:
1
2
3
4
5
6
7
8
9
10
|
- (id)init {
if (self = [super init]) {
_hourlyFormatter = [[NSDateFormatter alloc] init];
_hourlyFormatter.dateFormat = @"h a";
_dailyFormatter = [[NSDateFormatter alloc] init];
_dailyFormatter.dateFormat = @"EEEE";
}
return self;
}
|
你可能想知道爲什麼在-init
中初始化這些日期格式化,而不是在-viewDidLoad
中初始化他們。好問題!
實際上-viewDidLoad
可以在一個視圖控制器的生命週期中多次調用。 NSDateFormatter對象的初始化是昂貴的,而將它們放置在你的-init
,會確保被你的視圖控制器初始化一次。
在WXController.m
中,尋找tableView:numberOfRowsInSection:
,並用如下代碼更換TODO
到return
:
1
2
3
4
5
6
|
// 1
if (section == 0) {
return MIN([[WXManager sharedManager].hourlyForecast count], 6) + 1;
}
// 2
return MIN([[WXManager sharedManager].dailyForecast count], 6) + 1;
|
-
第一部分是對的逐時預報。使用最近6小時的預預報,並添加了一個作爲頁眉的單元格。
-
接下來的部分是每日預報。使用最近6天的每日預報,並添加了一個作爲頁眉的單元格。
1
|
注意:您使用表格單元格作爲標題,而不是內置的、具有粘性的滾動行爲的標題。這個table view設置了分頁,粘性滾動行爲看起來會很奇怪。
|
在WXController.m
找到tableView:cellForRowAtIndexPath:
,並用如下代碼更換TODO
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
if (indexPath.section == 0) {
// 1
if (indexPath.row == 0) {
[self configureHeaderCell:cell title:@"Hourly Forecast"];
}
else {
// 2
WXCondition *weather = [WXManager sharedManager].hourlyForecast[indexPath.row - 1];
[self configureHourlyCell:cell weather:weather];
}
}
else if (indexPath.section == 1) {
// 1
if (indexPath.row == 0) {
[self configureHeaderCell:cell title:@"Daily Forecast"];
}
else {
// 3
WXCondition *weather = [WXManager sharedManager].dailyForecast[indexPath.row - 1];
[self configureDailyCell:cell weather:weather];
}
}
|
-
每個部分的第一行是標題單元格。
-
獲取每小時的天氣和使用自定義配置方法配置cell。
-
獲取每天的天氣,並使用另一個自定義配置方法配置cell。
最後,添加如下代碼到WXController.m
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// 1
- (void)configureHeaderCell:(UITableViewCell *)cell title:(NSString *)title {
cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18];
cell.textLabel.text = title;
cell.detailTextLabel.text = @"";
cell.imageView.image = nil;
}
// 2
- (void)configureHourlyCell:(UITableViewCell *)cell weather:(WXCondition *)weather {
cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18];
cell.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18];
cell.textLabel.text = [self.hourlyFormatter stringFromDate:weather.date];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%.0f°",weather.temperature.floatValue];
cell.imageView.image = [UIImage imageNamed:[weather imageName]];
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
}
// 3
- (void)configureDailyCell:(UITableViewCell *)cell weather:(WXCondition *)weather {
cell.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:18];
cell.detailTextLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:18];
cell.textLabel.text = [self.dailyFormatter stringFromDate:weather.date];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%.0f° / %.0f°",
weather.tempHigh.floatValue,
weather.tempLow.floatValue];
cell.imageView.image = [UIImage imageNamed:[weather imageName]];
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
}
|
-
配置和添加文本到作爲section頁眉單元格。你會重用此爲每日每時的預測部分。
-
格式化逐時預報的單元格。
-
格式化每日預報的單元格。
構建並運行您的App,嘗試滾動你的table view,並…等一下。什麼都沒顯示!怎麼辦?
如果你已經使用過的UITableView
,可能你之前遇到過問題。這個table沒有重新加載!
爲了解決這個問題,你需要添加另一個針對每時預報和每日預報屬性的ReactiveCocoa觀察。
在WXController.m
的-viewDidLoad
中,添加下列代碼到其他ReactiveCocoa觀察代碼中:
1
2
3
4
5
6
7
8
9
10
11
|
[[RACObserve([WXManager sharedManager], hourlyForecast)
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(NSArray *newForecast) {
[self.tableView reloadData];
}];
[[RACObserve([WXManager sharedManager], dailyForecast)
deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(NSArray *newForecast) {
[self.tableView reloadData];
}];
|
構建並運行App;滾動table view,你將看到填充的所有預報數據。
給你的App添加效果
本頁面爲每時和每日預報不會佔滿整個屏幕。幸運的是,有一個非常簡單的修復辦法。在本教程前期,您在-viewDidLoad
中獲得屏幕高度。
在WXController.m
中,查找table
view的委託方法-tableView:heightForRowAtIndexPath:
,並且替換TODO
到return
的代碼:
1
2
|
NSInteger cellCount = [self tableView:tableView numberOfRowsInSection:indexPath.section];
return self.screenHeight / (CGFloat)cellCount;
|
屏幕高度由一定數量的cell所分割,所以所有cell的總高度等於屏幕的高度。
構建並運行你的App;table view填滿了整個屏幕,如下所示:
最後要做的是把我在本教程的第一部分開頭提到的模糊效果引入。當你滾動預報頁面,模糊效果應該動態顯示。
添加下列scroll delegate到WXController.m
最底部:
1
2
3
4
5
6
7
8
9
10
11
|
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 1
CGFloat height = scrollView.bounds.size.height;
CGFloat position = MAX(scrollView.contentOffset.y, 0.0);
// 2
CGFloat percent = MIN(position / height, 1.0);
// 3
self.blurredImageView.alpha = percent;
}
|
-
獲取滾動視圖的高度和內容偏移量。與0偏移量做比較,因此試圖滾動table低於初始位置將不會影響模糊效果。
-
偏移量除以高度,並且最大值爲1,所以alpha上限爲1。
-
當你滾動的時候,把結果值賦給模糊圖像的alpha屬性,來更改模糊圖像。
構建並運行App,滾動你的table view,並查看這令人驚異的模糊效果:
何去何從?
在本教程中你已經完成了很多內容:您使用CocoaPods創建了一個項目,完全用代碼書寫了一個視圖結構,創建數據模型和管理類,並使用函數式編程將他們連接到一起!
您可以從這裏下載該項目的完成版本。
這個App還有很多酷的東西可以去做。一個好的開始是使用Flickr API來查找基於設備位置的背景圖像。
還有,你的應用程序只處理溫度和狀態;有什麼其他的天氣信息能融入你的App?