開始
你有兩個選擇開始本教程:您可以使用在本教程的第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到檔案頂部:
1 2 |
#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
並匯入這管理者到檔案的頂部,如下所示:
1 |
#import "WXManager.h" |
新增如下程式碼到-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?