iOS 蓝牙传输 -- MultipeerConnectivity

一、MultipeerConnectivity框架简述

参考博客:《http://blog.csdn.net/phunxm/article/details/43450167》
iOS蓝牙通讯的三种方式:

  • GameKit.framework:iOS7之前的蓝牙通讯框架,从iOS7开始过期,但是目前已经被淘汰。
  • MultipeerConnectivity.framework:iOS7开始引入的新的蓝牙通讯开发框架,用于取代GameKit。
  • CoreBluetooth.framework:功能强大的蓝牙开发框架,要求设备必须支持蓝牙4.0

本篇文章主要是讲MultipeerConnectivity.framework框架。MultipeerConnectivity.framework并不仅仅支持蓝牙连接,准确的说它是一种支持Wi-Fi网络、Wi-FiP2P以及蓝牙个人局域网的通信框架,它屏蔽了具体的连接技术,让开发人员有统一的接口编程方法。通过MultipeerConnectivity连接的节点之间可以安全的传递信息(message)、流(Stream)或者其他文件资源(Resource)而不必通过网络服务。此外使用MultipeerConnectivity进行近场通信也不再局限于同一个应用之间传输,而是可以在不同的应用之间进行数据传输(当然你也可以选择在一个应用程序之间传输)。
对于MultipeerConnectivity.framework的使用,需要知道两个重要的概念,即广播(Advertisting)和发现(Disconvering),两个设备要进行数据传输,那两者都得是开放状态。

广播
  • 广播,就是将自己置于暴露状态(即打开广播),让寻找者(发现)可以找到,用MCAdvertiserAssistant创建广播对象;
  • 由于广播可能有多个,所以每个广播者都有一个名字,用MCPeerID来设置名称或者说标识,便于发现者区分。
  • 广播与发现之间的数据传输(发送或者接收),是通过一个会话对象来进行的,即MCSession。
  • 有连接,就有连接的状态,这里是通过一个代理方法来跟踪会话的状态,即-(void)session:(MCSession
    )session peer:(MCPeerID )peerID didChangeState:(MCSessionState)state
  • 广播者可以接收数据,接收即被动,所以是在一个代理方法中进行,即-(void)session:(MCSession
    )session didReceiveData:(NSData )data fromPeer:(MCPeerID *)peerID
  • 广播者可以发送数据,发送即主动,所以是调用对象方法来实现,即-(void)sendData: toPeers:withMode: error:
发现
  • 发现,就是自己去寻找,根据标识或名称,去找到自己想要连接的设备,用MCBrowserViewController创建发现对象
  • 当发现广播后,要与广播进行连接,这个连接要成功,是需要广播者同意连接才行,那如果当要连接“广播”的“发现”有很多时,就得需要MCPeerID(即“发现”的标识)来区分,便于“广播连”接对应的“发现”。
  • 要与广播进行会话,发送或接受数据,需要有个会话对象,即MCSession
  • 同样会话的连接状态,是通过代理方法来跟踪的,即-(void)session:(MCSession
    )session peer:(MCPeerID )peerID didChangeState:(MCSessionState)state
  • 和广播一样,发现者可以发送数据,发送即主动,调用方法-(void)sendData: toPeers:withMode: error:来发送数据
  • 发现者可以接收数据,也是在代理方法中执行,即-(void)session:(MCSession
    )session didReceiveData:(NSData )data fromPeer:(MCPeerID *)peerID

综上,可以看出,广播和发现的创建使用不同的类,但他们遵循一个相同的协议MCSessionDelegate,标识也都是使用MCPeerID来标识
对于要传输的数据,分为三种

  • message---一般都是用这种
  • Stream---数据流传输可以用在音频播放传输
  • Resource ---在传输比较大型的文件时,我们通常使用数据源传输

介绍就到这里,下面直接上代码

二、运用

创建一个项目,然后分别给广播和发现创建一个控制器,我这里是MultipeerConnectivityVC和MultipeerConVC,在两个控制器中都引入头文件

#import <MultipeerConnectivity/MultipeerConnectivity.h>

界面的控件创建就不细说了,截个图

广播界面
发现界面
广播和发现建立连接

