In the previous tutorials, we explored the fundamentals of the NSURLSession
API. There is one other feature of the NSURLSession
API that we haven't look into yet, that is, out-of-process uploads and downloads. In the next two tutorials, I will show you how to create a very simple podcast client that enables background downloads.
在前面的教程中,我們已經探索瞭 NSURLSession API 的基礎部分。關於 NSURLSession API 還有一個特性沒有被提及,就是在進程之外進行上傳和下載。在接下來的兩篇教程中,我將會向你展示如何創建一個簡單的播客客戶端,其支持後臺下載。
Introduction
The podcast client that we're about to create isn't really going to be that functional. It will allow the user to query the iTunes Search API for a list of podcasts, select a podcast, and download episodes. Since we are focusing on the NSURLSession
API, we won't go into playing the episodes the application downloads.
我們將要創建的播客客戶端,其實並沒有完全實現其功能。這將允許用戶通過 iTunes 的查詢接口,得到一系列的播客列表,選擇一個播客,然後進行下載。由於我們關註的是 NSURLSession 的 API,所以我們不會對下載的內容進行播放。
The project, however, will teach you how to use data tasks and download tasks in a real world application. The podcast client will also enable background downloads for which we'll leverage NSURLSession
's out-of-process API. We have quite a few things to do so let's not waste time and get started.
這個項目示例將會教你如何使用Data task 和 Download task,並真的在應用中下載數據。利用 NSURLSession 的 out-of-process API ,播客客戶端也支持後臺下載任務。我們有不少的事情要做,所以不浪費時間瞭介紹瞭,開始吧!
1. Project Setup
Fire up Xcode 5, select New > Project… from the File menu, and choose the Single View Application template from the list of iOS application templates. Name the application Singlecast, set the Device Family to iPhone, and tell Xcode where you'd like to save the project. Hit Create to create the project.
在Xcode中創建一個單視圖項目,命名為 Singlecast。
2. Update Storyboard
The first thing we need to do is edit the project's main storyboard. OpenMain.storyboard, select the storyboard's only view controller, and choose Embed In > Navigation Controller from the Editor menu. The reason for embedding the view controller in a navigation controller will become clear later in this tutorial.
首先需要做的是編輯storyboard。打開storyboard,選中唯一的視圖,然後在 Editor 菜單中,選擇 Embed In > Navigation Controller。為什麼要嵌入一個導航控制器後面就知道瞭,不解釋。
3. Search View Controller
Step 1: Create Class Files
As I mentioned in the introduction, to keep things simple, the user will only be able to subscribe to one podcast. Let's start by creating the search view controller. Select New > File… from the File menu and choose Objective-C class from the options on the right. Name the class MTSearchViewController
and make it a subclass of UIViewController
. Leave the check box labeled With XIB for user interface unchecked. Tell Xcode where you want to save the class files and hit Create.
如我在引言中所說,為瞭變得簡單,用戶隻能訂閱一個播客。讓我們開始創建一個搜索視圖控制器。創建一個Objective-C 類,名為 MTSearchViewController,繼承自 UIViewController。註意不要勾選下面的兩個選項,如下圖:
Step 2: Update Class Interface
Before we create the user interface, open the view controller's header file and update the class's interface as shown below. We specify that the MTSearchViewController
class conforms to the UITableViewDataSource
, UITableViewDelegate
, and UISearchBarDelegate
protocols, we declare two outlets, searchBar
and tableView
as well as an action,cancel
, to dismiss the search view controller.
在編輯UI之前,先打開視圖控制器的頭文件,指定 MTSearchViewController 類遵循 UITableViewDataSource
, UITableViewDelegate
, 和 UISearchBarDelegate
三個協議,然後創建兩個 outlet :searchBar 和 tableView,還有一個 cancel action,用於關閉 search view controller。
#import @interface MTSearchViewController: UIViewController @property(weak,nonatomic)IBOutlet UISearchBar *searchBar; @property(weak,nonatomic)IBOutlet UITableView *tableView; - (IBAction)cancel:(id)sender; @end
Step 3: Create User Interface
Revisit the project's main storyboard and drag a new view controller from the Object Library on the right. Select the new view controller, open the Identity Inspector on the right, and set the view controller's class to MTSearchViewController
. With the new view controller still selected, open the Editor menu and choose Embed In > Navigation Controller. Drag a table view to the view controller's view and connect the table view'sdataSource
and delegate
outlets with the search view controller.
重新打開 storyboard,添加一個新的view controller,選中這個新添加的view controller,在Xcode右側打開 Identity Inspector,設置 view controller的類為 MTSearchViewController。保持這個view controller的選中狀態,打開Editor菜單,選擇 Embed In > Navigation Controller。。拖動一個 table view 到這個新的 view controller 的 view 中,連接這個table view 的 dataSource 和 delegate。
With the table view still selected, open the Attributes Inspector, and set the number of prototype cells to 1
. Select the prototype cell and set its style property to Subtitle and its identifier to SearchCell
.
選中table view,打開 Attributes Inspector, 選擇 Table View ,設置 the number of prototype cells to 1
. 然後選中 cell 並設置其 style property 為 Subtitle,其 identifier 為 SearchCell
.
Drag a search bar from the Object Library and add it to the table view's header view. Select the search bar and connect its delegate
outlet with the view controller.
從 Object Library 中拖動一個 search bar 到table view的頭部。選中 search bar,連接其到view controller 的delegate outlet。
Select the view controller and connect its searchBar
and tableView
outlets with the search bar and table view respectively. There are a few other things that we need to do before we're done with the storyboard.
分別將 searchBar 和 tableView 的 outlet 和 view controller 中的控件連接起來。在結束storyboard的編輯之前還有一些事情要處理。
Open the Object Library and drag a bar button item to the navigation bar. Select the bar button item, connect it with the cancel:
action we declared in the search view controller's interface, and change its Identifier in the Attributes Inspector to Cancel.
打開 Object Library ,拖動一個 bar button 添加到導航欄,然後將這bar button 連接到 cancel action,同時在 Attributes Inspector 中修改其 Identifier 為 Cancel。
Drag a bar button item to the navigation bar of the view controller (not the search view controller) and change its Identifier in the Attributes Inspector to Add. Control drag from the bar button item to the search view controller's navigation controller and select modal from the menu that pops up. This creates a segue from the view controller to the search view controller's navigation controller.
拖動一個bar button添加到另一個view controller(不是search view controller),在 Attributes Inspector 中置其 Identifier 為Add。按住 control 按鍵的同時拖動 這個 bar button 連接到 search view controller,在彈出的菜單中選擇 modal。這樣就在這個view controller 和 search view controller 的navigation 之間建立瞭一個 segue。
If you were to control drag from the view controller's bar button item directly to the search view controller instead of its navigation controller, the navigation controller would never be instantiated and you wouldn't see a navigation bar at the top of the search view controller.(註意這個segue是建立在當前view controller 和 上面的 search view controller 的 navigation controller,如下圖所示:)
Step 4: Table View Implementation
Before we implement the UITableViewDataSource
and UITableViewDelegate
protocols in the MTSearchViewController
class, we need to declare a property that stores the search results we'll get back from the iTunes Search API. Name the property podcasts
as shown below. We also declare a static string that will serve as a cell reuse identifier. It corresponds to the identifier we set on the prototype cell a few moments ago.
在 MTSearchViewController 實現 UITableViewDataSource
和 UITableViewDelegate
協議之前,我們需要聲明一個屬性用以保存從 iTunes Search 接口查詢返回的結果。這個屬性命名為 podcasts。同時聲明一個靜態字符串常量作為 cell 表單元的復用標識符,這和之前我們設置 cell 的 identity 是一致的
#import MTSearchViewController.h @interface MTSearchViewController() @property(strong,nonatomic)NSMutableArray *podcasts; @end
static NSString *SearchCell = @SearchCell;
The implementation of numberOfSectionsInTableView:
is as easy as it gets. We return 1
if self.podcasts
is not nil
and 0
if it is. The implementation oftableView:numberOfRowsInSection:
is pretty similar as you can see below. IntableView:cellForRowAtIndexPath:
, we ask the table view for a cell by passing the cell reuse identifier, which we declared earlier, and indexPath
. We fetch the corresponding item from the podcasts
data source and update the table view cell. BothtableView:canEditRowAtIndexPath:
and tableView:canMoveRowAtIndexPath:
return NO
.
接著,分別對委托方法 numberOfSectionsInTableView: ,tableView:numberOfRowsInSection: ,tableView:cellForRowAtIndexPath: ,tableView:canEditRowAtIndexPath: ,tableView:canMoveRowAtIndexPath: 進行實現。
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { return self.podcasts?1:0; } - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { return self.podcasts?self.podcasts.count:0; } - (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:SearchCell forIndexPath:indexPath]; // Fetch Podcast NSDictionary *podcast = [self.podcasts objectAtIndex:indexPath.row]; // Configure Table View Cell [cell.textLabel setText:[podcast objectForKey:@collectionName]]; [cell.detailTextLabel setText:[podcast objectForKey:@artistName]]; return cell; } - (BOOL)tableView:(UITableView*)tableView canEditRowAtIndexPath:(NSIndexPath*)indexPath { return NO; } - (BOOL)tableView:(UITableView*)tableView canMoveRowAtIndexPath:(NSIndexPath*)indexPath { return NO; }
Before running the application, implement the cancel:
action in which we dismiss the search view controller.
然後再對關閉 search view controller 的 cancel action 進行實現。
- (IBAction)cancel:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; }
Build the project and run the application to make sure that the foundation is working as expected. It's time to start using the NSURLSession
API to query the iTunes Search API.
編譯運行項目確保一切正常運行,這時就可以開始使用 NSURLSession 的 API 進行查詢瞭。
Step 5: Creating a Session
Let's begin by declaring two additional private properties in the MTSearchViewController
class, session
and dataTask
. The session
variable is used to store a reference to theNSURLSession
instance we'll be using for querying Apple's API. We also keep a reference to the data task that we will use for the request. This will enable us to cancel the data task if the user updates the search query before we've received a response from the API. If you have an eye for detail, you may have noticed that theMTSearchViewController
class also conforms to the UIScrollViewDelegate
protocol. The reason for this will become clear in a few minutes.
在 MTSearchViewController 類中添加兩個私有屬性 session ,用於查詢 Apple 提供的接口; dataTask,用於數據請求(在接收到數據響應之前可以取消data task)。同時註意到 MTSearchViewController 遵循瞭 UIScrollViewDelegate 協議(原因不解釋,後面就知道瞭)。
#import MTSearchViewController.h @interface MTSearchViewController() @property(strong,nonatomic)NSURLSession *session; @property(strong,nonatomic)NSURLSessionDataTask *dataTask; @property(strong,nonatomic)NSMutableArray *podcasts; @end
The session is created in its getter method as you can see below. Its implementation shouldn't hold any surprises if you've read the previous tutorials. We override the getter method of the session
property to lazily load the session and confine the session's instantiation and configuration in its getter method. This makes for clean and elegant code.
實現如下的 session 獲取方法,在前面的教程中已經見過,所以不會感到陌生。
- (NSURLSession*)session { if(!_session) { // Initialize Session Configuration NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; // Configure Session Configuration [sessionConfiguration setHTTPAdditionalHeaders:@{@Accept:@application/json}]; // Initialize Session _session = [NSURLSession sessionWithConfiguration:sessionConfiguration]; } return _session; }
Step 6: Searching
To respond to the user's input in the search bar, we implementsearchBar:textDidChange:
of the UISearchBarDelegate
protocol. The implementation is simple. If searchText
is nil
, the method returns early. If the length of searchText
is less than four characters long, we reset the search by invoking resetSearch
. If the query is four characters or longer, we perform a search by calling performSearch
on the search view controller.
為瞭響應用戶在搜索欄的輸入,我們需要實現 UISearchBarDelegate 委托協議方法 searchBar:textDidChange: 。實現的過程是簡單的,如果 searchText 為 nil,則直接返回 return;如果 searchText 的長度小於4 個字符,則調用resetSearch 方法重置;如果查詢輸入長度大於等於 4,則調用 performSearch 方法執行搜索。
- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)searchText { if(!searchText) return; if(searchText.length<=3) { [self resetSearch]; }else{ [self performSearch]; } }
Before we inspect performSearch
, let's take a quick look at resetSearch
. All that we do in resetSearch
is clearing the contents of podcasts
and reloading the table view.
先看看 resetSearch 方法實現,先清除 podcasts 數組中的內容,然後刷新表視圖。
- (void)resetSearch { // Update Data Source [self.podcasts removeAllObjects]; // Update Table View [self.tableView reloadData]; }
The heavy lifting is done in performSearch
. After storing the user's input in a variable named query
, we check if dataTask
is set. If it is set, we call cancel
on it. This is important as we don't want to receive a response from an old request that may no longer be relevant to the user. This is also the reason why we have only one active data task at any one time. There is no advantage in sending multiple requests to the API.
繁重的工作是如何實現 performSearch 方法。先將用戶的輸入保存在 query 變量中,然後檢查 dataTask 是否被設置,如果已經被設置,則調用 cancel 。這是因為我們不希望接收的響應是來自舊的請求,同時這也是我們每一時刻隻有一個活躍的數據請求任務的原因。NSURLSession API 同時處理多個數據請求是沒有優勢的。
Next, we ask the session for a new data task instance by passing it an NSURL
instance and a completion handler. Remember that the session is the factory that creates tasks. You should never create tasks yourself. If we get a valid data task from the session, we call resume
on it as we saw in the previous tutorials.
接著,會話對象開始一個新的 data task 通過傳遞一個 NSURL 實例和一個完成處理程序塊。記住會話對象就是創建請求任務的工廠,你不能自己創建請求任務。然後調用 resume 方法啟動任務。
The logic inside the completion handler is interesting to say the least. The error
object is important to us for several reasons. Not only will it tell us if something went wrong with the request, but it's also useful for determining if the data task was canceled. If we do get an error object, we check whether its error code is equal to -999
. This error code indicates the data task was canceled. If we get another error code, we log the error to the console. In a real application, you'd need to improve the error handling and notify the user when an error is thrown.
完成處理程序塊中的邏輯很有趣。其中 error 對象是相當重要的,它不僅告訴請求是否出錯,而且對於判斷請求任務是否取消也很有用。如果我們接收到一個 error 對象,而且錯誤代碼為 -999,則說明請求任務被取消瞭。如果是其它的錯誤代碼,則我們將其輸出到終端。在實際的應用中,你需要提高錯誤的處理能力並在發送錯誤的時候通知用戶。
If no error was passed to the completion handler, we create a dictionary from theNSData
instance that was passed to the completion handler and we extract the results from it. If we have an array of results to work with, we pass it to processResults:
. Did you notice we invoked processResults:
in a GCD (Grand Central Dispatch) block? Why did we do that? I hope you remember, because it's a very important detail. We have no guarantee that the completion handler is invoked on the main thread. Since we need to update the table view on the main thread, we need to make sure thatprocessResults:
is called on the main thread.
如果沒有錯誤傳遞到完成處理程序塊中,我們就將傳遞到完成處理程序塊中的 NSData 實例創建一個 NSDictionary 對象,然後從這個字典中提取結果,如果我們獲得的結果是一個數組,則傳遞給processResults: 方法處理。是否註意到processResults: 方法的調用是在GCD代碼塊中,這是一個非常重要的細節,雖然我們不能保證完成處理程序塊會在主線程中調用,但由於我們需要在主線程中更新表視圖,所以我們需要確保processResults: 方法一定會在主線程中調用(譯者註:也即說,一旦完成處理程序塊調用瞭,那麼就一定要確保processResults: 方法被調用,以在主線程中更新表視圖,而 processResults: 方法置於GCD block中處理可以做到這一點)。
- (void)performSearch { NSString *query = self.searchBar.text; if(self.dataTask) { [self.dataTask cancel]; } self.dataTask= [self.session dataTaskWithURL:[self urlForQuery:query]completionHandler:^(NSData *data,NSURLResponse *response,NSError *error) { if(error) { if(error.code!= -999) { NSLog(@%@, error); } }else{ NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSArray *results = [result objectForKey:@results]; dispatch_async(dispatch_get_main_queue(), ^{ if(results) { [self processResults:results]; } }); } }]; if(self.dataTask) { [self.dataTask resume]; } }
Before we look at the implementation of processResults:
, I want to quickly show you what happens in urlForQuery:
, the helper method we use in performSearch
. InurlForQuery:
, we replace any spaces with a +
sign to ensure that the iTunes Search API is happy with what we send it. We then create an NSURL
instance with it and return it.
在瞭解如何實現 processResults: 方法之前,先看看 urlForQuery: 方法。在該方法中,我們將所有的空格都置換成 “+”確保 iTunes Search 接口使用正確,然後創建一個NSURL實例並返回,
- (NSURL*)urlForQuery:(NSString*)query { query = [query stringByReplacingOccurrencesOfString:@ withString:@+]; return [NSURL URLWithString:[NSString stringWithFormat:@https://itunes.apple.com/search?media=podcast&entity=podcast&term=%@,query]]; }
In processResults:
, the podcasts
variable is cleared, populated with the contents ofresults
, and the results are displayed in the table view.
在 processResults: 方法中,清除 podcasts
變量中的內容,填入results 變量中的內容,然後更新表視圖,顯示結果。
- (void)processResults:(NSArray*)results { if(!self.podcasts) { self.podcasts= [NSMutableArray array]; } // Update Data Source [self.podcasts removeAllObjects]; [self.podcasts addObjectsFromArray:results]; // Update Table View [self.tableView reloadData]; }
Step 6: Selecting a Podcast
When the user taps a row in the table view to select a podcast,tableView:didSelectRowAtIndexPath:
of the UITableViewDelegate
protocol is invoked. Its implementation may seem odd at first so let me explain what's going on. We select the podcast that corresponds with the user's selection, store it in the application's user defaults database, and dismiss the search view controller. We don't notify anyone about this? Why we do this will become clear once we continue implementing theMTViewController
class.
當用戶點擊表視圖中的一行來選擇一個播客時,UITableViewDelegate 委托協議方法tableView:didSelectRowAtIndexPath: 就會被調用。其實現過程看上去可能有點奇怪,下面稍作解釋。我們將與用戶選擇的播客保存在用戶默認的數據庫中,然後關閉 search view controller。接下來繼續實現MTViewController 類的時候就清楚這麼處理是為什麼瞭。
- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Fetch Podcast NSDictionary *podcast = [self.podcasts objectAtIndex:indexPath.row]; // Update User Defatuls NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; [ud setObject:podcast forKey:@MTPodcast]; [ud synchronize]; // Dismiss View Controller [self dismissViewControllerAnimated:YES completion:nil]; }
Step 7: Finishing Touches
There are two details I want to talk about before returning to the MTViewController
class. When the search view controller is presented to the user, it is clear that she wants to search for podcasts. It is therefore a good idea to immediately present the keyboard. We do this in viewDidAppear:
as shown below.
在繼續實現 MTViewController 類之前,有兩個需要註意的細節。當search view controller呈現給用戶的時候,顯然用戶此時是要進行查詢播客的操作,因此此時應該立即彈出鍵盤,我們在viewDidAppear: 方法中:
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; // Show Keyboard [self.searchBar becomeFirstResponder]; }
The keyboard needs to hide the moment the user starts scrolling through the search results. To accomplish this, we implement scrollViewDidScroll:
of theUIScrollViewDelegate
protocol. This explains why MTSearchViewController
conforms to the UIScrollViewDelegate
protocol. Have a look at the implementation ofscrollViewDidScroll:
shown below.
當用戶瀏覽查詢返回的結果的時候就應該將鍵盤隱藏,要實現這個功能,需要實現 UIScrollViewDelegate 委托協議的 scrollViewDidScroll: 方法。這就是為什麼MTSearchViewController 類要遵循UIScrollViewDelegate
協議瞭。實現scrollViewDidScroll: 方法如下所示:
- (void)scrollViewDidScroll:(UIScrollView*)scrollView { if([self.searchBar isFirstResponder]) { [self.searchBar resignFirstResponder]; } }
The UITableView
class is a subclass of UIScrollView
, which is the reason the above approach works.
4. Looping Back
As we saw earlier, we store the user's selection in the application's user defaults database. We need to update the MTViewController
class to make use of the user's selection in the search view controller. In the view controller's viewDidLoad
method, we load the podcast from the user defaults database and we add the view controller as an observer of the user defaults database for the key path MTPodcast
so that the view controller is notified when the value for MTPodcast
changes.
如前所見,我們將用戶的選擇保存在應用程序的用戶默認數據庫中,接著我們就需要在 MTViewController 中使用這個保存的用戶的選擇。在 viewDidLoad 方法中,我們先加載用戶保存在默認數據庫中選中的播客,然後將 view controller 作為用戶默認數據庫的觀察者,其key path 為MTPodcast ,這樣,當MTPodcast 的值發生變化的時候,就會通知 view controller。
- (void)viewDidLoad { [super viewDidLoad]; // Load Podcast [self loadPodcast]; // Add Observer [[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@MTPodcast options:NSKeyValueObservingOptionNew context:NULL]; }
All we do in loadPodcast
is storing the value for MTPodcast
from the user defaults database in the view controller's podcast
property. This value will be nil
if the user defaults database doesn't contain an entry for MTPodcast
. The view controller will gracefully handle this for us. Remember that, in Objective-C, you can send messages tonil
without all hell breaking loose. This has its disadvantages, but it certainly has its advantages to.
在 loadPodcast 方法中,將保存在用戶默認數據庫中key值為 MTPodcast 的value 保存到 view controller 的podcast
屬性中,如果用戶默認數據庫中沒有包含key為podcast
的鍵值對,這個值可能為 nil。
- (void)loadPodcast { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; self.podcast = [ud objectForKey:@MTPodcast]; }
This also means that we need to declare a property named podcast
in the view controller's implementation file.
這也就說明在 view controller 中聲明瞭一個名為podcast
的屬性:
#import MTViewController.h @interface MTViewController () @property (strong, nonatomic) NSDictionary *podcast; @end
Let's also take a quick look at setPodcast:
and updateView
.
讓我們看看 setPodcast:
方法和updateView 方法的實現:
- (void)setPodcast:(NSDictionary *)podcast { if (_podcast != podcast) { _podcast = podcast; // Update View [self updateView]; } }
- (void)updateView { // Update View self.title = [self.podcast objectForKey:@collectionName]; }
When the value in the user defaults database changes for the key MTPodcast
, the view controller can respond to this change inobserveValueForKeyPath:ofObject:change:context:
. That's how key value observing works. All we do in this method is updating the value of the view controller's podcast
property.
當用戶默認數據庫中key值為 MTPodcast 的value發送變化的時候,視圖控制器可以在observeValueForKeyPath:ofObject:change:context: 方法中響應這種變化。這就是KVO。我們在這個方法中修改podcast 屬性的值。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@MTPodcast]) { self.podcast = [object objectForKey:@MTPodcast]; } }
When working with key value observing, it is instrumental to be aware of memory management and retain cycles. In this case, it means that we need to remove the view controller as an observer when the view controller is deallocated.
當使用KVO的時候,要註意內存管理和避免出現 retain cycle。在這個例子中,意味著我們需要在 delloc 方法中解除這個觀察者對象。
- (void)dealloc { [[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:@MTPodcast]; }
5. Fetching and Parsing the Feed
Step 1: Adding Dependencies
The response we get back from the iTunes Search API includes a feedUrl
attribute for each podcast. We could manually fetch the feed and parse it. However, to save some time, we'll make use of MWFeedParser, a popular library that can do this for us. You can manually download and include the library in your project, but I am going to opt for Cocoapods. I prefer Cocoapods for managing dependencies in iOS and OS X projects. You can read more about Cocoapods on its website or on Mobiletuts+.
我們從 iTunes 搜索得到的響應中,每一個播客都包含一個 feedUrl 屬性。 我們可以手動提取並進行解析。不過,為瞭提高效率,我們可以使用 MWFeedParser 這個流行的庫。你可以手動下載並添加到項目中。但是我更喜歡在iOS 和 OS X 中使用 Cocoapods ,可以在 its website 和 Mobiletuts+ 獲取更詳細的信息。
譯者註:
CocoaPods is the dependency manager for Objective-C projects. It has thousands of libraries and can help you scale your projects elegantly.
關於使用 CocoaPods 來做包依賴管理的有關內容,可以參考如下:
使用CocoaPods來做iOS程序的包依賴管理
CocoaPods進階:本地包管理
OBJC依賴庫管理利器cocoapods 安裝及使用詳細圖解
https://github.com/CocoaPods/CocoaPods/wiki
I am going to assume the Cocoapods gem is installed on your system. You can find detailed instructions in this tutorial.
Quit Xcode, navigate to the root of your Xcode project, and create a file named Podfile. Open this file in your text editor of choice and add the following three lines of code. In the first line, we specify the platform and the deployment target, which is iOS 7 in this example. The next two lines each specify a dependency of our Xcode project. The first one is the MWFeedParser library and I've also included the popular SVProgressHUD library, which will come in handy a bit later.
退出Xcode,導航定位到項目的更目錄下,創建一個名為 Podfile 文件。用文本編輯器打開這個文件,添加如下的三行代碼。第一行代碼指定開發平臺和部署目標位iOS 7.接下來的兩行指定該Xcode項目的依賴庫,第一個是MWFeedParser 庫,還有流行的 SVProgressHUD 庫。
platform :ios, '7' pod 'MWFeedParser' pod 'SVProgressHUD'
Open a Terminal window, navigate to the root of your Xcode project, and execute the command pod install
. This should install the dependencies and create an Xcode workspace. When Cocoapods is finished installing the project's dependencies, it tells you to use the workspace it created for you. This is important so don't ignore this advice. In the root of your Xcode project, you will see that Cocoapods has indeed created an Xcode workspace for you. Double-click this file and you should be ready to go.
打開終端窗口,導航到 Xcode 項目的根目錄下,然後執行命令 pod install 。這將會下載項目所需要的依賴庫,並創建一個Xcode 的 workspace。當 完成下載項目所需的依賴庫的時候,它會提示打開這個新創建的 workspace。特別要註意這個提示。在Xcode 的項目根目錄下就會看到確實新增瞭 Xcode 項目的 workspace。雙擊 workspace 文件打開。
Step 2: Fetching and Parsing the Feed
Open the implementation file of the MTViewController
class, add an import statement for MWFeedParser and SVProgressHUD, and declare two properties, episodes
andfeedParser
. We also need to make MTViewController
conform to theMWFeedParserDelegate
protocol.
打開 類文件,添加 MWFeedParser 和 SVProgressHUD 的 import 導入語句,聲明兩個屬性 episodes 和 feedParser 。同時讓 MTViewController 遵循 MWFeedParserDelegate 協議。
#import MTViewController.h #import MWFeedParser.h #import SVProgressHUD.h @interface MTViewController () @property (strong, nonatomic) NSDictionary *podcast; @property (strong, nonatomic) NSMutableArray *episodes; @property (strong, nonatomic) MWFeedParser *feedParser; @end
Next, we update setPodcast:
by invoking fetchAndParseFeed
, a helper method in which we use the MWFeedParser
class to fetch and parse the podcast's feed.
接著,在 setPodcast: 方法中調用 fetchAndParseFeed ,在這個方法中,使用 MWFeedParser類的方法獲取並解析podcast's feed。
- (void)setPodcast:(NSDictionary *)podcast { if (_podcast != podcast) { _podcast = podcast; // Update View [self updateView]; // Fetch and Parse Feed [self fetchAndParseFeed]; } }
In fetchAndParseFeed
, we get rid of our current MWFeedParser
instance if we have one and initialize a new instance with the podcast's feed URL. We set the feedParseType
property to ParseTypeFull
and set the view controller as the feed parser's delegate. Before we fetch the feed, we use SVProgressHUD
to show a progress HUD to the user.
在 fetchAndParseFeed 方法中,為瞭避免使用已經存在的 MWFeedParser 類實例,根據 podcast 的 feed URL 重新創建一個新的實例對象。設置 feedParseType 屬性為 ParseTypeFull ,該 view controller 為 feed 解析的委托對象。在開始獲取 feed之前,使用 SVProgressHUD 顯示進度。
- (void)fetchAndParseFeed { if (!self.podcast) return; NSURL *url = [NSURL URLWithString:[self.podcast objectForKey:@feedUrl]]; if (!url) return; if (self.feedParser) { [self.feedParser stopParsing]; [self.feedParser setDelegate:nil]; [self setFeedParser:nil]; } // Clear Episodes if (self.episodes) { [self setEpisodes:nil]; } // Initialize Feed Parser self.feedParser = [[MWFeedParser alloc] initWithFeedURL:url]; // Configure Feed Parser [self.feedParser setFeedParseType:ParseTypeFull]; [self.feedParser setDelegate:self]; // Show Progress HUD [SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeGradient]; // Start Parsing [self.feedParser parse]; }
We also need to implement two methods of the MWFeedParserDelegate
protocol,feedParser:didParseFeedItem:
and feedParserDidFinish:
. InfeedParser:didParseFeedItem:
, we initialize the episodes
property if necessary and pass it the feed item that the feed parser hands to us.
需要實現 MWFeedParserDelegate 協議的兩個方法:feedParser:didParseFeedItem: 和 feedParserDidFinish: 。在 feedParser:didParseFeedItem: 方法中:
- (void)feedParser:(MWFeedParser *)parser didParseFeedItem:(MWFeedItem *)item { if (!self.episodes) { self.episodes = [NSMutableArray array]; } [self.episodes addObject:item]; }
In feedParserDidFinish:
, we dismiss the progress HUD and update the table view. Did you say table view? That's right. We need to add a table view and implement the necessary UITableViewDataSource
protocol methods.
在 feedParserDidFinish: 方法中,隱藏進度顯示,同時更新 table view。這樣就需要在 view controller 中添加一個 table view,同時實現 UITableViewDataSource 協議中的方法。
- (void)feedParserDidFinish:(MWFeedParser *)parser { // Dismiss Progress HUD [SVProgressHUD dismiss]; // Update View [self.tableView reloadData]; }
Step 3: Displaying the Feed
Before we update the user interface, open MTViewController.h
, declare an outlet for the table view, and tell the compiler the MTViewController
class conforms to theUITableViewDataSource
and UITableViewDelegate
protocols.
在更新UI界面之前,打開 MTViewController.h ,聲明一個 table view 的 outlet,並讓 MTViewController 類遵循 UITableViewDataSource 和 UITableViewDelegate 協議。
#import @interface MTViewController : UIViewController @property (weak, nonatomic) IBOutlet UITableView *tableView; @end
Open the main storyboard one more time and add a table view to the view controller's view. Connect the table view's dataSource
and delegate
outlets with the view controller and connect the view controller's tableView
outlet with the table view. Select the table view, open the Attributes Inspector, and set the number of prototype cells to 1
. Select the prototype cell, set its style to Subtitle, and give it an identifier of EpisodeCell.
再次打開 storyboard,添加一個 table view。連接 table view 的 dataSource 和 delegate ,同時連接其到文件中的 tableView outlet。選中 table view ,打開 Attributes Inspector,設置 the number of prototype cells 為 1.同時設置其類型 style 為 Subtitle,identifier 為 EpisodeCell。
Before we implement the UITableViewDataSource
protocol, declare a static string named EpisodeCell
in MTViewController.m. This corresponds with the identifier we set for the prototype cell in the storyboard.
實現 UITableViewDataSource 協議之前,在 MTViewController.m 中聲明一個靜態字符串常量 EpisodeCell 。這和之前在 storyboard 中設置 cell 的 identifier 相同。
static NSString *EpisodeCell = @EpisodeCell;
Implementing the UITableViewDataSource
protocol is simple as pie and very similar to how we implemented the protocol in the search view controller. The only difference is that the episodes
variable contains instances of the MWFeedItem
class instead ofNSDictionary
instances.
實現 UITableViewDataSource 協議和之前在 search view controller 中實現該協議類似,隻不過 episodes 變量包含的是 MWFeedItem 類實例,而非 MWFeedItem 實例。
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.episodes ? 1 : 0; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.episodes ? self.episodes.count : 0; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:EpisodeCell forIndexPath:indexPath]; // Fetch Feed Item MWFeedItem *feedItem = [self.episodes objectAtIndex:indexPath.row]; // Configure Table View Cell [cell.textLabel setText:feedItem.title]; [cell.detailTextLabel setText:[NSString stringWithFormat:@%@, feedItem.date]]; return cell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
Run the application in the iOS Simulator or on a physical device and run it through its paces. You should now be able to search for podcasts, select a podcast from the list, and see its episodes.
在模擬器或真機上運行該項目程序,你應該可以 search for podcasts, select a podcast from the list, and see its episodes.
Conclusion
We've done a lot in this tutorial, but we still have quite a bit of work in front of us. In the next tutorial, we zoom in on downloading episodes from the feed and we'll discuss background or out-of-process downloads. Stay tuned.
在本篇教程中,我們做瞭很多,但仍有很多工作擺在我們面前。在下一篇教程中,我們深入瞭解下載任務,討論有關後臺下載和線程外下載的內容,敬請關註。