看板 Knuckles_note
作者 標題 [Xcode] iOS APP 初學: 熱門文章閱讀器 2
時間 2014年07月30日 Wed. PM 07:15:54
這是 iOS APP 初學: 網路文章閱讀器 的第二篇
第一篇在這: http://disp.cc/b/11-7U4v
在第一篇教學已經知道如何用 table view 產生動態列表
也知道如何在 -tableView:cellForRowAtIndexPath
設定 table view 的每個 row 裡要顯示什麼內容
這一篇要學習如何使用第三方的程式 AFNetworking
下載網路上的資料放進每個 row 的 cell 裡
然後點了 row 後要如何使用 web view 瀏覽網路上的內容
◎ 熱門文章的 JSON檔格式
先看一下我們要抓的 JSON檔 http://disp.cc/api/hot_text.json
這是由 Server 端產生的 API,裡面的資料格式要由網頁後端程式(例如PHP)來產生
在這邊他的格式是像這樣
{ "err":0, "list":[Row1, Row2, ..., RowN] }
最外層是一個JS物件,裡面有兩個值"err" 是代表產生的檔案有沒有錯誤,沒問題的話就是 0
"list" 為一個JS陣列,裡面存了N個JS物件,記錄了每篇熱門文章的資料
他的格式是像這樣
Row1 = {"hot_num":"100","bi":"163","ti":"7Zf3","title":"文章標題","board_name":"看板名稱","author":"作者帳號","desc":"文章摘要","img_list":["縮圖網址1","縮圖網址2","縮圖網址3"]}
hot_num 是這篇文章目前的人氣值bi 是看板的編號, ti是文章的編號, 文章的網址為 http://disp.cc/m/{bi}-{ti}
在 Objective-C 裡,要將 JS物件 存成 NSDictionary
將 JS陣件 存成 NSArray 或 NSMutableArray
◎ 使用 AFNetworking
用 AFNetworking 可以很容易的將 JSON檔 轉成 NSDictionary 和 NSArray
首先到 AFNetworking 的 GitHub 頁,點「Download Zip」下載
https://github.com/AFNetworking/AFNetworking
解壓縮後,將裡面的兩個資料夾:「AFNetworking」、「UIKit+AFNetworking」
拖至專案的資料夾
在跳出來的對話視窗,要點選「Copy items into...」以及「Create groups for ...」
完成後像這樣:
其中 UIKit+AFNetworking 資料夾是把一些 UI 元件的 class 修改成可以有網路功能
例如這個 APP 就會用到 UIImageView+AFNetworking 讓 UIImageView 元件可以顯示網路上的圖
接著我們要在 HotTextViewController 這個頁面產生時
使用 AFNetworking 下載熱門文章的JSON檔: http://disp.cc/api/hot_text.json
轉成 NSMutableArray 後存成 HotTextViewController 這個類別的成員變數
再用 HotTextViewController 的成員函數 -tableView:cellForRowAtIndexPath
將熱門文章的內容填入每個 row 的 cell 裡
首先編輯 HotTextViewController.h 檔
在 @interface HottextViewController : UITableViewController
和 @end 的中間,加上一個成員變數,用來存抓下來的熱門文章列表
@property (nonatomic, strong) NSMUtableArray *hotTexts;
編輯 HotTextViewController.m 檔
在 #import "HotTextCell.h" 這行下面加上
#import "AFNetworking.h"
這樣就可以使用 AFNetworking 提供的類別了在 @implementation HotTextViewController 這行下面
新增一個類別的成員函數 -loadHotTexts
- (void)loadHotTexts
{
// 設定成員變數 hotTexts 的初始大小為20,用來存抓下來的熱門文章列表
self.hotTexts = [NSMutableArray arrayWithCapacity:20];
// 將網址字串轉為 NSURLRequest 物件 request
NSString *urlString = @"http://disp.cc/api/hot_text.json";
// 使用 AFNetworking 的類別 AFHTTPRequestOperationManager 來建立物件 manager
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 設定將傳回的資料從 JSON 轉為 NSDictionary
manager.responseSerializer = [AFJSONResponseSerializer serializer];
// 設定 manager 執行成功及失敗後要做什麼
[manager GET:urlString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
// 將抓回來的資料存成 NSDictionary 的物件 data
NSDictionary *data = (NSDictionary *) responseObject;
// 用 NSLog 顯示錯誤訊息
NSLog(@"err:%@",data[@"err"]);
// 將 data 裡的陣列 list 存到 hotTextViewController 的成員變數 hotTexts
self.hotTexts = data[@"list"];
// 重新顯示 tableView
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// opration 執行失敗的話用 UIAlertView 顯示錯誤訊息
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving HotTexts"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alertView show];
}];
}
{
// 設定成員變數 hotTexts 的初始大小為20,用來存抓下來的熱門文章列表
self.hotTexts = [NSMutableArray arrayWithCapacity:20];
// 將網址字串轉為 NSURLRequest 物件 request
NSString *urlString = @"http://disp.cc/api/hot_text.json";
// 使用 AFNetworking 的類別 AFHTTPRequestOperationManager 來建立物件 manager
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 設定將傳回的資料從 JSON 轉為 NSDictionary
manager.responseSerializer = [AFJSONResponseSerializer serializer];
// 設定 manager 執行成功及失敗後要做什麼
[manager GET:urlString parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
// 將抓回來的資料存成 NSDictionary 的物件 data
NSDictionary *data = (NSDictionary *) responseObject;
// 用 NSLog 顯示錯誤訊息
NSLog(@"err:%@",data[@"err"]);
// 將 data 裡的陣列 list 存到 hotTextViewController 的成員變數 hotTexts
self.hotTexts = data[@"list"];
// 重新顯示 tableView
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// opration 執行失敗的話用 UIAlertView 顯示錯誤訊息
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Error Retrieving HotTexts"
message:[error localizedDescription]
delegate:nil
cancelButtonTitle:@"Ok"
otherButtonTitles:nil];
[alertView show];
}];
}
修改 -viewDidLoad
此成員函數會在頁面產生時執行
在 [super viewDidLoad]; 這行下面加上
[self loadHotTexts];
就會在熱門文章的頁面產生時執行 -loadHotTexts 了修改 -tableView:numberOfRowsInSection
將內容改為
return [self.hotTexts count];
將 Rows 的數目改為載入的陣列 hotTexts 的大小修改 -tableView:cellForRowAtIndexPath:
將之前加上的
cell.titleLabel.text = ...
cell.descLabel.text = ...
這兩行刪掉,改成這三行
NSDictionary *hotText = self.hotTexts[indexPath.row];
cell.titleLabel.text = hotText[@"title"];
cell.descLabel.text = hotText[@"desc"];
cell.titleLabel.text = hotText[@"title"];
cell.descLabel.text = hotText[@"desc"];
其中第一行是將之前載入的陣列 hotTexts 取出第 indexPath.row 個值
存成 NSDictionary 的物件 hotText
再將其中的 title 與 desc 存入 cell 的屬性
執行一下看看有沒有成功
接下來要讀取縮圖的網址並設定在 cell 裡的 UIImageView
要讓 UIImageView 可以顯示網路上的圖,必需使用 UIImageView+AFNetworking
在檔案上方的 #import "AFNetworking.h" 的下一行再加上
#import "UIImageView+AFNetworking.h"
然後在 -tableView:cellForRowAtIndexPath: 裡
刪除之前加的 cell.thumbImageView.image = ... 這行
改為
//取出hotText裡的陣列 img_list,可能會有0~3個縮圖
NSArray *img_list = hotText[@"img_list"];
if ([img_list count]) { //如果有縮圖的話
//在網路上的圖還沒載入前,先顯示 displogo
UIImage *placeholderImage = [UIImage imageNamed:@"displogo120.png"];
//取得陣列 img_list 裡的第一個縮圖網址,轉為 NSURLRequest
NSString *imgUrlString = img_list[0];
NSURL *url = [NSURL URLWithString:imgUrlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//載入網路上的縮圖,並設定到 cell 的 thumbImageView
__weak HotTextCell *weakCell = cell;
[cell.thumbImageView setImageWithURLRequest:request
placeholderImage:placeholderImage
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
weakCell.thumbImageView.image = image;
[weakCell setNeedsLayout];
} failure:nil
];
} else { //沒有縮圖的話,使用 displogo 當縮圖
cell.thumbImageView.image = [UIImage imageNamed:@"displogo120.png"];
}
NSArray *img_list = hotText[@"img_list"];
if ([img_list count]) { //如果有縮圖的話
//在網路上的圖還沒載入前,先顯示 displogo
UIImage *placeholderImage = [UIImage imageNamed:@"displogo120.png"];
//取得陣列 img_list 裡的第一個縮圖網址,轉為 NSURLRequest
NSString *imgUrlString = img_list[0];
NSURL *url = [NSURL URLWithString:imgUrlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//載入網路上的縮圖,並設定到 cell 的 thumbImageView
__weak HotTextCell *weakCell = cell;
[cell.thumbImageView setImageWithURLRequest:request
placeholderImage:placeholderImage
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
weakCell.thumbImageView.image = image;
[weakCell setNeedsLayout];
} failure:nil
];
} else { //沒有縮圖的話,使用 displogo 當縮圖
cell.thumbImageView.image = [UIImage imageNamed:@"displogo120.png"];
}
打開 storyboard 將縮圖的 UIImageView 的屬性設定為
Mode: Aspect Fill,等比例縮小到長或寬其中一個符合顯示範圍
Drawing 裡的 Clip Subviews 要打勾,裁切掉超出顯示範圍的部份
執行看看
接下來要加上點一下熱門文章的列表後,會開啟的 web view 頁面
到 storyboard,拖一個 View Controller 進來
點 HotTextCell,按著 Ctrl鍵 拉到新的 View Controller,選 Selction Segue 的 push
發現 cell 的右邊多了個鍵頭,把本來的 title 和 desc 擋住了
可以到 cell 的屬性設定,將 Accessory 改成 None
順便將 Selection 改成 Gray,點選後底色變灰色
還有每個 Row 之間的分隔線,預設在左邊有15px的間距
可以在 Separator Insets 這邊選「Custom」後,將 Left 改成 0
點兩下新的 View Controller 上面的 Navigation,輸入「閱讀文章」
拉一個 Web View 進來,大小就跟設為跟 View Controller 一樣大
幫新的 View Controller 建立一個類別
按 command+n 新增 Objective-C class 檔案
類別名稱 Class: TextViewController
繼承自 Subclass of: UIViewController
新增完後,編輯 TextViewController.h
在 @interface TextViewController : UIViewController 這行下面
幫這個類別新增兩個成員變數
@property (nonatomic, weak) IBOutlet UIWebView *webView;
@property (nonatomic, strong) NSString *urlString;
@property (nonatomic, strong) NSString *urlString;
其中 webView 有加 IBOutlet,是用來連結 storyboard 上的 Web View
urlString 是用來儲存文章網址,網址會從熱門文章頁傳來,再設定到 webView 上
到 storyboard,點一下新的 View Controller,再點 Identity設定
設定 Custom Class 為 TextViewController
再點一下 Connections設定,裡面的 Outlets 有我們剛剛加的 webView
將右邊的圈圈拖到閱讀文章頁上面的 Web View
再來我們要讓熱門文章列表頁點擊時,會將文章網址傳給閱讀文章頁
編輯 HotTextViewcontroller.m
在上方加上
#import "TextViewController.h"
將最後面預先產生的 -prepareForSegue:sender: 的註解/* */拿掉,並修改為
#pragma mark - Navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//從成員變數 hotTexts 中,利用目前點選第幾個 Row,來取得點選文章的網址
NSDictionary *hotText = self.hotTexts[self.tableView.indexPathForSelectedRow.row];
NSString *urlString = [NSString stringWithFormat:@"http://disp.cc/m/%@-%@", hotText[@"bi"], hotText[@"ti"]];
//透過 segue 取得目標頁面的 View Controller,將網址存到該類別的屬性
TextViewController *textViewController = segue.destinationViewController;
textViewController.urlString = urlString;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
//從成員變數 hotTexts 中,利用目前點選第幾個 Row,來取得點選文章的網址
NSDictionary *hotText = self.hotTexts[self.tableView.indexPathForSelectedRow.row];
NSString *urlString = [NSString stringWithFormat:@"http://disp.cc/m/%@-%@", hotText[@"bi"], hotText[@"ti"]];
//透過 segue 取得目標頁面的 View Controller,將網址存到該類別的屬性
TextViewController *textViewController = segue.destinationViewController;
textViewController.urlString = urlString;
}
在點擊 table view 上的 row 後,就會執行 -prepareForSegue:sender:
然後再換到新的頁面
現在我們已經將網址傳到閱讀文章頁的 TextViewController 了
接著我們要把網址設定到 web view 上
修改 TextViewController.m
在前面的 #import "TextViewController.h" 下一行加上
#import "AFNetworking.h"
#import "UIWebView+AFNetworking.h"
這樣就可以使用 AFNetworking 修改過的 UIWebView#import "UIWebView+AFNetworking.h"
接著修改 -viewDidLoad
在 [super viewDidLoad] 這行下面加上
NSURL *url = [NSURL URLWithString:self.urlString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:urlRequest progress:nil success:^NSString *(NSHTTPURLResponse *response, NSString *HTML) {
return HTML;
} failure:^(NSError *error) {
NSLog(@"error: %@",[error localizedDescription]);
}];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:urlRequest progress:nil success:^NSString *(NSHTTPURLResponse *response, NSString *HTML) {
return HTML;
} failure:^(NSError *error) {
NSLog(@"error: %@",[error localizedDescription]);
}];
這樣就可以在閱讀文章頁載入時,將網址傳給 web View,並載入該網頁了
按 command+r 執行看看g
◎ 顯示"載入中..."的圖示
在載入網頁時,如果網路很慢,程式會像沒反應一樣
應該要加個載入中的圖示比較好
而 AFNetworking 有提供一個簡單的方法
修改 DispAppDelegate.m
在 #import "DispAppDelegate.h" 的下一行加上
#import "AFNetworkActivityIndicatorManager.h"
然後修改 - application:didFinishLaunchingWithOptions: 這個成員函數
在 return YES; 前一行加上
AFNetworkActivityIndicatorManager.sharedManager.enabled = YES;
這樣就可以在程式執行後,啟動 AFNetworkActivityIndicatorManager
之後只要是用 AFNetworking 來做存取網路的動作時
程式的上方就會出現轉圈圈的圖示了
但是在開啟 web view 時,只有一開始會顯示一下載入中
接著 web view 裡的網頁再載入其他檔案,像是圖檔、JS檔時,就不會顯示載入中了
需要修改一下 UIWebView 的 delegate 成員函數
讓 web view 可以在載入檔案時,通知 AFNetworkActivityIndicatorManager
修改 TextViewController.h
將 @interface TextViewController : UIViewController 這行改為
@interface TextViewController : UIViewController <UIWebViewDelegate>
也就是在後面加一個 <UIWebViewDelegate> 讓類別 TextViewController 可以執行 UIWebView 的 delegate 成員函數
修改 TextViewController.m
前面的 #import 再加上一行
#import "AFNetworkActivityIndicatorManager.h"
才能使用 AFNetworkActivityIndicatorManager修改成員函數 -viewDidLoad,裡面再加一行
self.webView.delegate = self;
設定 webView 會將 delegate 事件傳給 TextViewController接著在後面新增三個 UIWebview 的 delegate 成員函數
- (void)webViewDidStartLoad:(UIWebView *)webView{
// web view 中有載入事件時,將 manager 的計數器+1
[AFNetworkActivityIndicatorManager.sharedManager incrementActivityCount];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView{
// web view 中的載入事件結束時,將 manager 的計數器-1
[AFNetworkActivityIndicatorManager.sharedManager decrementActivityCount];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{
// web view 中的載入事件失敗時,將 manager 的計數器-1
[AFNetworkActivityIndicatorManager.sharedManager decrementActivityCount];
}
// web view 中有載入事件時,將 manager 的計數器+1
[AFNetworkActivityIndicatorManager.sharedManager incrementActivityCount];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView{
// web view 中的載入事件結束時,將 manager 的計數器-1
[AFNetworkActivityIndicatorManager.sharedManager decrementActivityCount];
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{
// web view 中的載入事件失敗時,將 manager 的計數器-1
[AFNetworkActivityIndicatorManager.sharedManager decrementActivityCount];
}
其中 manager 計數器原本為0,只要變成大於0的話,程式上方就會顯示載入中
所以產生了幾個連線加了多少,就要有一樣的連線結束將數字減回來,載入中才會消失
但若是還在載入中時,點了上一頁的話,載入中就不會消失
可以再加上成員函數 -viewWillAppear: ,在 view 要被關閉前,先將載入中的圖示關掉
-(void) viewWillAppear:(BOOL)animated {
while ([AFNetworkActivityIndicatorManager.sharedManager isNetworkActivityIndicatorVisible]){
[AFNetworkActivityIndicatorManager.sharedManager decrementActivityCount];
}
}
while ([AFNetworkActivityIndicatorManager.sharedManager isNetworkActivityIndicatorVisible]){
[AFNetworkActivityIndicatorManager.sharedManager decrementActivityCount];
}
}
◎ 設定 Auto Layout
到這邊大致上已經完成了,但還有點問題就是
如果把螢幕橫著放 (在 XCode 的模擬器按command+→)
會變成像這樣
版面沒有自動調整
如果不想讓APP可以轉方向的話
可以在專案設定 → Deployment Info → Device Orientation
將 Landscape Left 與 Landscape Right 取消勾選即可
如果想要版面會自動調整的話,就要在 storyboard 設定 auto layout
到 storyboard,點一下 File設定
確認這邊的 Use Auto Layout 有勾選 (預設就是使用 Auto Layout)
先點選熱門文章頁的 Label - title
我們希望他能在橫放時自動變寬
點一下下面的「Pin」
點一下上、左、右,三個方向的虛線,讓他變成實線
點一下左間距的下拉箭頭,選擇是與「Content View」的間距,而不是 Image View
點一下「Height」固定高度後,點「Add 4 Contraints」
接著幫 Label - desc 加上四個方向的 Contraints
左邊一樣要改成與 Content View 的間距
上方的間距就保持為與 Label - title 的間距即可
加上這些 Constraints 後,當版面大小改變,例如 cell 變寬時
Constraints 的設定會優先於元件的 Size 設定
所以 title 和 desc 的左右間距不會變,而是寬度會跟著變寬
由於元件只要設了左右的 Constraints 後,就會出現上下的位置不確定的警告訊息
所以雖然 cell 的高度不會變,但還是得設定上下的 Constraints
注意加了 Constraints 後,如果又調整了 Label - title 的位置或寬高
會出現警告:「Frame for "label -title" will be different at run time.」
只要點選 Label - title 後,再點下面的「Resolve Auto Layout Issues」按鈕
選「Update Constraints」即可將 Constraints 的值重新設定
執行看看,在模擬器按 command+→
在閱讀文章頁,將 Web View 也加上四個方向的 Constraints
執行看看
◎ 設定 iPad 的 storyboard
到這邊已經沒什麼問題了
不過只有設定 iPhone 的 storyboard 而已
還要在 iPad 的 storyboard 照前面的步驟再全部設定一次
- 刪除預設的空白 View Controller 頁面
- 拉一個 Navigation Controller 進來,後面會附帶一個 Table View Controller
- Table View 上方的 Navigation Item,屬性設定的 Title:「Disp BBS」,Back Button:「主選單」
- Table View 的屬性設定,Content:「Static Cells」
- Table View Section 的屬性設定,Rows:「1」,Header:「看板討論區」
- Tabel View Cell 的屬性設定,Style:「Basic」,Image:「HotText.png」
Size設定,勾選「Custom」,Row Height:「80」
- Label - Title 的 Text 改為「熱門文章」,Font:「System 60.0」,Alignment:「置中」
- 再拉一個新的 Table View Controller 進來
- 按住 Ctrl鍵,將前一個 Table View 的 Row 拖到新的 Table View,選 Selection Segue 的「Push」
- 新的 Table View 上方的 Navigation Item,屬性設定 Title:「熱門文章」,Back Button:「回列表」
- 新的 Table View Controller 的 Identity設定,Custom Class 為「HotTextViewController」
- Table View 的 Size設定,Row Height:100
- Table View Cell 的 Identity設定,Custom Class:「HotTextCell」
屬性設定,Identifier:「HotTextCell」
- 拉一個 Image View 與兩個 Label 進 HotTextCell 裡
Image View 的屬性設定,Mode:「Aspect Fill」, Drawing: 勾「Clip Subviews」
Size設定,X:15, Y:0, Width:150, Height:100
第一個 Label 的屬性設定,Text:「title」, Color: 淡藍色, Font:「System Bold 20.0」
Size設定,X:175, Y:5, Width:550, Height:25
第二個 Label 的屬性設定,Text:「desc」, Font:「System 17.0」, Lines:3
Size設定,X:175, Y:30, Width:550, Height:65
- HotTextCell 的 Connections設定,將 Outlets 裡的 thumbImageView、titleLabel、descLabel
右邊的圈圈分別拉至對應的三個元件
- 拉一個新的 View Controller 頁面進來
- 按住 Ctrl鍵,將熱門文章頁的 Hot Text Cell 拖到新的 View,選 Selection Segue 的「Push」
- 設定新的 View 上方的 Navigation Item,Title:「閱讀文章」
- 新的 View Controller 的 Identity設定,Custom Class 為「TextViewController」
- 拉一個 Web View 進 Text View Controller
- Text View Controller 的 Connections設定,將 Outlet 裡的 webView 右邊的圈圈拉至 UIWebView
- 設定熱門文章頁,title 與 desc 四個方向的 Constraints
- 設定閱讀文章頁,Web View 四個方向的 Constraints
最後的結果像這樣:
將模擬器改為 iPad Retina,執行看看
若螢幕不夠大的話,可以按 command+→ 轉成橫的
或是按 command+1~3 可調整模擬器的大小
完整程式: 點此下載
--
※ 作者: Knuckles 時間: 2014-07-30 19:15:54
※ 編輯: Knuckles 時間: 2015-10-19 05:20:12
※ 同主題文章:
07-14 18:46 □ [Xcode] iOS APP 初學: 網路文章閱讀器 (1/2)
● 07-30 19:15 □ [Xcode] iOS APP 初學: 網路文章閱讀器 (2/2)
回列表(←)
分享