-->

2013年4月30日 星期二

使用php及mysql使app具備連線網路資料庫(web database)的能力 (三) - 撰寫 app連線 php

前面兩篇使用php及mysql使app具備連線網路資料庫(web database)的能力 (一) - 建構資料庫使用php及mysql使app具備連線網路資料庫(web database)的能力 (二) - 撰寫 php以 mysql管理資料庫。我們建置了mysql並且知道如何撰寫php去存取mysql。現在,終於可以開始撰寫app的部份了。

本篇會撰寫一個簡單的登入頁面,能夠進行註冊,以及登入,並會顯示註冊及登入成功或失敗。







要達成上述的功能,需要進行http request。雖說Objective-C本身就有提供完成這類工作的方法,但是使用起來太過繁雜,不容易使用。而網路上有許多open source提供了這方面的功能,筆者在此使用 AFNetworking來完成這部份的工作。因此,你必須先到這裡下載這個開放原始碼,稍後會在app中使用到。下載完成後,可以開始撰寫程式碼了!

建立專案,及使用者介面

首先,開一個single view樣板的專案,使用storyboard。並將UI設計成如下。筆者這裡提供已經設計好的原始碼,可到這裡下載。如果你懶得進行以下的實作內容,你也可以到這裡下載已經完成的程式碼。

對帳號密碼做簡單的認證

一般對於帳號密碼都會做一些限制,例如不能使用特殊符號,或者字數必須介於多少之間等等。在AAViewController.m中的@end前加入程式碼,這兩個方法分別是驗證帳號跟密碼是否滿足設定的條件。分別是帳號只能由a-z、0-9、A-Z等組成,且必須介於1-18字元之間。密碼則是必須介於6-18個字元之間。

- (BOOL)validateAccount:(NSString *)account{
NSString *regex = @"[A-Z0-9a-z]{1,18}";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];
return [predicate evaluateWithObject:account];
}
- (BOOL)validatePassword:(NSString *)password{
NSString *regex = @"[A-Z0-9a-z]{6,18}";
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];
return [predicate evaluateWithObject:password];
}
view raw gistfile1.m hosted with ❤ by GitHub

使用AFNetworking

前面筆者提過,此app使用AFNetworking做http request等動作。首先將AFNetworking資料夾拖曳進專案內。並在AAViewController.m中加入:

#import "AFHTTPClient.h"
#import "MBProgressHUD.h"
#define ServerApiURL @"http://localhost:8888/AddAccount/"
view raw gistfile2.m hosted with ❤ by GitHub

等等,為什麼還有一個MBProgressHUD.h?MBProgressHUD也是開放原始碼,其功能是像UIActivityIndicator一樣,但是有更酷炫的效果,有興趣的可以到這裡觀看詳細內容。最後我們定義了伺服器的位址,這個位址就是api.php的位址,沒錯就是在使用php及mysql使app具備連線網路資料庫(web database)的能力 (二) - 撰寫 php以 mysql管理資料庫中,我們所撰寫的php,類似但是其內容有些微的不同,稍後筆者會從新撰寫所需要的api.php。我們還是先來把app的部份搞定。

首先實作sign up這個button的action:

- (IBAction)signUpBtn:(id)sender {
//將鍵盤縮回
[_userIDTF resignFirstResponder];
[_passwordTF resignFirstResponder];
//判斷基本的認證結果
if ([self validateAccount:_userIDTF.text] && [self validatePassword:_passwordTF.text]) {
//啟動一個hud
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
//設定hud的顯示文字
[hud setLabelText:@"connecting"];
//取得userID及password
NSString *userID = _userIDTF.text;
NSString *password = _passwordTF.text;
//設定伺服器的根目錄
NSURL *hostRootURL = [NSURL URLWithString:ServerApiURL];
//設定POST的內容
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:@"signUp", @"cmd", userID, @"userID", password, @"password", nil];
//產生控制request的物件
AFHTTPClient *client = [[AFHTTPClient alloc]initWithBaseURL:hostRootURL];
//POST
[client postPath:@"api.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
//request成功之後要做的事情
//對responseObject編碼,並輸出結果。
NSString *response = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(@"response:%@",response);
NSError *err;
//將responseObject編碼成JSON格式
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:&err];
//取的api的key值,並輸出
NSDictionary *apiResponse = [json objectForKey:@"api"];
NSLog(@"apiResponse:%@",apiResponse);
//取的signUp的key值,並輸出
NSString *result = [apiResponse objectForKey:@"signUp"];
NSLog(@"result:%@",result);
//判斷signUp的key值是否等於success
if ([result isEqualToString:@"success"]) {
//顯示註冊成功
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"success.png"]];
[_statusLB setText:@"Status:sign up successed"];
}else {
//顯示註冊失敗
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"signUpFail.png"]];
[_statusLB setText:@"Status:sign up fail"];
}
[MBProgressHUD hideHUDForView:self.view animated:YES];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//request失敗要做的事情
NSLog(@"request error:%@",error);
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"connectError.png"]];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}];
}else {
//基本認證失敗
[_statusLB setText:@"validate fail"];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
}
view raw gistfile3.m hosted with ❤ by GitHub

