鍍金池/ 教程/ iOS/ 客戶端
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

客戶端

客戶端程序是這個項目的一個組成部分,這個項目能將目的地的坐標(biāo)發(fā)送給無人機上綁定的電話。 整個過程是個很簡單的任務(wù),而其中又不乏有趣的部分,例如使用新的( iOS7 上的)Multipeer Connectivity API 和 NSSecureCoding。

這個程序最后表現(xiàn)的界面很簡單,但卻不太漂亮:

http://wiki.jikexueyuan.com/project/objc/images/8-4.jpg" alt="" />

多點連接

為了創(chuàng)建客戶端和無人機上的導(dǎo)航程序之間的連接,我們打算使用新的 Multipeer Connectivity 的 API。 為了做到這一點,我們只需相互連接兩個設(shè)備, 所以 multipeer API 在這里并不能完全發(fā)揮它的潛力。但是如果更多的客戶端要加入的話,代碼實際上是一樣的。

發(fā)送

我們決定讓客戶端程序作為發(fā)送端,而無人機上的導(dǎo)航程序作為瀏覽接收端??蛻舳耸褂靡韵潞唵蔚恼Z句來開始發(fā)送:

NSString *displayName = [UIDevice currentDevice].name;
self.peer = [[MCPeerID alloc] initWithDisplayName:displayName];
self.advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peer     discoveryInfo:nil serviceType:ServiceTypeIdentifier];
self.advertiser.delegate = self;
[self.advertiser startAdvertisingPeer];

一旦另一臺有同樣服務(wù)類型的瀏覽客戶端的設(shè)備發(fā)現(xiàn)了發(fā)送端, 我們就會收到一個代理回調(diào)函數(shù)以便建立連接:

- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL accept, MCSession *session))invitationHandler
{
    self.session = [[MCSession alloc] initWithPeer:self.peer];
    self.session.delegate = self;
    invitationHandler(YES, self.session);
}   

一旦我們收到邀請,我們就建立一個新的會話對象,設(shè)置我們自己為會話的代理,并且通過調(diào)用 invitationHandler 并將 YES 和會話做為參數(shù)傳遞,來接受邀請。

為了能在屏幕上顯示連接狀態(tài),我們要實現(xiàn)另一個會話代理方法。因為我們只能連接到一個另外的設(shè)備,所以我們僅使用當(dāng)前已連接的節(jié)點數(shù)量作為標(biāo)示,標(biāo)示大于 0 代表已連接:

- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        NSString *notificationName = session.connectedPeers.count > 0 ? 
                            MultiPeerConnectionDidConnectNotification : 
                            MultiPeerConnectionDidDisconnectNotification;
        [[NSNotificationCenter defaultCenter] postNotificationName:notificationName object:self];
    }];
}

因為六分之五的 MCSessionDelegate 協(xié)議里的方法都是必須的,所以盡管我們沒有特別的目的要使用他們,我們也必須把那些協(xié)議都加上。

這時候,連接建立好以后,我們就能使用會話的 sendData:toPeers:withMode:error: 方法傳遞數(shù)據(jù)。我們會在后面部分更多的探討這個內(nèi)容。

瀏覽接收

運行在飛行器手機上的導(dǎo)航程序必須通過給客戶端發(fā)送邀請來初始化連接。做法同樣直白,第一步是啟動掃描節(jié)點。

MCPeerID* peerId = [[MCPeerID alloc] initWithDisplayName:@"Drone"];
self.browser = [[MCNearbyServiceBrowser alloc] initWithPeer:peerId  serviceType:ServiceTypeIdentifier];
self.browser.delegate = self;
[self.browser startBrowsingForPeers];

一個節(jié)點被發(fā)現(xiàn)以后, 我們就獲得一個代理回調(diào)函數(shù),而且能邀請該節(jié)點加入我們的會話:

- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
    self.session = [[MCSession alloc] initWithPeer:peerId];
    self.session.delegate = self;
    [browser invitePeer:peerID toSession:self.session withContext:nil timeout:0];
}

一旦客戶端發(fā)送數(shù)據(jù),我們就能從會話的代理方法 session:didReceiveData:fromPeer: 接收到這些數(shù)據(jù)。

傳輸數(shù)據(jù)

多點會話中的每個節(jié)點能很方便地使用 sendData:toPeers:withMode:error: 方法發(fā)送數(shù)據(jù)。 我們只需要考慮如何打包數(shù)據(jù)來傳輸。

一個通常的選擇是簡單的編碼為 JSON。 盡管這對于我們的目的簡單易行, 但是我們想要做一些更有趣的辦法,使用 NSSecureCoding. 這對于我們的例子來講實際上并沒有什么差別, 但是如果你想要傳輸更多的數(shù)據(jù),這將是比編解碼 JSON 更有效的方式。

首先,我們創(chuàng)建一個類,用來打包我們要發(fā)送的數(shù)據(jù):

