2010年9月29日 星期三

iOS - Event Kit Framework

iOS 4.0 可以充許開發者去存取存iPhone裡的行事曆,
和通訊錄相同的行事曆的整個是用Core Foundation架構起來的是C 而不是Objective-C
Core Foundation是C, Foundation是Objective-C
但是兩者之間有很方便的轉換方法,其實就是直接cast,這個又叫做Toll-Free bridge
比如CFStringRef和NSString *要轉換就直接寫
CFStringRef cfString = xxxxxx;
NSString * objString =  (NSString *) cfString;
這樣就可以直接用objString來當成cfString的替身了
先介紹一下幾個重要的c structure在event kit 會用到的
CFGregorianDate : 標準的日曆表示法,含有年,月,日,時,分,秒。至於為什麼叫Gregorian 請參考
CFGregorianUnits : 表示一段時間,和CFGregorianDate有相同的結構,但沒有限制值的範圍。比如說在CFGregorianDate"時"的值不能超過24小時,但在CFGregorianUnits"時"的值就可以超過24小時,因為它代表的是一段時間
我們來直接來看一個按下去按鈕就新增自訂event的例子
-(IBAction) createEvent:(id)sender{
     NSLog(@"launched");
     EKEventStore * myEventStore = [[EKEventStore alloc] init];

     EKEvent * myEvent = [EKEvent eventWithEventStore:myEventStore];
     myEvent.title = @"Knowledage Convergence Meeting";

     CFGregorianDate gregorianStartDate, gregorianEndDate;
     CFGregorianUnits startUnits = {0, 0, 0, 1, 3, 0};
     CFGregorianUnits endUnits = {0, 0, 0, 1, 5, 0};
     CFTimeZoneRef timeZone = CFTimeZoneCopySystem();


     gregorianStartDate = CFAbsoluteTimeGetGregorianDate(
            CFAbsoluteTimeGetCurrent(), timeZone);

     gregorianStartDate.hour = 17;
     gregorianStartDate.minute = 55;
     gregorianStartDate.second = 10;

     gregorianEndDate = CFAbsoluteTimeGetGregorianDate(
     CFAbsoluteTimeAddGregorianUnits(CFAbsoluteTimeGetCurrent(), timeZone, endUnits),
           timeZone);
     gregorianEndDate.hour = 17;
     gregorianEndDate.minute = 55;
     gregorianEndDate.second = 25;

     NSDate* startDate =  [NSDate     dateWithTimeIntervalSinceReferenceDate:CFGregorianDateGetAbsoluteTime(gregorianStartDate,     timeZone)];
     NSDate* endDate =  [NSDate dateWithTimeIntervalSinceReferenceDate:CFGregorianDateGetAbsoluteTime(gregorianEndDate, timeZone)];

     CFRelease(timeZone);

     myEvent.startDate = startDate;
     myEvent.endDate = endDate;
     myEvent.calendar = [myEventStore defaultCalendarForNewEvents];

     EKAlarm *myAlarm = [EKAlarm alarmWithAbsoluteDate:startDate];

     myEvent.alarms = [NSArray arrayWithObject:myAlarm];
     NSError *theError = nil;
     [myEventStore saveEvent:myEvent span:EKSpanThisEvent error:&theError];
     NSLog(@"error is %@", theError);
}

這個流程主要就是產生自己的EKEvent, 藍色的property都是必要的,除了alarms之外,EKEvent又必需要和 EKEventStore結合,因為event實體是EKEventStore給的,event裡的calendar也是EKEventStore給的,而EKEventStore實體的產生就只是用alloc, init方式。
其中最麻煩的就是startDate和endDate,輾轉要從 CFGregorianDate 或是  CFGregorianUnits而來
單看是要從 CFAbsoluteTimeGetGregorianDate產生還是要從 CFAbsoluteTimeAddGregorianUnits產生,不過記得一點,產生 CFGregorianDate 的實體之後可以從他的field,year, month, day, hour, minute, second 來設定絕對的時間就如墨綠色部分所示。
EKAlarm 可有可無,它是先和NSDate結合,再一起被設定到EKEvent