要建立连接,按上面说的,就得有广播者(MCAdvertiserAssistant)和发现者(MCBrowserViewController),同时都要有名称(MCPeerID),然后还要有进行会话的载体对象(MCSession)。
先在MultipeerConnectivityVC控制其中创建广播对象
在.m文件中添加一个广播属性和会话属性

@property(nonatomic,strong)MCAdvertiserAssistant * advertiserAssistant;//广播助手
@property(nonatomic,strong)MCSession * session;

在viewDidLoad中初始化广播

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建ID
    MCPeerID * peerID = [[MCPeerID alloc] initWithDisplayName:@"蓝牙设备1"];
    //根据ID创建会话对象
    self.session = [[MCSession alloc] initWithPeer:peerID];
    self.session.delegate =self;
    //创建广播
    //ServiceType的值可以自定义,但是一定要和发现的相同
    _advertiserAssistant = [[MCAdvertiserAssistant alloc] initWithServiceType:@"connect" discoveryInfo:nil session:self.session];
    _advertiserAssistant.delegate = self;
}

会话对象有代理,广播也有代理,所以要遵循协议

@interface MultipeerConnectivityVC ()<MCSessionDelegate,MCAdvertiserAssistantDelegate>

在“开始广播”按钮中发起广播

- (IBAction)beginBroadcast:(UIBarButtonItem *)sender {
    [self.advertiserAssistant start];
}

广播发出去了,该让“发现”去发现“广播”了,在MultipeerConVC控制器中创建“发现”.
添加发现属性和会话对象属性

@property(nonatomic,strong)MCBrowserViewController * browserController;
@property(nonatomic,strong)MCSession * session;

先初始化会话对象

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建标识
    MCPeerID * peerID = [[MCPeerID alloc] initWithDisplayName:@"蓝牙设备2"];
    //创建会话对象
    self.session = [[MCSession alloc] initWithPeer:peerID];
    self.session.delegate = self;
}

在“搜索广播”的点击事件里初始化发现

- (IBAction)searchBroadcast:(UIBarButtonItem *)sender {
    //ServiceType的值可以自定义,但是一定要和广播的相同
    _browserController = [[MCBrowserViewController alloc] initWithServiceType:@"connect" session:self.session];
    _browserController.delegate = self;
    [self presentViewController:_browserController animated:YES completion:nil];
}

同样遵循代理

@interface MultipeerConVC ()<MCSessionDelegate,MCBrowserViewControllerDelegate>

下面来讲一下两个协议,即MCAdvertiserAssistantDelegate ,MCBrowserViewControllerDelegate的代理方法。通过操作流程来讲
首先,点击广播页面的“开始广播”按钮,广播就发出去了。然后点击发现界面的“搜索广播”按钮,就会弹出搜索浏览界面也就是MCBrowserViewController控制器,这个时候,只要发现附近的广播,就会走MCBrowserViewControllerDelegate协议的代理方法

- (BOOL)browserViewController:(MCBrowserViewController *)browserViewController
      shouldPresentNearbyPeer:(MCPeerID *)peerID
            withDiscoveryInfo:(nullable NSDictionary<NSString *, NSString *> *)info{
    NSLog(@"发现附近的广播");
    return YES;
}

搜索出来后,会列出广播,如下图


搜索到的广播

如果你点击当前界面的左上角的Cancel按钮,就会走MCBrowserViewControllerDelegate协议的代理方法

-(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
    NSLog(@"取消浏览.");
    [self.browserController dismissViewControllerAnimated:YES completion:nil];
}

如果你点击了右上角的Done按钮就会走MCBrowserViewControllerDelegate协议的代理方法

