顯示具有 objective-C 標籤的文章。 顯示所有文章
顯示具有 objective-C 標籤的文章。 顯示所有文章

2015年11月12日 星期四

[iOS XMPP] 關於XMPP Framework的大小事 - xmpp stream設定

上一篇我們已經提到了XMPP Framework裡面主要分為兩個部分,xmpp core & xmpp extension。而xmpp core裡面最重要的就是xmpp stream,所以今天我們來看看如何在我們的app裡設定xmpp stream吧!
(在設定以下這些東西,通常都是在AppDelegate中進行實作)

1. 首先,設定連線

其實設定連線步驟很簡單,只要設定JID就可以了,範例如下:

xmppStream.myJID = [XMPPJID jidWithString:@"tungtungtsai@gmail.com"];
至於剩下的就交給xmpp stream完成吧!等等..我都還沒跟xmpp stream說我的server在哪啊?沒關係,在這裡,當你輸入了XMPPJID,xmpp stream就會去做SRV查找_xmpp-client._tcp.domain。例如,我在範例中打的domain是gmail.com,google server就很可能回傳像是"talk.google.com"的東西,然後xmpp stream就可以去和這個server建立連線。當然,如果說SRV查找失敗的話,那麼xmpp stream就直接去連JID的domain了。所以你要是已經知道你準備要連過去的xmpp server並沒有xmpp SRV records,你可以直接告訴xmpp stream你的host name,讓他不用花時間去查找了(反正也找不到...),範例如下:
xmppStream.myJID = [XMPPJID jidWithString:@"user@tungtungtsai.com"];
xmppStream.hostName = @"tungtungtsai.com";
你直接指派hostname有個最大好處就是他在development xmpp server也可以work。例如你的伺服器沒有DNS address只有實體IP,或是只能在local network run,那也沒關係,一樣能夠work。範例如下:

xmppStream.myJID = [XMPPJID jidWithString:@"user@dev1.myCompany.com"];
xmppStream.hostName = @"192.168.2.27";
設定了這些之後還有一件事,就是port。我們可以選擇性的看要不要給port,對每個xmpp spec來說,預設xmpp server都是開port 5222,當然如果你要開不同port就要特別設定一下hostPost了。


2. 添加代理(Delegates)

上一篇提到關於XMPPStream有些讓framework更具彈性、延展性、易開發的設計,而其一就是MulticastDelegate。由於xmpp framework需要支援很多的extensions,包含官方的和個人開發者所自己撰寫的plugin,所以傳統的代理模式(Delegate pattern)並不適合。因此,MulticastDelegate就可以讓你在你自己的extension中使用標準的delegate paradigm,並且允許多個classes去接收同一個delegate notification。這樣地好處是你不需要把你所有的xmpp handling code放進單一個class。

這裡簡單提一下:

在iOS開發中,主要有兩個callback systems,delegate和notification。Delegate很直覺也很簡單。使用者把自己設為delegate,然後就可以實作自己需要的delegate method。例如:

[worker setDelegate:self];

- (void)workerDidFinish:(Worker *)sender
{
}

- (void)worker:(Worker *)sender didFinishSubTask:(id)subtask inDuration:(NSTimeInterval)elapsed
{
}

- (BOOL)worker:(Worker *)sender shouldPerformSubTask:(id)subtask
{
}
Notifications也不難,使用者需要個別地註冊每個notification type (寫起來複雜一些),例如:

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(workerDidFinish:)
                                             name:WorkerDidFinishNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(workerDidFinishSubTask:)
                                             name:WorkerDidFinishSubTaskNotification
                                           object:nil];

- (void)workerDidFinish:(NSNotification *)notification
{
    Worker *sender = [notification object];
}

- (void)workerDidFinishSubTask:(NSNotification *)notification
{
    Worker *sender = [notification object];
    id subtask = [[notification userInfo] objectForKey:@"subtask"];
    NSTimeInterval elapsed = [[[notification userInfo] objectForKey"duration"] doubleValue];
}

有時候要從notification中的Dictionary取出參數的話,就要先知道他擁有哪些keys。另外,notification是不允許回傳變數的。因此簡單地對兩個callback機制的優缺點分析如下:

Delegate優點:

1. 易於註冊多個callback
2. 當有多個參數時較易於使用
3. 允許回傳變數

Delegate缺點:

1. 只能夠是單一delegate

Notification優點:

1. 可以多個物件註冊同一個notification

Notification缺點:

1. 註冊多個callback較複雜
2. 要從dictionary中拿到參數很麻煩(要知道正確的key)
3. 當需要回傳變數時就不能使用