iOS - 用POST Request上傳檔案給Server

在UIKit 就有提供功能很強大的class讓我們使用可以自訂HTTP Request的格式
讓我們可以傳送http request給server
接下來我們會用到幾個class
NSURL, NSMutableURLRequest, NSURLConnection
有關HTTP基本的知識,請參考
在iOS 手持式裝置中我們就是要用上面提到的Class來自訂
自已的Post Request並且把file 轉成NSData傳到server,要記得,要傳到遠端的東西都要轉成NSData
先來看看怎麼把file轉成NSData
============================
NSString * filePath = @"/Users/macuser/Desktop/back.png";
NSData *fileContent = [NSData dataWithContentsOfFile:filePath options:0 error:nil];
============================
很簡單地利用NSData的 dataWithContentsOfFile:options:error: 這個method把file轉成NSData
在這裡是利用simulator來拿桌面上的file,真正放到iOS裝置上時,要用合法位置的檔案
再來就是自訂POST Request的部分
============================
-(void) uploadBinary:(NSData * ) binary withURL:(NSString * ) urlAddress withAttName:(NSString *) attname andFilename:(NSString *) filename{
   
    NSURL *theURL = [NSURL URLWithString:urlAddress];
    NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:theURL
                                                              cachePolicy:NSURLRequestReloadIgnoringCacheData
                                                          timeoutInterval:20.0f];
    [theRequest setHTTPMethod:@"POST"];
    NSString *boundary = [[NSProcessInfo processInfo] globallyUniqueString];
    NSString *boundaryString = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
    [theRequest addValue:boundaryString forHTTPHeaderField:@"Content-Type"];
   
    // define boundary separator...
    NSString *boundarySeparator = [NSString stringWithFormat:@"--%@\r\n", boundary];
   
    //adding the body...
    NSMutableData *postBody = [NSMutableData data];

    [postBody appendData:[boundarySeparator dataUsingEncoding:NSUTF8StringEncoding]];
    [postBody appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",attname, filename] dataUsingEncoding:NSUTF8StringEncoding]];
    [postBody appendData:[@"Content-Type: application/octet-stream\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
    [postBody appendData:binary];
   
    [postBody appendData:[[NSString stringWithFormat:@"\r\n--%@--\r \n",boundary]
                          dataUsingEncoding:NSUTF8StringEncoding]];
    [theRequest setHTTPBody:postBody];   
   
   
    [[[NSURLConnection alloc] initWithRequest:theRequest delegate:self] autorelease];
}
============================
綠色的部分是Header,藍色的部分是Post body,其中boundary的部分都不能省略
  [theRequest setHTTPMethod:@"POST"];是設定POST Method
NSString *boundaryString = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary]; 
[theRequest addValue:boundaryString forHTTPHeaderField:@"Content-Type"];設定 Type

整個BODY都要用NSData傳出去,所以要宣告一個NSMutableData
@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",attname, filename
這一串是和Server說,POST的Key要叫做attname,這個file的名字要放在 filename(上列右邊)這個變數而server是用"filename"這個key把filename的值取出來

2010年9月25日 星期六

漫談HTTP

HTTP 全名就是Hyper Text Transfer Protocol
也就是我們最常用triple w, 電火,電鍋,電冰箱前面加的前置符號http://
://是用來和後面的domain name有所區分,至於為什麼要用//而不用/就好呢?
最近www的老爸 Time Berners-Lee 說:好像真得不必要用兩個/ 請參見
而在HTTP中最常用到GET 和 POST 這兩個Method
先來說說GET Method
如果我們在瀏覽器的網址打上http://www.scentsome.com/index.html?name=michael
Browser會幫我們把下方的訊息傳給 www.scentsome.com這台電腦

GET /index.html?name=michael  HTTP/1.1
Host: 127.0.0.1:3000
User-Agent: Mozilla/5.0 Gecko/20100914 Firefox/3.6.10
Accept: text/html,application/xhtml+xml,application/xml;
Accept-Language: zh-tw,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive

這個又叫做GET Request ,而GET 之後從Host開始叫做Header,標頭
因為www是client-server 的架構,一般都是由client請一個請求給server,server再回一個Response給client
這個是response的Header
HTTP/1.1 200
Date: Fri, 24 Sep 2010 14:52:36 GMT
Etag: "61b4e760d49edcc77c565ce63e1e7a09"
Content-Type: text/html; charset=utf-8
X-Runtime: 2
Content-Length: 13
Cache-Control: private, max-age=0, must-revalidate
對於GET來說呢,通常我們只關心
?name=michael
這個代表的是我們把name這個參數和michael這個參數值傳給server
在Server那就可以把name參數值取出來,達到動態依參數改變行為的目的
在Ruby on Rail
     params['name']
在PHP
    $_GET['name']
來得到michael這個值

至於什麼時候我們什麼時候會用改變Header的值呢?
這想我們就要提到POST method
一個最常見的例子就是from 表單
相信大家都有填過帳號/密碼的頁面吧?
這個頁就是用HTML form tag寫成的,
當大家把傳送/確認按鈕接下時,
Browser就會用POST 把資料傳到Server
POST Request 就有分兩個部分
Request Header
POST Body
Header 裡有個
Content-Type: application/x-www-form-urlencoded
預設就是冒號右邊那一串
如果是這種Type
Key-Value 這種配對的值
Name: Jonathan Doe
Age: 23
Formula: a + b == 13%!
就會被編碼成

Name=Jonathan+Doe&Age=23&Formula=a+%2B+b+%3D%3D+13%25%21
送出去,感覺就和GET一樣只是有編碼過不是用明文傳送

當我們要上傳檔案到server的時候就不像GET這麼簡單,幾個key-value的配對就好了
用程式連結HTTP Server的時候我們就要手動去更改POST Header的值好讓server知道我們要做的動作是上傳檔案,我們要傳給它的不是一個value而是一個file,一個binary file.
此時的Content-Type:就要用
Content-Type: multipart/form-data

接著在Body的地方也要手動給一些值
我們來看一個上傳file的POST Body要長什麼樣子
Content-Disposition: form-data; name="token"; filename="file.bin"
Content-Type: application/octet-stream


[file binary......]
-----------------------------19277021961952509530130060903
藍色的部分都是要和Server 溝通時,很重要的設定,不然server是會搞不清楚現在要傳的東西是什麼。
注意:要上傳的binary檔要緊接在Content-Type 之後,而且整個POST Body都要轉成用binary傳出去,不是用string
所以如果不是用HTML或是一些現成的tool去做的話,我們的程式裡面就要有可以去設定這些值的method來完成事情
最後,介紹一個很好用的tool給大家
Firebug : http://getfirebug.com/
對於firefox上的addons,用來觀察網頁元素和Request / Response之間傳遞了什麼訊息很方便的工具
不知道什麼時候會移到safari extension
雖然safari本來就有類似的
但還是firebug 好用

2010年9月24日 星期五

iPhone 與 RoR 對話

這篇主要是利用POST把iPhone上的東西傳給RoR Server
iPhone的寫法就是這樣
- (void)viewDidLoad {
    [super viewDidLoad];
    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/getTest"];
    NSString* content = @"first=michael&second=pan";
   
    NSMutableURLRequest* urlRequest = [[NSMutableURLRequest alloc]initWithURL:url];
    [urlRequest setHTTPMethod:@"POST"];
    [urlRequest setHTTPBody:[content dataUsingEncoding:NSASCIIStringEncoding]];
    NSURLConnection * myConnection = [[[NSURLConnection alloc] initWithRequest:urlRequest delegate:self] autorelease];
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
    NSLog(@"response %@", response.URL );
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSLog(@"received data %@", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease ]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
    NSLog(@"error occurs");
}

在RoR端
直接用
http://tinyurl.com/28b4w23 設定
在Controller的部分改成這樣
=====================
class EchoTestController < ApplicationController
    skip_before_filter :verify_authenticity_token
    def index
          render :text => 'Hello ' + params["first"]
    end
end
=====================
這樣就可以了
在iPhone端會收到

2010-09-24 16:07:05.519 AsiTest[15495:207] response http://127.0.0.1/getTest
2010-09-24 16:07:05.521 AsiTest[15495:207] received data Hello michael

2010年9月23日 星期四

Ruby on Rail - 拿到POST Method參數值

POST method最常用的就是Form了
延續上一篇的設定
我們要新增一個File 在 app/views/echo_test 叫 form.rhtml
為什麼用rhtml就是因為裡面會有RoR預設的tag
========================
<% form_tag( { :action => "postData", }, { :method => :post }) do %>

  First name : <%= text_field "person", "first", "size" => 20 %> <br>
  Second name : <%= text_field "person", "second", "size" => 20 %> <br>
  <%= submit_tag( "send" ) %>

<% end %>
========================
在<% %> 裡面的就是RoR用的,已經和HTML綁在一起了
我們從頭開始看一下
<% form_tag( { :action => "postData", }, { :method => :post }) do %>
 ...
<% end %>

就是相對於HTML的
<form action="postData" method="post">
</form>
接下來
<%= text_field "person", "first", "size" => 20 %>
<%= text_field "person", "second", "size" => 20 %>
的意思是
這個text field 被傳出去的時候會用
{"person"=>{"second"=>"Pan", "first"=>"Michael"}}

我們在rb的file裡要接參數值就要這樣寫
params[:person] 這樣是接到 {"second"=>"Pan", "first"=>"Michael"}
params[:person]["first"] 這樣是接到 Michael
所以我們修改echo_test_controller 成
============================
class EchoTestController < ApplicationController
    def index
       render :file => 'app/views/echo_test/form.rhtml'
    end
    def postData
       render :text => 'Hello ' + params[:person]["first"]
    end

end
===========================
一連進http:127.0.0.1/getTest 就會先去讀 index這個method
因為這行 render :file => 'app/views/echo_test/form.rhtml'
會開始讀取form.rhtml
然後按下send之後form.rhtml會因為action => 'postData'
再回到echo_test_controller的 postData這個method
然後網頁就render出
render :text => 'Hello ' + params[:person]["first"]

Hello Michael

最後要提醒大家,如果Form不是用RoR的tag產生的
比如說是用NSURLRequest或是其他程式連的
那麼就很有可能會產生以下的錯誤
ActionController::InvalidAuthenticityToken
此時要在Class一開始就貼上
skip_before_filter :verify_authenticity_token

============================
class EchoTestController < ApplicationController
   skip_before_filter :verify_authenticity_token
    def index
       render :file => 'app/views/echo_test/form.rhtml'
    end
    def postData
       render :text => 'Hello ' + params[:person]["first"]
    end

end
===========================
請參考:http://goo.gl/yrmS

Ruby on Rail - 拿到GET Method參數值

Ruby on Rail設計理念也是Model View Controller的基本
如果手邊的電腦是Leopard或是Snow Leopard就直接打開Terminal 打上
cd ~/Sites
rails getTest
之後會看到下方的message
      create 
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  config/initializers
      create  config/locales
      create  db
      create  doc
      create  lib
      create  lib/tasks
      create  log
      create  public/images
      create  public/javascripts
      create  public/stylesheets
   [以下省略]
ruby on rail就會新增這麼多的資料夾在getTest底下
重點當然就是controllers, models, views啦
這篇的入門很簡單,就是當使用者打上
http://127.0.0.1?name=michael
的時候ruby server會回傳michael這個值給瀏覽器
這個就是name=michael就是GET Method用的的參數(name)和參數值(michael)

這麼簡單的例子,我們只需要新增controller就可以了
在Terminal getTest/ 底下打上
script/generate controller EchoTest
會跑出底下的訊息
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/echo_test
      create  test/functional/
      create  test/unit/helpers/
      create  app/controllers/echo_test_controller.rb
      create  test/functional/echo_test_controller_test.rb
      create  app/helpers/echo_test_helper.rb
      create  test/unit/helpers/echo_test_helper_test.rb
就在app/controllers底下新增了echo_test_controller.rb
接下來就是編寫其內容
==================================

class EchoTestController < ApplicationController
  def index
     render :text => 'Hello ' + params[:name]
  end
end

================================== 
存檔之後
我們來要設定一下routes.rb
在config/routes.rb中最後面幾行
# Install the default routes as the lowest priority.
  # Note: These default routes make all actions in every controller accessible via GET requests. You should
  # consider removing or commenting them out if you're using named routes and resources.
  map.connect ':controller/:action/:id'
  map.connect ':controller/:action/:id.:format'
請把最後兩行前面加上#

#  map.connect ':controller/:action/:id'
#  map.connect ':controller/:action/:id.:format'


加上一行
map.connect '/getTest', :controller => 'echo_test'
這行是要告訴server 當使用者打上
http://127.0.0.1/getTest 要用 echo_test_controller.rb這個controller 也就是我們名命的EchoTestController來做事情
 
最後把server啟動
還有一點要注意就是只有administrator才可以用80這個port
所以我們要這樣打(一樣是在getTest/ 底下)
 sudo script/server -p 80
Password:
輸入密碼後會看到

=> Booting Mongrel
=> Rails 2.3.5 application starting on http://0.0.0.0:80
=> Call with -d to detach
=> Ctrl-C to shutdown server
表示server在run了哦

打開Browser,在網址的地方輸入
http://127.0.0.1/getTest?name=michael
就會看到
Hello michael
在網頁上了

2010年9月20日 星期一

iOS 4 Message UI Framework

4.0 新的Framework讓我們不用透過外部連結來引發系統的SMS和Mail App而是可以將MFMessageComposeViewController 和 MFMailComposeViewController 和自己的App整合在一起相當的方便。
我們來看一下SMS的例子
  if ([MFMessageComposeViewController canSendText]) {
        MFMessageComposeViewController * mySMSController = [[MFMessageComposeViewController alloc]init];
        mySMSController.body = inputText.text;
        mySMSController.messageComposeDelegate = self; // 當傳送message有發生什麼情況要採取的動作
        [self presentModalViewController:mySMSController animated:YES];
        [mySMSController release];
    }else{
        NSLog(@"sorry not support sms text");
    }

利用body這個property可以把預設的文字傳到SMS ViewController上,接受者是用一個NSArray傳給recipients這個property,NSArray裡面給NSString就可以了也就是電話號碼。
接下來再來看messageComposeDelegate 這個property,負責處理的任務就是當傳送有什麼情況的回報,有這幾個情況:
enum MessageComposeResult {
   MessageComposeResultCancelled,
   MessageComposeResultSent,
   MessageComposeResultFailed
};
typedef enum MessageComposeResult MessageComposeResult;

分別是,按下右上Cancel鈕,成功傳送,傳送失敗。
可以如此判斷
- (void)messageComposeViewController:(MFMessageComposeViewController *)controller didFinishWithResult:(MessageComposeResult)result{
    if (result == MessageComposeResultCancelled) {    // pressed the "Cancel" button
        [self dismissModalViewControllerAnimated:YES];
    }
   
}
因為MFMessageComposeViewController是要被加到modal view上的,所以當Cancel鈕被按下,就要dismiss其modal view