-(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserViewController{
    NSLog(@"已选择");
    [self.browserController dismissViewControllerAnimated:YES completion:nil];
}

当然,一般初次进来的时候,Done按钮是灰色的,点不了,而连接成功后,列表界面会自动消失,感觉都用不着Done按钮,其实第二次进到这个界面的时候就可以用了。
好,到这里的话我既不点“Cancel”,也不点“Done”,而是选择广播设备“蓝牙设备1”,只要选择,那广播端就会收到信号,会弹出一个接受请求的选择弹框,如下图


选择弹框

可以看到发现的设备名称是“蓝牙设备2”。弹出这个弹框之前,会走MCAdvertiserAssistantDelegate协议的代理方法

- (void)advertiserAssistantWillPresentInvitation:(MCAdvertiserAssistant *)advertiserAssistant{
    NSLog(@"一个邀请求将出现");
}

当选择弹框的“Decline”或者“Accept”按钮,弹框就会消失,就会走代理方法

- (void)advertiserAssistantDidDismissInvitation:(MCAdvertiserAssistant *)advertiserAssistant{
    NSLog(@"一个邀请将从屏幕消失");
}

如果点击的是“ Accept”,那么连接成功,“发现”端的MCBrowserViewController控制器界面会自动消失。到这一步,发现和广播的连接已经完成。下面就是传送数据了

Message数据传输

我这里要实现的是,在广播端从相册选择图片,然后传送给发现端;或者在发现端从相册选择图片,然后传送给广播端。就这部分功能来说,两个广播和发现的功能是相同的,代码也相同。所以这里只写一遍.
首先,在控制器中添加UIImagePickerController属性

@property(nonatomic,strong)UIImagePickerController * imagePickerController;

在选择照片按钮中初始化UIImagePickerController对象

- (IBAction)selectedPhoto:(UIBarButtonItem *)sender {
    _imagePickerController = [[UIImagePickerController alloc] init];
    _imagePickerController.delegate = self;
    [self presentViewController:_imagePickerController animated:YES completion:nil];
}

遵循协议

UIImagePickerControllerDelegate,UINavigationControllerDelegate

实现两个代理方法

#pragma mark - UIImagePickerController代理方法
-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
    //获取照片并展示
    UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage];
    [self.imageView setImage:image];
    [self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}
-(void)imagePickerControllerDidCancel:(UIImagePickerController *)picker{
    [self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}

到这一步,我已经获取到我要传输的照片了,那就开始传输吧,在上面的代理方法中添加代码如下

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
    //获取照片并展示
    UIImage *image=[info objectForKey:UIImagePickerControllerOriginalImage];
    [self.imageView setImage:image];
    //发送数据给所有已连接设备
    NSError *error=nil;
    [self.session sendData:UIImagePNGRepresentation(image) toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
    NSLog(@"开始发送数据...");
    if (error) {
        NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription);
    }
    [self.imagePickerController dismissViewControllerAnimated:YES completion:nil];
}

发送方法sendData:toPeers:withMode:error在这里解释下

  • 第一个参数Data:就是要发送的数据UIImagePNGRepresentation(image)
  • 第二个参数Peers:指的是发送给谁,[self.session connectedPeers] 就是获取与这个session建立连接的所有设备
  • 第三个参数Mode:该参数是枚举类型,有两个
MCSessionSendDataReliable,      // 保证可靠和顺序发出
MCSessionSendDataUnreliable     //立即发出没有排队,不能保证交货。

发送成功的话,则接收方就会收到数据,可以从MCSessionDelegate协议的代理方法中获得数据,

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
    NSLog(@"开始接收数据...");
    if ([UIImage imageWithData:data]) {
        UIImage *image=[UIImage imageWithData:data];
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self.imageView setImage:image];
        });
        //保存到相册
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
    }
}

到此,广播和发现之间的相片数据传输的功能实现完成。

注意:从相册选择照片,获取照片,发送照片数据,接收照片数据的代码,在广播和发现端是相同。因为要实现相互可以发送
字符串数据传输

在广播和发现的界面上都添加一个文字传输按钮,在广播的界面按钮事件中实现代码如下

- (IBAction)textButtonAction:(UIButton *)sender {
    NSString * string = @"住进布达拉宫,你是雪域的王;走在拉萨街头,你是世上最美的情郎";
    NSData * data = [string dataUsingEncoding:(NSUTF8StringEncoding)];
    NSError * error = nil;
    [self.session sendData:data toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
    NSLog(@"开始发送文字数据...");
    if (error) {
        NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription);
    }
}

在发现的按钮事件中实现代码如下