所以,對於XMPP framework來說哪一個方式適用呢?對XMPP來說,xmpp framework需要能夠廣播給多個listeners。例如當你有個message進來,你需要同時廣播給聊天的視窗、歷史紀錄和推播系統等等。此外,xmpp framework需要易於延展。xmpp要能夠支持很多的XEP,所以要能在broadcast端與listener端易於使用。xmpp framework 需要選擇能支持回傳變數的方式,也需要能夠幫助維持 thread-safety,因為xmpp framework大量地使用平行化與多線程。

講完你就發現好像delegate和notification都不是最好的解決方法,因此xmpp就創了一個GCDMulticastDelegate class。你可以在任何時候把你自己新增/移除為XMPPStream的delegate。

[xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
...
[xmppStream removeDelegate:self];
簡單範例如下:
// Add myself as a delegate, and tell xmppStream to invoke my delegate methods on the main thread
[xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];

// Then just implement whatever delegate methods you need like normal
- (void)xmppStream:(XMPPStream *)sender didReceiveMessage:(XMPPMessage *)message
{
   ...
}
就和傳統的代理模式差不多,但是你可以具體地說明thread-specific資訊。而如果你想要讓某些process離開主線程到背景執行也很容易,在iPhone中這種操作方式可以讓你的app維持良好的perfomance,範例如下:
// Handle most stuff on the main thread
[xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];

// But do that one slow thing on a background queue so it doesn't slow down the UI anymore
[xmppStream addDelegate:bgProcessor delegateQueue:bgProcessorQueue];


3. 添加模組(Modules)

接下來就是要加入我們自己需要的extensions了,當然你也可以自己寫自己需要的extensions,這裡我們不會探討到底有哪些extensions,僅簡單列出一些,因為這裡最重要的是要學會加入模組。
XMPPReconnect - 當斷線時自動重新連線
XMPPRoster - 提供聯絡人功能
XMPPRoom - 提供聊天室,群聊功能
XMPPPubSub - 提供發布與訂閱功能
現在我們來試著加入XMPPReconnect module到我們的xmpp stream吧!

xmppReconnect = [ [XMPPReconnect alloc] init];

// Optional configuration of xmppReconnect could go here.
// The defaults are fine for our purposes.

[xmppReconnect activate:xmppStream];

// You can also optionally add delegates to the module.

[xmppReconnect addDelegate:self delegateQueue:dispatch_get_main_queue()];

// And that's all that is needed.
// The module will receive any delegate methods it needs automatically
// from the xmpp stream, and will continue to do its thing unless you deactivate it.

4. 建立連線

當我們都加好要用的modules,就可以開始建立連線了!

NSError *error = nil;
if (![xmppStream connect:&error])
{
    NSLog(@"Oops, I probably forgot something: %@", error);
}

若是我們在過程中忘記設定必須的屬性,例如JID,那連線就會回傳NO,並在error message中給予提示。

在連線過程中,client端和server端會建立xmpp handshake(交握),達成協定。如果你的伺服器需要建立安全連線,例如透過SSL/TLS方式,xmpp stream會自動建立安全連線。如果你的伺服器有不正確的X509 certificate,那就必須去實作xmppStream:willSecureWithSettings: 這個方法來改掉預設的安全設定。

5. 認證(Authenticating)

在所有連線handshaking都已經完成後,就會開始呼叫 xmppStreamDidConnect:方法,並開始進行認證程序。程式碼如下:

- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
    [xmppStream authenticateWithPassword:password error:NULL];
}


五個步驟進行完,在實際執行時就能與xmpp stream進行連線操作嘍!


參考連結:https://github.com/robbiehanson/XMPPFramework/wiki/IntroToFramework

2015年11月4日 星期三

[iOS] Action Extension 簡易教學

這幾天在研究iOS裡面的Extension。中文資源好像不太多,所以我把一些最近看到的心得陸續寫一下。

今天我要做的簡單教學,是讓APP可以打開Action Sheet,裡面會有一個Read按鈕,當使用者點了Read,APP就會幫你念出APP中的文字內容。




其實Extension最常被使用的地方是:雖然你沒有開啟APP,但你可以讓使用者可以使用某些APP功能或內容,所以通常使用Extension的時候都是因為有些特殊工作需要執行。Extension的種類很多,今天我要介紹的是Action Extension,這個Extension主要的目的是讓使用者查看或操作你APP裡的內容。在實作Extension特別需要注意的一個觀念是:Extension並不是一個APP唷!

好了,我們開始吧!