這串程式碼相對較長,不過筆者已經加入註解,相信大家應該很容易看,一些瑣碎的細項,相信各位都可以看懂得。這裡直接解說15-54行的程式碼的作用。

這段程式碼,是產生client這個物件,並定義根目錄是hostRootURL。設定POST的key及keyvalue,這些key及keyvalue為什麼要這麼設定,稍後會做解釋。最後使用client的一個方法,將這些參數POST給api.php。

//設定伺服器的根目錄
NSURL *hostRootURL = [NSURL URLWithString:ServerApiURL];
//設定POST的內容
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:@"signUp", @"cmd", userID, @"userID", password, @"password", nil];
//產生控制request的物件
AFHTTPClient *client = [[AFHTTPClient alloc]initWithBaseURL:hostRootURL];
//POST
[client postPath:@"api.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
view raw gistfile4.m hosted with ❤ by GitHub

在postPath.....的方法中,提供兩個block分別是成功及失敗。其中的responseObject就是網頁的回應。可以透過stringWithData:encoding的方法將其轉換NSString,並以NSLog輸出,一般使用在開發階段,幫助除錯。由於筆者在api.php中設計其response要以JSON格式輸出。因此,筆者以NSJSONSerialization將responseObject轉成NSDictionary。並取其key值來判定註冊是否成功。

//request成功之後要做的事情
//對responseObject編碼,並輸出結果。
NSString *response = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(@"response:%@",response);
NSError *err;
//將responseObject編碼成JSON格式
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:&err];
//取的api的key值,並輸出
NSDictionary *apiResponse = [json objectForKey:@"api"];
NSLog(@"apiResponse:%@",apiResponse);
//取的signUp的key值,並輸出
NSString *result = [apiResponse objectForKey:@"signUp"];
NSLog(@"result:%@",result);
//判斷signUp的key值是否等於success
if ([result isEqualToString:@"success"]) {
//顯示註冊成功
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"success.png"]];
[_statusLB setText:@"Status:sign up successed"];
}else {
//顯示註冊失敗
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"signUpFail.png"]];
[_statusLB setText:@"Status:sign up fail"];
}
[MBProgressHUD hideHUDForView:self.view animated:YES];
view raw gistfile5.m hosted with ❤ by GitHub

failure的block也是一樣的,因此筆者就不再贅述。
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//request失敗要做的事情
NSLog(@"request error:%@",error);
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"connectError.png"]];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
view raw gistfile6.m hosted with ❤ by GitHub

再來進行"sign in" button的action實作,內容如下。如果你看懂上述的內容,相信你有能力看懂這個實作內容,你會發現,他們完全一樣。現在,這個app已經能夠溝通php,再透過php去控制mysql了。最後,還需要完成api.php的部份。