- (IBAction)textButtonAction:(UIButton *)sender {
    NSString * string = @"白云山上白云边,善男信女饰能仁,而我独饰九龙泉";
    NSData * data = [string dataUsingEncoding:(NSUTF8StringEncoding)];
    NSError * error = nil;
    [self.session sendData:data toPeers:[self.session connectedPeers] withMode:MCSessionSendDataUnreliable error:&error];
    NSLog(@"开始发送文字数据...");
    if (error) {
        NSLog(@"发送数据过程中发生错误,错误信息:%@",error.localizedDescription);
    }
}

触发了按钮,就会发送数据,然后在接收方的代理方法-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID中(广播和发现都要添加)添加代码如下

-(void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID{
    NSLog(@"开始接收数据...");
    if ([UIImage imageWithData:data]) {
        UIImage *image=[UIImage imageWithData:data];
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self.imageView setImage:image];
        });
        //保存到相册
        UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);
    }
    if ([[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.textField.text = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
        });
    }
}

这样就可以实现广播和发现的文字数据传输,其实,实现原理是一样的,下面来说说不一样的,即Stream和Resource传输

Stream数据传输

关于NSStream的介绍可以参考文章:NSStream我只简单的说说我的理解,便于使用就可以。

  • NSStream有两个子类即NSInputStream(输入数据流通道)和NSOutputStream(输出数据流通道)。
  • NSStream有一个协议NSStreamDelegate,协议中只有一个代理方法,即
-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode

数据读写都是在这个代理方法中执行的。

通过代码来理清他们的操作流程。先在控制器中添加两个子类的属性,另外添加两个属性,下面会用的到(广播和发现都需要添加)

@property(nonatomic,strong)NSOutputStream * outputStream;//输出流
@property(nonatomic,strong)NSInputStream * inputStream;//输入流
@property(nonatomic,assign)NSInteger byteIndex;//字节下标,用于记录传输数据的字节数
@property(nonatomic,strong)NSMutableData * streamData;//用于存储二进制流数据

流数据要传输,我必须得有一个触发事件,那我在"Stream"按钮的点击事件中,添加如下代码

- (IBAction)streamButtonAction:(UIButton *)sender {
    NSError *error;
    //将输出流通道初始化,即与连接上会话的蓝牙设备进行关联----我是输出数据流通道,我要流给谁
    self.outputStream = [self.session startStreamWithName:@"superStream" toPeer:[self.session.connectedPeers firstObject] error:&error];
    self.outputStream.delegate = self;
    //将输出流通道放到runloop上,这一步可以理解为,输出流通道需要动力,才能运输,那刚好runloop可以提供动力,
    [self.outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    if(error || !self.outputStream) {
        NSLog(@"%@", error);
    }
    else{
        //打开输出流通道
        [self.outputStream open];
    }
}

代码注释比较详细,应该能看懂。然后往下,输出数据流有动力了,也有方向了,但是还需要一个缓冲容器,以便于控制,毕竟是隔空传输,这个地方就是内存空间,检测到有了内存空间,数据流就会往内存空间缓缓注入,完毕后就要关闭输出数据流,然后还要把动力系统关闭,当然这些操作都是在代理方法中执行的

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch (eventCode) {
        case NSStreamEventOpenCompleted:{//打开输出数据流通道会走到这一步
            NSLog(@"数据流开始");
            self.byteIndex = 0;
            self.streamData = [[NSMutableData alloc]init];
        }
            break;
        case NSStreamEventHasSpaceAvailable:{//监测到有内存空间可用,就把输出流通道中的流数据写入到内存空间
            NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:[self recordPath]]];
            NSOutputStream *output = (NSOutputStream *)aStream;
            NSUInteger len = ((data.length - self.byteIndex >= 1024) ? 1024 : (data.length-self.byteIndex));
            NSData *data1 = [data subdataWithRange:NSMakeRange(self.byteIndex, len)];
            [output write:data1.bytes maxLength:len];
            self.byteIndex += len;
        }
            break;
        case NSStreamEventEndEncountered:{//监测到输出流通道中的流数据写入内存空间完成
            [aStream close];//关闭输出流通道
            [aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];//将输出流从runloop中清除
            self.byteIndex = 0;
        }
            break;
        case NSStreamEventErrorOccurred:{
            //发生错误
            NSLog(@"error");
        }
            break;
        default:
            break;
    }
}