Step 1. 打開Xcode 新增一個Project,使用Single View Application並取一個自己喜歡的Project名字,我這裡取的是:“ReadExtPractice”





Step 2. 從Target的地方新增一個Target,並選擇Action Extension,一樣自己取一個你喜歡的名字吧!這裡我是叫做"Read"。然後在Activate處先選Cancel吧。






Step 3. 現在就開始來動工做一下簡單的UI吧!這裡我們以iPhone作為例子,就暫且不考慮那些iphone尺寸不同問題拉。先到ReadActionExt裡面Main.storyboard裡點選View Controller,並把 Simulated Metrics裡面的size設定成 iPhone 4.7-inch,就假設他是iPhone6吧。然後加入一個TextView與一個Button,並且把TextView的內文設定為自己想要的內容,如果不知道寫什麼,就跟我寫一樣吧!我的是“Hello, my name is Tung-tung Tsai. Welcome to Use Action Extension. Have a nice day.”並將按鈕的Title換成"Action"。如果不知道元件的size如何設定比較好,可以參考我這裡是設定textview{0, 28, 375, 200},button{112, 250, 150, 40}。






Step 4. 接下來開始寫點code拉。先到ViewController.h裡加入textView這個property並命名為"textView"並加入按鈕的Action命名為"ActionPressed",然後到ViewController.m裡面找到- (IBAction)ActionPressed:(id)sender 這個函數,並在裡面加上這段code,讓使用者點擊按鈕後出現一個Activity Controller。

- (IBAction)ActionPressed:(id)sender {
    UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:@[self.textView.text] applicationActivities:nil];
    [self presentViewController:activityVC animated:YES completion:nil];
}





Step 5.  都沒問題後就要開始來處理Action Extension的部分拉!一樣先處理UI,我們到Read裡面打開MainInterface.storyboard,這裡原本會有一個imageView不過我們不需要他,就直接將他Delete吧,然後加上一個TextView,並將Navigation Bar的標題改成"Read Ext"。

然後進入ActionViewController.h裡加入一個property命名為"TextView"連結到剛剛你加到storyboard裡的TextView吧!





Step 6.  打開ActionViewController.m,先在上面@import AVFoundation後,將- (void)viewDidLoad 的code先拿掉並重新寫上:

    NSExtensionItem *item = self.extensionContext.inputItems[0];
    NSItemProvider *itemProvider = item.attachments[0];
    
    if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypePlainText]) {

        [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypePlainText options:nil completionHandler:^(NSString* item, NSError * error) {
            if (item) {
                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    [self.TextView setText:item];
                    
                    //Set Synthesizer and utterence
                    AVSpeechSynthesizer *synthesizer = [[AVSpeechSynthesizer alloc]init];
                    AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:self.TextView.text];
                    [utterance setRate:0.5];
                    [synthesizer speakUtterance:utterance];
                }];
            }
        }];
        
    }



Step 7. 完成上述程式碼就代表他會在Extension被呼叫時讀取原本在TextView裡面的文字並且朗讀出來。但還沒結束,還需要在Read裡面的Info.plist設定一些參數才行。先進入Info.plist,然後打開NSExtension -> NSExtensionAttributes -> NSExtensionActivationRule,將NSExtensionActivationRule的type改為Dictionary,並加入一個新的項目NSExtensionActivationSupportsText,把這個type設為Boolean,並把Value設為YES。



Step 8. 終於完成了,最後到Asset.xcassets裡面把120x120的圖示放進去,並在App icon source裡面設定Icon來源為AppIcon就完成了。







Step 9. 實際執行看看吧!!應該會出現下面的樣子,如果我點選Action,再點選Read就會出現一個新頁面然後開始讀文字內容給你聽嘍!!!!











完成了! 希望這個教學可以幫助你更認識Action Extension嘍!如果有需要這份代碼,可以從Github上面查看。


[iOS] 透過UIWebview內嵌Html5 video影片到APP時常見問題

最近在著手研究關於iOS內嵌影片的問題,最簡單的方式是在設計APP時把影片包進APP裡,但是這樣的方式雖然簡單卻不見得符合需求,因為這樣操作等APP上架後就無法隨時更新,給使用者看新影片了!

因此我們接下來要討論的就是希望能把放在網路上的影片拿到手機上來播放。最基本的方式當然是先從網路上把整個影片下載下來,然後再拿播放器放映。不過這方法如果是一些應付大檔案就麻煩了,光是要等待影片下載好的時間可能就會讓使用者受不了。於是第二個好方法就是透過串流方式,不過要串流需要streaming server的配合,並不好說做就做。因此,最近我試著使用webview內嵌html5 video來進行播放。這樣的好處自然是使用者不用下載就可以觀看影片,同時,他不用straming server也有類似streaming的效果可以邊播邊載,他的載影片方式是一次一個request去load一部分的影片,不一個request就把所有的影片載完,同時他也可以做seek,就是讓使用者直接點擊影片時間條,點擊到哪裡就播哪裡。

