顯示廣告
隱藏 ✕
看板 Knuckles_note
作者 Knuckles (站長 那克斯)
標題 [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];
       
    }];
}

修改 -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"];

其中第一行是將之前載入的陣列 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"];
    }

打開 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;

其中 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;
}

在點擊 table view 上的 row 後,就會執行 -prepareForSegue:sender:
然後再換到新的頁面

現在我們已經將網址傳到閱讀文章頁的 TextViewController 了
接著我們要把網址設定到 web view 上


修改 TextViewController.m
在前面的 #import "TextViewController.h" 下一行加上
#import "AFNetworking.h"
#import "UIWebView+AFNetworking.h"
這樣就可以使用 AFNetworking 修改過的 UIWebView

接著修改 -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]);
    }];

這樣就可以在閱讀文章頁載入時,將網址傳給 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];
}

其中 manager 計數器原本為0,只要變成大於0的話,程式上方就會顯示載入中
所以產生了幾個連線加了多少,就要有一樣的連線結束將數字減回來,載入中才會消失

但若是還在載入中時,點了上一頁的話,載入中就不會消失
可以再加上成員函數 -viewWillAppear: ,在 view 要被關閉前,先將載入中的圖示關掉
-(void) viewWillAppear:(BOOL)animated {
    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 照前面的步驟再全部設定一次

  1. 刪除預設的空白 View Controller 頁面
  2. 拉一個 Navigation Controller 進來,後面會附帶一個 Table View Controller
  3. Table View 上方的 Navigation Item,屬性設定的 Title:「Disp BBS」,Back Button:「主選單」
  4. Table View 的屬性設定,Content:「Static Cells」
  5. Table View Section 的屬性設定,Rows:「1」,Header:「看板討論區」
  6. Tabel View Cell 的屬性設定,Style:「Basic」,Image:「HotText.png」
                      Size設定,勾選「Custom」,Row Height:「80」
  7. Label - Title 的 Text 改為「熱門文章」,Font:「System 60.0」,Alignment:「置中」
  8. 再拉一個新的 Table View Controller 進來
  9. 按住 Ctrl鍵,將前一個 Table View 的 Row 拖到新的 Table View,選 Selection Segue 的「Push」
  10. 新的 Table View 上方的 Navigation Item,屬性設定 Title:「熱門文章」,Back Button:「回列表」
  11. 新的 Table View Controller 的 Identity設定,Custom Class 為「HotTextViewController」
  12. Table View 的 Size設定,Row Height:100
  13. Table View Cell 的 Identity設定,Custom Class:「HotTextCell」
                           屬性設定,Identifier:「HotTextCell」
  14. 拉一個 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
  15. HotTextCell 的 Connections設定,將 Outlets 裡的 thumbImageView、titleLabel、descLabel
    右邊的圈圈分別拉至對應的三個元件
  16. 拉一個新的 View Controller 頁面進來
  17. 按住 Ctrl鍵,將熱門文章頁的 Hot Text Cell 拖到新的 View,選 Selection Segue 的「Push」
  18. 設定新的 View 上方的 Navigation Item,Title:「閱讀文章」
  19. 新的 View Controller 的 Identity設定,Custom Class 為「TextViewController」
  20. 拉一個 Web View 進 Text View Controller
  21. Text View Controller 的 Connections設定,將 Outlet 裡的 webView 右邊的圈圈拉至 UIWebView
  22. 設定熱門文章頁,title 與 desc 四個方向的 Constraints
  23. 設定閱讀文章頁,Web View 四個方向的 Constraints

最後的結果像這樣:
[圖]
 

將模擬器改為 iPad Retina,執行看看
若螢幕不夠大的話,可以按 command+→ 轉成橫的
或是按 command+1~3 可調整模擬器的大小
[圖]
 


完整程式: 點此下載


--
※ 作者: Knuckles 時間: 2014-07-30 19:15:54
※ 編輯: Knuckles 時間: 2015-10-19 05:20:12
※ 看板: KnucklesNote 文章推薦值: 2 目前人氣: 0 累積人氣: 6655 
※ 本文也出現在看板: ott
分享網址: 複製 已複製
( ̄︶ ̄)b showyou, yy33k 說讚!
ott 轉錄至看板 ott (使用連結) 時間:2014-08-10 18:33:30
showyou 轉錄至看板 Showyou (使用複製) 時間:2014-08-11 20:51:45
honercek 轉錄至某隱板 (使用複製) 時間:2014-08-11 23:09:42
r)回覆 e)編輯 d)刪除 M)收藏 ^x)轉錄 同主題: =)首篇 [)上篇 ])下篇