@interface RemoteControlCommand : NSObject <NSSecureCoding>

+ (instancetype)commandFromNetworkData:(NSData *)data;
- (NSData *)encodeAsNetworkData;

@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic) BOOL stop;
@property (nonatomic) BOOL takeoff;
@property (nonatomic) BOOL reset;

@end

為了使 secure coding 有效(確保收到的數(shù)據(jù)使我們期望收到的類型),我們需要添加 supportsSecureCoding 類方法到我們的實現(xiàn)中:

+ (BOOL)supportsSecureCoding;
{
    return YES;
}

接下來,我們要添加方法來編碼一個對象的實例并把它打包成一個NSData對象使其能夠通過多點連接發(fā)送。

- (NSData *)encodeAsNetworkData;
{
    NSMutableData *data = [NSMutableData data];
    NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    archiver.requiresSecureCoding = YES;
    [archiver encodeObject:self forKey:@"command"];
    [archiver finishEncoding];
    return data;
}

- (void)encodeWithCoder:(NSCoder *)coder;
{
    [coder encodeDouble:self.coordinate.latitude forKey:@"coordinate.latitude"];
    [coder encodeDouble:self.coordinate.longitude forKey:@"coordinate.longitude"];
    [coder encodeBool:self.stop forKey:@"stop"];
    [coder encodeBool:self.stop forKey:@"takeoff"];
    [coder encodeBool:self.stop forKey:@"reset"];
}

現(xiàn)在我們能簡單地用幾行代碼發(fā)送控制指令:

RemoteControlCommand *command = [RemoteControlCommand alloc] init];
command.coordinate = self.location.coordinate;
NSData *data = [command encodeAsNetworkData];
NSError *error;
[self.session sendData:data toPeers:self.session.connectedPeers withMode:MCSessionSendDataReliable error:&error];

為了使接收端能夠解碼數(shù)據(jù),我們要添加另一個類方法到我們的 RemoteControlCommand 中:

+ (instancetype)commandFromNetworkData:(NSData *)data;
{
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    unarchiver.requiresSecureCoding = YES;
    RemoteControlCommand *result = [unarchiver decodeObjectOfClass:self forKey:@"command"];
    return result;
}

最后,我們需要實現(xiàn) initWithCoder: 來讓已被編碼的對象能從數(shù)據(jù)中解碼出來。

- (id)initWithCoder:(NSCoder *)coder;
{
    self = [super init];
    if (self != nil) {
        CLLocationCoordinate2D coordinate = {};
        coordinate.latitude = [coder decodeDoubleForKey:@"coordinate.latitude"];
        coordinate.longitude = [coder decodeDoubleForKey:@"coordinate.longitude"];
        self.coordinate = coordinate;
        self.stop = [coder decodeBoolForKey:@"stop"];
        self.takeoff = [coder decodeBoolForKey:@"takeoff"];
        self.reset = [coder decodeBoolForKey:@"reset"];
    }
    return self;
}

綜合試一試

現(xiàn)在,我們在這有了多點連接并且我們能編解碼遠(yuǎn)程控制指令,我們已經(jīng)為無線發(fā)送位置坐標(biāo)和控制指令做好了準(zhǔn)備。 為了解釋這個例子,因為其他指令也是完全一樣的,我們只是看一下坐標(biāo)的傳輸。

像在項目概述里討論過的那樣,為了使這個飛行器導(dǎo)航測試簡單一點,這個客戶端程序可以發(fā)送當(dāng)前的地理位置或者是地圖上的選點。 在第一種情況,我們僅需要實現(xiàn) CLLocationManager 的代理方法 locationManager:didUpdateLocations: 并在屬性里存儲當(dāng)前坐標(biāo):

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    self.location = locations.lastObject;
}

我們設(shè)置一個定時器來定期發(fā)送當(dāng)前位置:

- (void)startBroadcastingLocation
{
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(broadcastLocation) userInfo:nil repeats:YES];
}

最后,broadcastLocation 方法每一秒調(diào)用一次,會創(chuàng)建一個 RemoteControlCommand 對象而且把它發(fā)送到已連接的節(jié)點:

- (void)broadcastLocation
{
    RemoteControlCommand *command = [RemoteControlCommand alloc] init];
    command.coordinate = self.location.coordinate;
    NSData *data = [command encodeAsNetworkData];
    NSError *error;
    [self.session sendData:data toPeers:self.session.connectedPeers withMode:MCSessionSendDataReliable error:&error];
    if (error) {
        NSLog(@"Error transmitting location: %@", error);
    }
}

大概就是這樣。跟隨著閱讀關(guān)于飛行器上導(dǎo)航軟件和用于與飛行器通信的 Core Foundation 網(wǎng)路 API 的其他幾篇文章能了解這些與飛行器交互的指令接收端并且能真正讓它起飛!