前面兩篇使用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]; | |
} |
使用AFNetworking
前面筆者提過,此app使用AFNetworking做http request等動作。首先將AFNetworking資料夾拖曳進專案內。並在AAViewController.m中加入:
#import "AFHTTPClient.h" | |
#import "MBProgressHUD.h" | |
#define ServerApiURL @"http://localhost:8888/AddAccount/" |
等等,為什麼還有一個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]; | |
} | |
} |
這串程式碼相對較長,不過筆者已經加入註解,相信大家應該很容易看,一些瑣碎的細項,相信各位都可以看懂得。這裡直接解說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) { |
在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]; |
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]; | |
} |
再來進行"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]; | |
} | |
} |
完成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; | |
} | |
} | |
} |
第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; | |
} |
我們接著看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 ; | |
}; | |
} | |
} |
還記得上述有提到要設定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]; |
現在,執行你的iphone模擬器,別忘了開啟伺服器,來測試編寫好的app吧!慢著,現在我們改到實機上測試,出現問題了,無法連接伺服器。你還必須修改一個東西,你的伺服器位址。打開系統設定->網路,找到你得IP位址去替換掉localhost。大功告成!現在可以在實機上對伺服器進行存取了。
注意:如果你跟筆者一樣使用浮動ip的話,記得再測試之前都要確認一下你的ip位址,避免不必要的錯誤。
//修改這個 | |
#define ServerApiURL @"http://localhost:8888/AddAccount/" | |
//為這個 | |
#define ServerApiURL @"http://192.168.x.x:8888/AddAccount/" |
reference:
沒有留言:
張貼留言