代理方法中eventCode是数据流事件,监测输出数据流的操作到哪一步了。到这一步,剩下的就交给会话了即MCSession,会话会将数据流信号连接到匹配的蓝牙设备,会触发接收方所遵循的MCSessionDelegate协议的代理方法

-(void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID

从这个代理方法中获取流数据信号,注意,这里的流数据信号,可以理解为NSInputStream的接口信号,告诉接收设备,流数据输出端通道已经准备好,当你(输入端)准备好了,并对接上,就可以进行流数据传输,那怎么准备的呢?怎么对接呢?那就要用到NSInputStream了。将控制器的NSInputStream的属性通过代理方法中的“stream”实例化,就对接上了。但是和输出流一样,需要动力,需要打开输出流。但是不需要空间了,因为这里是从输出流端的空间获取流,所以代理方法实现如下

- (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID{
    NSLog(@"获取流数据");
    //输入流实例化---既然有流通道伸过来了,那我也得有可以对接的通道
    self.inputStream = stream;
    self.inputStream.delegate = self;
    //有了载体,要将这个通道流通起来,就需要添加动力,即放到runloop上
    [self.inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [self.inputStream open];//打开输入流通道
}

打开输入流通道,就会调用数据流的代理方法

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch (eventCode) {
        case NSStreamEventOpenCompleted:{//打开输出数据流通道或者打开输入数据流通道就会走到这一步
            NSLog(@"数据流开始");
            self.byteIndex = 0;
            self.streamData = [[NSMutableData alloc]init];
        }
            break;
        case NSStreamEventHasBytesAvailable:{//监测到输入流通道中有数据流,就把数据一点一点的拼接起来
            NSInputStream *input = (NSInputStream *)aStream;
            uint8_t buffer[1024];
            NSInteger length = [input read:buffer maxLength:1024];
            NSLog(@"%ld", length);
            [self.streamData appendBytes:(const void *)buffer length:(NSUInteger)length];
        }
            break;
        case NSStreamEventEndEncountered:{//监测到输入流通道中的流数据获取完成
            [aStream close];//关闭输入流通道
            [aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];//将输入流通道从runloop中清除
            //输入流数据拼接完成,可以直接获取数据
            if([aStream isKindOfClass:[NSInputStream class]]){
                self.imageView.image = [[UIImage alloc] initWithData:self.streamData];
            }
        }
            break;
        case NSStreamEventErrorOccurred:{
            //发生错误
            NSLog(@"error");
        }
            break;
        default:
            break;
    }
}

由于广播和发现是可以互传数据的,所以我就将代理方法

-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode

的中输入流和输出流的数据处理都写上,整理如下

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode{
    switch (eventCode) {
        case NSStreamEventOpenCompleted:{//打开输出数据流通道或者打开输入数据流通道就会走到这一步
            NSLog(@"数据流开始");
            self.byteIndex = 0;
            self.streamData = [[NSMutableData alloc]init];
        }
            break;
        case NSStreamEventHasBytesAvailable:{//监测到输入流通道中有数据流,就把数据一点一点的拼接起来
            NSInputStream *input = (NSInputStream *)aStream;
            uint8_t buffer[1024];
            NSInteger length = [input read:buffer maxLength:1024];
            NSLog(@"%ld", length);
            [self.streamData appendBytes:(const void *)buffer length:(NSUInteger)length];
            // 记住这边的数据陆陆续续的
        }
            break;
        case NSStreamEventHasSpaceAvailable:{//监测到有内存空间可用,就把输出流通道中的流写入到内存空间
            NSData *data = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:[self recordPath]]];
            NSOutputStream *output = (NSOutputStream *)aStream;
            NSUInteger len = ((data.length - self.byteIndex >= 1024) ? 1024 : (data.length-self.byteIndex));
            NSData *data1 = [data subdataWithRange:NSMakeRange(self.byteIndex, len)];
            [output write:data1.bytes maxLength:len];
            self.byteIndex += len;
        }
            break;
        case NSStreamEventEndEncountered:{//监测到输出流通道中的流数据写入内存空间完成或者输入流通道中的流数据获取完成
            [aStream close];//关闭输出流
            [aStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];//将输出流从runloop中清除
            //输入流数据拼接完成,可以直接获取数据
            if([aStream isKindOfClass:[NSInputStream class]]){
                self.imageView.image = [[UIImage alloc] initWithData:self.streamData];
            }
            self.byteIndex = 0;
        }
            break;
        case NSStreamEventErrorOccurred:{
            //发生错误
            NSLog(@"error");
        }
            break;
        default:
            break;
    }
}