- (IBAction)signInBtn:(id)sender {
//將鍵盤縮回
[_userIDTF resignFirstResponder];
[_passwordTF resignFirstResponder];
//判斷基本的認證是否成功
if ([self validateAccount:_userIDTF.text] && [self validatePassword:_passwordTF.text]) {
//產生hud物件,並設定其顯示文字
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
[hud setLabelText:@"connecting"];
//存去userID及password
NSString *userID = _userIDTF.text;
NSString *password = _passwordTF.text;
//設定根目錄
NSURL *hostRootURL = [NSURL URLWithString:ServerApiURL];
//設定要POST的鍵值
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:@"signIn", @"cmd", userID, @"userID", password, @"password", nil];
//產生控制request的物件
AFHTTPClient *client = [[AFHTTPClient alloc]initWithBaseURL:hostRootURL];
//以POST的方式request並
[client postPath:@"api.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
//request成功之後要做的事情
//對responseObject編碼,並輸出結果。
NSString *response = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding];
NSLog(@"response:%@",response);
NSError *err;
//將responseObject編碼成JSON格式
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableLeaves error:&err];
//取的api的key值,並輸出
NSDictionary *apiResponse = [json objectForKey:@"api"];
NSLog(@"apiResponse:%@",apiResponse);
//取的signIn的key值,並輸出
NSString *result = [apiResponse objectForKey:@"signIn"];
NSLog(@"result:%@",result);
[MBProgressHUD hideHUDForView:self.view animated:YES];
//判斷signUp的key值是否等於success
if ([result isEqualToString:@"success"]) {
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"success.png"]];
[_statusLB setText:@"Status:sign in successed"];
}else {
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"signInFail.png"]];
[_statusLB setText:@"Status:sign in successed"];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"request error:%@",error);
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:5];
[imageView setImage:[UIImage imageNamed:@"connectError.png"]];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}];
}else {
[_statusLB setText:@"validate fail"];
[MBProgressHUD hideHUDForView:self.view animated:YES];
}
}
view raw gistfile7.m hosted with ❤ by GitHub

完成api.php

新增api.php,程式碼如下。筆者假設各位已經了解function及class的用法,前者就像是objective-c裡面的方法,後者則像是類別,若是初學者,可到這裡完成簡單的php課程。而function內的程式碼,在使用php及mysql使app具備連線網路資料庫(web database)的能力 (二) - 撰寫 php以 mysql管理資料庫都解說過了。因此,筆者只針對幾個新出現的方法做解說。

<?php
try {
require_once 'config.php';
$config = $server_config['db'];
sleep(3);
$api = new api($config);
$api->handleCommit();
$response['api'] = $api->api_response;
echo json_encode($response);
} catch (Exception $e) {
echo "error:" . $e;
}
class api {
private $pdo;
public $api_response;
// 產生物件時的初始話
function __construct($config) {
$this->pdo = new PDO(
'mysql:host=' . $config['host'] . ';dbname=' . $config['dbname'],
$config['username'],
$config['password']
);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->query('SET NAMES utf8');
}
function handleCommit() {
if (isset($_POST['cmd'])) {
switch (trim($_POST['cmd'])) {
case 'signIn': $this->handleSignIn(); return ;
case 'signUp': $this->handleSignUp(); return ;
};
}
}
function handleSignIn() {
if (isset($_POST['userID']) && isset($_POST['password'])) {
$this->pdo->beginTransaction();
$stmt = $this->pdo->prepare('SELECT * FROM test_users WHERE nickname = :userID AND secret_code = :password');
$stmt->bindValue(':userID', trim($_POST['userID']), PDO::PARAM_STR);
$stmt->bindValue(':password', trim($_POST['password']), PDO::PARAM_STR);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
$this->api_response['signIn'] = "success";
}else {
$this->api_response['signIn'] = "fail";
}
$this->pdo->commit();
}
}
function handleSignUp() {
if (isset($_POST['userID']) && isset($_POST['password'])) {
if ($this->validID(trim($_POST['userID']))) {
$this->pdo->beginTransaction();
$stmt = $this->pdo->prepare('INSERT INTO test_users (nickname, secret_code) VALUES (:userID, :password)');
$stmt->bindValue(':userID', trim($_POST['userID']), PDO::PARAM_STR);
$stmt->bindValue(':password', trim($_POST['password']), PDO::PARAM_STR);
$stmt->execute();
$this->pdo->commit();
$this->api_response['signUp'] = "success";
}else {
$this->api_response['signUp'] = "fail";
}
}else {
$this->api_response['signUp'] = "lost userID or password of cmd";
}
}
function validID($userID) {
$this->pdo->beginTransaction();
$stmt = $this->pdo->prepare('SELECT * FROM test_users WHERE nickname = :userID');
$stmt->bindValue(':userID', $userID, PDO::PARAM_STR);
$stmt->execute();
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if ($data) {
$this->api_response['validID'] = "existed";
$this->pdo->commit();
return false;
}else {
$this->api_response['validID'] = "new";
$this->pdo->commit();
return true;
}
}
}
view raw api.php hosted with ❤ by GitHub