關於html5 video內嵌,可以從Apple的文件上取得一些相關資訊,以下我就幾個我自己在過程中遇到且很常會有人想問的問題做簡單說明。

Q1. 在iPhone內嵌html5 video是否一定要全螢幕才可以播放?

Ans. 是的。官方文件說要播影片就是要全屏,我試了一陣子真的沒辦法,只要要播就會跳到全屏。但是iPad的話就不需要,可以不用全屏就能播影片。

Q2. 可以讓影片自動進行播放嗎?

Ans. 可以。在html端要設定control。

<video controls = "controls" autoplay ="autoplay">

在app端你的webview要設定以下屬性:

self.WebView.allowsInlineMediaPlayback = YES; // for autoplay
self.WebView.mediaPlaybackRequiresUserAction = NO; //for autoplay


Q3. 如何透過objective-C內嵌html5 video,代碼來一下?

self.WebView = [[UIWebView alloc] init];
 self.WebView.scalesPageToFit = YES;
 self.WebView.allowsInlineMediaPlayback = NO; // for autoplay
 self.WebView.mediaPlaybackRequiresUserAction = YES; //for autoplay
        
 NSString *serverUrl = [NSString stringWithFormat:@"http://%@:%@@domain.com/video.mp4", username,password];
 serverUrl = [serverUrl stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
        
 NSURL *url = [NSURL URLWithString:serverUrl];
 NSURLRequest *req = [NSURLRequest requestWithURL:url];
 [self.WebView loadRequest:req];
 [self.view addSubview: self.VideoWebView];

如果是iPad,可以設定video的frame來fit你的webview,因此你可以透過loadHTMLString來做。
 NSString *serverUrl = [NSString stringWithFormat:@"http://%@:%@@domain.com/video.mp4", username,password];
 serverUrl = [serverUrl stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *videoHTML= [NSString stringWithFormat: @"<style type='text/css'>body { margin:0;background-color:black}</style><video width='%f' height='%f' frameborder='0' controls autoplay><source src=\"%@\" type=\"video/mp4\"></video><meta name='viewport' content='width=device-width, initial-scale=1,user-scalable=no,minimum-scale=1.0, maximum-scale=1.0'>", self.VideoWebView.frame.size.width, self.VideoWebView.frame.size.height,serverUrl];
[self.VideoWebView loadHTMLString:videoHTML baseURL:nil];
 [self.view addSubview: self.VideoWebView];



Q4. 網址一旦使用https:// 影片就無法播了拉!!!怎麼這樣?

Ans. 因為你的server需要先裝憑證。

Q5. 我已經在app登入過拉,為什麼拿不到伺服器的影片,哭哭,怎麼辦?

Ans. 因為在web裡面要用basic authentication才行,他沒有幫你記住...所以你要把原本的連結由:"http://domain.com/video/sample.mp4"改為在前面加上帳號密碼,來通過認證,變這樣:
"http://username:password@domain.com/video/sample.mp4" 那你會問我這樣不就被人家發現帳號密碼...對啊,但這是資安問題了QQ

Q6. 現在我已經會透過webview來播影片了,但是我想要從特定時間點開始播可以嗎?例如直接從十秒開始播!

Ans. 可以的,請參考以下代碼。把<script>寫進去,然後currenttime設定一下,設定幾秒就會從幾秒開始播嘍!他的方式是從開始就從10秒處打request,所以前面1~9秒就不會loading了!

 NSString *videoHTML= [NSString stringWithFormat: @"<style type='text/css'>body { margin:0;background-color:black}</style><video id='video1' width='%f' height='%f' frameborder='0' controls autoplay><source src=\"%@\" type=\"video/mp4\"></video><script>document.getElementById('video1').addEventListener('loadedmetadata', function() {this.currentTime = 10;}, false);</script><meta name='viewport' content='width=device-width, initial-scale=1,user-scalable=no,minimum-scale=1.0, maximum-scale=1.0'>", self.WebView.frame.size.width, self.WebView.frame.size.height,serverUrl];


Q7.  我有辦法知道他正在看哪裡(第幾秒)嗎?

Ans. 應該是不行,因為我試了好一陣子,我也想知道答案,誰知道快教教我XDD