补充一个落下方法 recordPath,也就是获取本地文件的路径

-(NSString*)recordPath{
    NSString * path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString * newPath = [path stringByAppendingPathComponent:@"nothing.jpeg"];
    UIImage * image = [UIImage imageNamed:@"WechatIMG2"];
    NSData * data = UIImageJPEGRepresentation(image, 0.1);
    [data writeToFile:newPath atomically:YES];
    return newPath;
}

这样的话,不管谁作为输出方,谁做输入方,都是可以实现数据流传输的。到这一步,Stream的数据传输讲解完毕,继续看下一个。

Resource数据源传输

在传输比较大型的文件时,我们通常使用数据源传输
数据传输都是在session的代理方法中进行的。数据源传送方式有两个代理方法,分别为数据源传输刚刚开始调用

- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress

和数据源传输结束时调用

- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(nullable NSError *)error 

首先添加一个“Resource”按钮,在它的点击事件中调用MCSession的发送数据源的方法发送数据

- (IBAction)resourceButtonAction:(UIButton *)sender {
    //获取导数据的路径
    NSURL *fileURL = [NSURL fileURLWithPath:[self imagePath]];
    //发送数据给匹配的蓝牙设备,Name随意取
    [self.session sendResourceAtURL:fileURL withName:@"image_" toPeer:[self.session.connectedPeers firstObject] withCompletionHandler:^(NSError *error) {\
        if (error) {
            NSLog(@"发送源数据发生错误:%@", error);
        }
    }];
}

获取数据路径的方法,如下

-(NSString *)imagePath{
    NSString * path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
    NSString * newPath = [path stringByAppendingPathComponent:@"nothing.jpeg"];
    UIImage * image = [UIImage imageNamed:@"WechatIMG2"];
    NSData * data = UIImageJPEGRepresentation(image, 0.1);
    [data writeToFile:newPath atomically:YES];
    return newPath;
}

数据发送了,在指定的蓝牙设备上会收到数据,会依次走MCSession的两个代理方法,即上面说的刚开始接收到和接受完成的代理方法,可以在相应的时机,进行相关操作

  • 数据源传输刚刚开始调用:一般用于设置一些初始值,比如文件接受者的进度Progress进度KVO观察。
  • 数据源传输结束时调用:主要用于将传输的文件从暂时存放的位置放到真正需要存放的位置。
//开始接收Resource数据
- (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress{
    NSLog(@"开始获取文件数据");
}
//完成Resource数据的接收
- (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(nullable NSError *)error{
    NSLog(@"获取文件数据结束");
    NSURL *destinationURL = [NSURL fileURLWithPath:[self imagePath]];
    //判断文件是否存在,存在则删除
    if ([[NSFileManager defaultManager] isDeletableFileAtPath:[self imagePath]]) {
        [[NSFileManager defaultManager] removeItemAtPath:[self imagePath] error:nil];
    }
    //转移文件
    NSError *error1 = nil;
    if (![[NSFileManager defaultManager] moveItemAtURL:localURL toURL:destinationURL error:&error1]) {
        NSLog(@"[Error] %@", error1);
    }
    //转移成功展示数据
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSData * data = [NSData dataWithContentsOfURL:destinationURL];
        UIImage * image = [[UIImage alloc] initWithData:data];
        self.imageView.image = image;
    });
}

和上面一样,在广播和发现的控制器中都实现上面的代码,就可以实现互传。
到这一步,MultipeerConnectivity框架讲解完毕。

三、代码下载

MultipeerConnectivity

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容