第2行開始的try{...}catch(Expection $e){...},會執行try{...}的程式碼,若有錯誤,則會執行catch(Expection $e){...},並捕獲錯誤訊息在$e中。在11行筆者echo出錯誤訊息,若有發生錯誤,則api.php的response就會是錯誤訊息,還記得在上一小節中,AFNetworking的postPath...方法中的responseObject,api.php的response就會被擷取到其中。因此,將responseObject編碼並輸出,就可以知道這個request發生什麼事情了。

再try{..}中,一開始require的config.php就是伺服器的環境參數,跟使用php及mysql使app具備連線網路資料庫(web database)的能力 (二) - 撰寫 php以 mysql管理資料庫是一樣的。

而sleep(3)是讓程式碼暫時停頓,筆者用來模擬以個反應較慢的伺服器。接著產生一個api物件,並執行handleCommand這個方法。最後,產生一個陣列,以"api"這個key值對應api_response,api_response也是一個陣列,用來存放api中註冊及登入的結果。最後以json_encode將其輸出,也就是它是api.php的response,同上一段,ios app中會對其編碼,並輸出。

<?php
try {
require_once 'config.php';
$config = $server_config['db'];
sleep(3);
$api = new api($config);
$api->handleCommit();
$response['api'] = $api->api_response;
echo json_encode($response);
} catch (Exception $e) {
echo "error:" . $e;
}
view raw api2.php hosted with ❤ by GitHub

我們接著看class API{...},function __construct($config){...}是new一個API物件時,要給予的初始化方法,內容相信各位一定不陌生,我們在上一篇文章中一起做過了。接著看handleCommand,這是在try{...}中,產生完api物件之後,執行的方法,它做了什麼事?一開始它使用了isset,來判定$_POST['cmd']是否存在,isset的詳細資料可以看這裡。若存在的話,就進入switch中,使用trim來取得純string的部份,switch分兩個方法,一個是signIn令一個是signUp。

<?php
class api {
private $pdo;
public $api_response;
// 產生物件時的初始話
function __construct($config) {
$this->pdo = new PDO(
'mysql:host=' . $config['host'] . ';dbname=' . $config['dbname'],
$config['username'],
$config['password']
);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->query('SET NAMES utf8');
}
function handleCommit() {
if (isset($_POST['cmd'])) {
switch (trim($_POST['cmd'])) {
case 'signIn': $this->handleSignIn(); return ;
case 'signUp': $this->handleSignUp(); return ;
};
}
}
view raw api3.php hosted with ❤ by GitHub

還記得上述有提到要設定POST的key及keyvalue,我們以NSDictionary *param存放這些key-value。以下是param的物件程式碼。AFNetworking會將它POST給api.php並存在$_POST中。藉由這個方式,可以使用app來告訴php要執行什麼事情。當然,必須先把php建置好。後面的三個function內容都與使用php及mysql使app具備連線網路資料庫(web database)的能力 (二) - 撰寫 php以 mysql管理資料庫大同小異,筆者就不再贅述。

//設定POST的內容
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:@"signUp", @"cmd", userID, @"userID", password, @"password", nil];
view raw gistfile8.m hosted with ❤ by GitHub

現在,執行你的iphone模擬器,別忘了開啟伺服器,來測試編寫好的app吧!慢著,現在我們改到實機上測試,出現問題了,無法連接伺服器。你還必須修改一個東西,你的伺服器位址。打開系統設定->網路,找到你得IP位址去替換掉localhost。大功告成!現在可以在實機上對伺服器進行存取了。

注意:如果你跟筆者一樣使用浮動ip的話,記得再測試之前都要確認一下你的ip位址,避免不必要的錯誤。

//修改這個
#define ServerApiURL @"http://localhost:8888/AddAccount/"
//為這個
#define ServerApiURL @"http://192.168.x.x:8888/AddAccount/"
view raw gistfile9.m hosted with ❤ by GitHub

reference:

http://www.php.net/

https://github.com/AFNetworking/AFNetworking

https://github.com/jdg/MBProgressHUD

沒有留言:

張貼留言