http://www.appcoda.com/intro-ios-multipeer-connectivity-programming/
Editor’s note: Inpart 1 of the multipeer connectivity series, we gave an introduction of the Multipeer Connectivity Framework and built chat feature of the demo app. The Multipeer Connectivity Framework is one of the many new frameworks introduced in iOS 7. As you can see in part 1, the framework allows developers to easily establish communication between nearby devices and implement features for data exchanging. In part 2 of the series, let’s continue to explore theMultipeer Connectivity Frameworkand see how we can implement the file sharing feature.
Enter the multipeer connectivity tutorial.
We’ll continue to work on the demo app. If you haven’t read the first part of the tutorial series, go back andcheck it out.
Setup The File Sharing User Interface
Main.storyboard
As a first step, go to the Second View Controller scene and select and delete the default contents of it. Then, add the following controls from the Objects Library, setting the same time the properties as described next:
UILabel
Frame: X=20, Y=20, Width=280, Height=21
Text: My files:
UITableView
Frame: X=0, Y=49, Width=320, Height=519
UITableViewCell object, and set its Row Height
UILabel
Frame: X=20, Y=8, Width=280, Height=21
Tag: 100
UILabel
Frame: X=20, Y=37, Width=280, Height=21
Tag: 200
Color: Light Gray
UIProgressView
Frame: X=20, Y=66, Width=280, Height=2
Tag: 300
Attributes Inspectorof the Utilities Pane, set thenewFileCellIdentifiervalue to theIdentifierfield, under theTable View Cell
Here is how the scene should look like after all having added all these controls:
SecondViewController.h
@interface SecondViewController : UIViewController
@property ( weak , nonatomic ) IBOutlet UITableView *tblFiles ;
@end
Also, once being in this file, adopt some required protocols as we’ll need to implement some delegate methods, as shown below:
@interface SecondViewController : UIViewController
tblFiles
Sharing Files
SecondViewControllerclass just like we did in the chat feature, meaning that we’ll first declare and instantiate an application delegate object, so we are able to access themcManager
SecondViewController.mfile and import theAppDelegate.h
#import "AppDelegate.h"
Next, declare an object to the private section of the interface:
@interface SecondViewController ( )
@property ( nonatomic , strong ) AppDelegate *appDelegate ;
@end
viewDidLoad
- ( void ) viewDidLoad
{
[ super viewDidLoad ] ;
_appDelegate = ( AppDelegate * ) [ [ UIApplication sharedApplication ] delegate ] ;
}
For the sake of the sample application, two demo files are provided to be used for sharing from peer to peer, thesample_file1.txtand thesample_file2.txt. It’s a good time now to download them and add them to the project.
These two files, along with any files that will be transfered from other peers, should reside in theDocuments directory
So, let’s start working on all this, and our first task will be to take the sample files from the application bundle and copy them to the documents directory. We’ll do that by using a private method that we’ll create right next, but first, let’s declare it in the private section of the interface. Apart from the method declaration, we will also declare aNSString
@interface SecondViewController ( )
. . .
@property ( nonatomic , strong ) NSString *documentsDirectory ;
- ( void ) copySampleFilesToDocDirIfNeeded ;
@end
Let’s go to the implementation now:
- ( void ) copySampleFilesToDocDirIfNeeded {
NSArray *paths = NSSearchPathForDirectoriesInDomains ( NSDocumentDirectory , NSUserDomainMask , YES ) ;
_documentsDirectory = [ [ NSString alloc ] initWithString : [ paths objectAtIndex : 0 ] ] ;
NSString *file1Path = [ _documentsDirectory stringByAppendingPathComponent : @"sample_file1.txt" ] ;
NSString *file2Path = [ _documentsDirectory stringByAppendingPathComponent : @"sample_file2.txt" ] ;
NSFileManager *fileManager = [ NSFileManager defaultManager ] ;
NSError *error ;
if ( ! [ fileManager fileExistsAtPath :file1Path ] || ! [ fileManager fileExistsAtPath :file2Path ] ) {
[ fileManager copyItemAtPath : [ [ NSBundle mainBundle ] pathForResource : @"sample_file1" ofType : @"txt" ]
toPath :file1Path
error : & error ] ;
if ( error ) {
NSLog ( @"%@" , [ error localizedDescription ] ) ;
return ;
}
[ fileManager copyItemAtPath : [ [ NSBundle mainBundle ] pathForResource : @"sample_file2" ofType : @"txt" ]
toPath :file2Path
error : & error ] ;
if ( error ) {
NSLog ( @"%@" , [ error localizedDescription ] ) ;
return ;
}
}
}
First of all, we specify and keep the documents directory path to thedocumentsDirectory
Now we have to call it, and the suitable place to do so is in theviewDidLoadmethod.
- ( void ) viewDidLoad
{
. . .
[ self copySampleFilesToDocDirIfNeeded ] ;
}
From now on, every time that the view controller is loaded our application will search for those files in the documents directory and will copy them there if they don’t exist.
NSMutableArray
@interface SecondViewController ( )
. . .
@property ( nonatomic , strong ) NSMutableArray *arrFiles ;
@end
How should we add objects to thearrFiles
@interface SecondViewController ( )
. . .
- ( NSArray * ) getAllDocDirFiles ;
@end
NSFileManagerobject to get all contents of the Documents directory as aNSArray
- ( NSArray * ) getAllDocDirFiles {
NSFileManager *fileManager = [ NSFileManager defaultManager ] ;
NSError *error ;
NSArray *allFiles = [ fileManager contentsOfDirectoryAtPath : _documentsDirectory error : & error ] ;
if ( error ) {
NSLog ( @"%@" , [ error localizedDescription ] ) ;
return nil ;
}
return allFiles ;
}
There is nothing difficult to this method. If any error occurs, we just show a description to the log. Now, it’s time to add objects to thearrFilesarray for first time, and this will take place on theviewDidLoad
- ( void ) viewDidLoad
{
. . .
_arrFiles = [ [ NSMutableArray alloc ] initWithArray : [ self getAllDocDirFiles ] ] ;
}
selfthe delegate and datasource of it, while being in theviewDidLoad:
- ( void ) viewDidLoad
{
. . .
[ _tblFiles setDelegate :self ] ;
[ _tblFiles setDataSource :self ] ;
}
To let our sample files appear on the table view upon the view controller loading, we need to force it to do that after ourarrFilesarray have got its values. Therefore add just this line on theviewDidLoad
- ( void ) viewDidLoad
{
. . .
[ _tblFiles reloadData ] ;
}
Before you run and test what we’ve done so far in this view controller, you need to implement the table view delegate and datasource required methods. So, here they are:
- ( NSInteger ) numberOfSectionsInTableView : ( UITableView * ) tableView {
return 1 ;
}
- ( NSInteger ) tableView : ( UITableView * ) tableView numberOfRowsInSection : ( NSInteger ) section {
return [ _arrFiles count ] ;
}
- ( UITableViewCell * ) tableView : ( UITableView * ) tableView cellForRowAtIndexPath : ( NSIndexPath * ) indexPath {
UITableViewCell *cell ;
cell = [ tableView dequeueReusableCellWithIdentifier : @"CellIdentifier" ] ;
if ( cell == nil ) {
cell = [ [ UITableViewCell alloc ] initWithStyle :UITableViewCellStyleDefault reuseIdentifier : @"CellIdentifier" ] ;
[ cell setAccessoryType :UITableViewCellAccessoryDisclosureIndicator ] ;
}
cell . textLabel . text = [ _arrFiles objectAtIndex :indexPath . row ] ;
[ [ cell textLabel ] setFont : [ UIFont systemFontOfSize : 14.0 ] ] ;
return cell ;
}
- ( CGFloat ) tableView : ( UITableView * ) tableView heightForRowAtIndexPath : ( NSIndexPath * ) indexPath {
return 60.0 ;
}
If you want, run the application to test it, but the only thing you’ll see for now is just the listing of the sample files.
Let’s keep going to enable the application send a file once it gets selected. What we actually want to happen, is when tapping on a table view row, a list of all peers to appear so we choose the peer that the selected file should be sent to. To make things easy for us and for the purposes of this example, we will use aUIActionSheet
- ( void ) tableView : ( UITableView * ) tableView didSelectRowAtIndexPath : ( NSIndexPath * ) indexPath {
NSString *selectedFile = [ _arrFiles objectAtIndex :indexPath . row ] ;
UIActionSheet *confirmSending = [ [ UIActionSheet alloc ] initWithTitle :selectedFile
delegate :self
cancelButtonTitle :nil
destructiveButtonTitle :nil
otherButtonTitles :nil ] ;
for ( int i = 0 ; i
tableview:didSelectRowAtIndexPath:method by creating an action sheet object, providing the selected file name as its title. It might look weird that we set the nil value to all button titles, but this is done in purpose. It would be really handful to us if we could create a nil-terminated string that would contain all peer display names and set them as button titles, but unfortunately there is no way to do that. Therefore there is thefor
Using the last two lines we keep the selected file name and the selected row in two private members, as we will need to know these two values later. Xcode throws an error though, as we haven’t still declared them, so let’s do it now:
@interface SecondViewController ( )
. . .
@property ( nonatomic , strong ) NSString *selectedFile ;
@property ( nonatomic ) NSInteger selectedRow ;
@end
Now every time that a file is selected on the table view, an action sheet appears, containing every peer name as a button so we can choose the file’s recipient. But what is going to happen after we have a peer selected on the action sheet? Well, nothing, as we haven’t implemented any behaviour yet.
We need to implement theactionSheet:clickedButtonAtIndex:delegate method (that’s also why we adopted theUIActionSheetDelegateprotocol previously). In there, after having checked that a peer name has been tapped and not the cancel button, we will invoke another new to us method of the Multipeer Connectivity framework, namedsendResourceAtURL:withName:toPeer:withCompletionHandler:. Actually, this is a method of theMCSession
An important thing necessary to be denoted here is the fact that this method returns aNSProgressobject. NSProgress is a new class in iOS 7, so pay a visit to the Apple’s documentation if you’d like to know more about it. Anyway, we care about the results of the method, as this is our only way to keep track of the sending progress and update our UI by showing a percentage 服务器托管网value of the completion of the whole process. However, there is a big trap here and that is that our interface will freeze if we call this method on the main thread. Don’t worry though, as this is an easy obstacle to overcome. We will simply do our entire job inside adispatch_asyncblock, so our interface will remain responsive while sending files.
Just a last note before we see its implementation. As this is a demo application, we want to know what files come from other peers, so we make sure that our code really works. Therefore, as you’ll see in the implementation right next, we slightly modify the file name, by adding the peer’s display name.
The implementation:
- ( void ) actionSheet : ( UIActionSheet * ) actionSheet clickedButtonAtIndex : ( NSInteger ) buttonIndex {
if ( buttonIndex != [ [ _appDelegate . mcManager . session connectedPeers ] count ] ) {
NSString *filePath = [ _documentsDirectory stringByAppendingPathComponent : _selectedFile ] ;
NSString *modifiedName = [ NSString stringWithFormat : @"%@_%@" , _appDelegate . mcManager . peerID . displayName , _selectedFile ] ;
NSURL *resourceURL = [ NSURL fileURLWithPath :filePath ] ;
dispatch_async ( dispatch_get_main_queue ( ) , ^ {
NSProgress *progress = [ _appDelegate . mcManager . session sendResourceAtURL :resourceURL
withName :modifiedName
toPeer : [ [ _appDelegate . mcManager . session connectedPeers ] objectAtIndex :buttonIndex ]
withCompletionHandler : ^ ( NSError *error ) {
if ( error ) {
NSLog ( @"Error: %@" , [ error localizedDescription ] ) ;
}
else {
UIAlertView *alert = [ [ UIAlertView alloc ] initWithTitle : @"MCDemo"
message : @"File was succ服务器托管网essfully sent."
delegate :self
cancelButtonTitle :nil
otherButtonTitles : @"Great!" , nil ] ;
[ alert performSelectorOnMainThread : @selector ( show ) withObject :nil waitUntilDone :NO ] ;
[ _arrFiles replaceObjectAtIndex : _selectedRow withObject : _selectedFile ] ;
[ _tblFiles performSelectorOnMainThread : @selector ( reloadData )
withObject :nil
waitUntilDone :NO ] ;
}
} ] ;
} ) ;
}
}
Let me point a few things out regarding this code fragment:
- Any error description is just logged, but in the case of a successful sending we show an alert view to the user. Note that this code runs on a secondary queue, so we show the alert view on the main thread of the application.
- You’ll probably wonder what those two lines are, just right after the alert view:
[ _arrFiles replaceObjectAtIndex : _selectedRow withObject : _selectedFile ] ;
[ _tblFiles performSelectorOnMainThread : @selector ( reloadData ) withObject :nil waitUntilDone :NO ] ;
Well, as you will finally find out by yourself and as I have already stated, a percentage value is going to be displayed next to the selected file name during the sending process, indicating the whole progress. After a sending has been completed, we need to simply show the file name again, so that’s what we do here. We just replace the combined file name and progress value with the single file name, by updating the array and the table view subsequently. Further than that, here it’s clearly shown why we kept the selected row and file name on theselectedRowandselectedFile
- Where do we keep track of the progress? Nowhere yet, as it’s necessary to say that the NSProgress class contains a property namedfractionCompletedthat gives us the progress as adoublevalue. Moreover, all NSProgress class properties, including thefractionCompleted, are KVO (Key-Value Observing), so we must observe for any value changes of this property and update appropriately our UI.
So, having said all that, it’s clear that our next goal is to observe thefractionCompletedvalue of the progress. To do that, add the next code fragment right before thedispatch_async
- ( void ) actionSheet : ( UIActionSheet * ) actionSheet clickedButtonAtIndex : ( NSInteger ) buttonIndex {
if ( buttonIndex != [ [ _appDelegate . mcManager . session connectedPeers ] count ] ) {
. . .
dispatch_async ( dispatch_get_main_queue ( ) , ^ {
NSProgress *progress = . . .
[ progress addObserver :self
forKeyPath : @"fractionCompleted"
options :NSKeyValueObservingOptionNew
context :nil ] ;
} ) ;
}
}
progress
fractionCompleted
- ( void ) observeValueForKeyPath : ( NSString * ) keyPath ofObject : ( id ) object change : ( NSDictionary * ) change context : ( void * ) context {
NSString *sendingMessage = [ NSString stringWithFormat : @"%@ - Sending %.f%%" ,
_selectedFile ,
[ ( NSProgress * ) object fractionCompleted ] * 100
] ;
[ _arrFiles replaceObjectAtIndex : _selectedRow withObject :sendingMessage ] ;
[ _tblFiles performSelectorOnMainThread : @selector ( reloadData ) withObject :nil waitUntilDone :NO ] ;
}
As you see, we create a new string value that contains both the selected file name and the current progress expressed as a percentage value. This string replaces the existing object at the specific index in the array, and the table view is updated on the main thread, so it reflects the sending progress.
If you feel so, go and give it a try. Send files and watch it working. The recipient won’t receive anything yet, as for the time being we have implemented the sender’s side, but not the receiver’s.
Let’s focus now on the actions that should be taken when a file is received. In this case, we are going to use the table view cell prototype we previously added in the Interface Builder. While a file receiving is in progress, we will display in the last row of the table view such a cell, showing the file’s name, the sender and the progress using the progress view object. When a file has been successfully received, this cell will be replaced by a default, normal cell that will display just the file name, exactly as the sample files are shown.
Open theMCManager.mfile, and locate thesession:didStartReceivingResourceWithName:fromPeer:withProgress:delegate method. Its name clearly states its purpose, and we will use it to keep track of the progress while a new file is being received. In here we will act just like we did at the previous two session delegate methods, simply by adding the parameter values into aNSDictionary
There is something more we’ll do here though. As we want to watch the progress of the file being received and update our progress view accordingly, we will register the class with theNSProgressobject of the parameter, so we can observe any changes taking place on thefractionCompletedproperty. Actually, we are going to do the exact same thing we previously did on the file sending feature implementation, where we also added code for observing thefractionCompletedchanges. So, here it is:
- ( void ) session : ( MCSession * ) session didStartReceivingResourceWithName : ( NSString * ) resourceName fromPeer : ( MCPeerID * ) peerID withProgress : ( NSProgress * ) progress {
NSDictionary *dict = @ { @"resourceName" : resourceName ,
@"peerID" : peerID ,
@"progress" : progress
} ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : @"MCDidStartReceivingResourceNotification"
object :nil
userInfo :dict ] ;
dispatch_async ( dispatch_get_main_queue ( ) , ^ {
[ progress addObserver :self
forKeyPath : @"fractionCompleted"
options :NSKeyValueObservingOptionNew
context :nil ] ;
} ) ;
}
dispatch_async
Now, implement the next method so we are notified on any progress changing:
- ( void ) observeValueForKeyPath : ( NSString * ) keyPath ofObject : ( id ) object change : ( NSDictionary * ) change context : ( void * ) context {
[ [ NSNotificationCenter defaultCenter ] postNotificationName : @"MCReceivingProgressNotification"
object :nil
userInfo : @ { @"progress" : ( NSProgress * ) object } ] ;
}
Every time that the progress is changed, we will post a new notification.
Let’s head back to theSecondViewController.mfile now, and let’s deal with these two new notifications. In theviewDidLoad
- ( void ) viewDidLoad
{
. . .
[ [ NSNotificationCenter defaultCenter ] addObserver :self
selector : @selector ( didStartReceivingResourceWithNotification : )
name : @"MCDidStartReceivingResourceNotification"
object :nil ] ;
[ [ NSNotificationCenter defaultCenter ] addObserver :self
selector : @selector ( updateReceivingProgressWithNotification : )
name : @"MCReceivingProgressNotification"
object :nil ] ;
}
didStartReceivingResourceWithNotification:and theupdateReceivingProgressWithNotification:
@interface SecondViewController ( )
. . .
- ( void ) didStartReceivingResourceWithNotification : ( NSNotification * ) notification ;
- ( void ) updateReceivingProgressWithNotification : ( NSNotification * ) notification ;
@end
Let’s go with the first one:
- ( void ) didStartReceivingResourceWithNotification : ( NSNotification * ) notification {
[ _arrFiles addObject : [ notification userInfo ] ] ;
[ _tblFiles performSelectorOnMainThread : @selector ( reloadData ) withObject :nil waitUntilDone :NO ] ;
}
arrFilesarray, and we reload the table view data so the new file name, the sender and the initial value of the project to be displayed. But are they really going to be displayed? The answer is not until we update thetableView:cellForRowAtIndexPath:
- ( UITableViewCell * ) tableView : ( UITableView * ) tableView cellForRowAtIndexPath : ( NSIndexPath * ) indexPath {
UITableViewCell *cell ;
if ( [ [ _arrFiles objectAtIndex :indexPath . row ] isKindOfClass : [ NSString class ] ] ) {
cell = [ tableView dequeueReusableCellWithIdentifier : @"CellIdentifier" ] ;
if ( cell == nil ) {
cell = [ [ UITableViewCell alloc ] initWithStyle :UITableViewCellStyleDefault reuseIdentifier : @"CellIdentifier" ] ;
[ cell setAccessoryType :UITableViewCellAccessoryDisclosureIndicator ] ;
}
cell . textLabel . text = [ _arrFiles objectAtIndex :indexPath . row ] ;
[ [ cell textLabel ] setFont : [ UIFont systemFontOfSize : 14.0 ] ] ;
}
else {
cell = [ tableView dequeueReusableCellWithIdentifier : @"newFileCellIdentifier" ] ;
NSDictionary *dict = [ _arrFiles objectAtIndex :indexPath . row ] ;
NSString *receivedFilename = [ dict objectForKey : @"resourceName" ] ;
NSString *peerDisplayName = [ [ dict objectForKey : @"peerID" ] displayName ] ;
NSProgress *progress = [ dict objectForKey : @"progress" ] ;
[ ( UILabel * ) [ cell viewWithTag : 100 ] setText :receivedFilename ] ;
[ ( UILabel * ) [ cell viewWithTag : 200 ] setText : [ NSString stringWithFormat : @"from %@" , peerDisplayName ] ] ;
[ ( UIProgressView * ) [ cell viewWithTag : 300 ] setProgress :progress . fractionCompleted ] ;
}
return cell ;
}
Here is the deal: Our strategy is to check the kind of the class for each object existing in thearrFiles
Besides this method, alter the following as well:
- ( CGFloat ) tableView : ( UITableView * ) tableView heightForRowAtIndexPath : ( NSIndexPath * ) indexPath {
if ( [ [ _arrFiles objectAtIndex :indexPath . row ] isKindOfClass : [ NSString class ] ] ) {
return 60.0 ;
}
else {
return 80.0 ;
}
}
That’s necessary so the last row to have the appropriate height when a file is being received and everything to be properly displayed.
updateReceivingProgressWithNotification:
- ( void ) updateReceivingProgressWithNotification : ( NSNotification * ) notification {
NSProgress *progress = [ [ notification userInfo ] objectForKey : @"progress" ] ;
NSDictionary *dict = [ _arrFiles objectAtIndex : ( _arrFiles . count - 1 ) ] ;
NSDictionary *updatedDict = @ { @"resourceName" : [ dict objectForKey : @"resourceName" ] ,
@"peerID" : [ dict objectForKey : @"peerID" ] ,
@"progress" : progress
} ;
[ _arrFiles replaceObjectAtIndex : _arrFiles . count - 1
withObject :updatedDict ] ;
[ _tblFiles performSelectorOnMainThread : @selector ( reloadData ) withObject :nil waitUntilDone :NO ] ;
}
What’s actually taking place here is quite simple. From the current dictionary existing in thearrFiles
If you run it now, you’ll see that when a file is being received the progress view perfectly shows the progress.
One last thing remains to be done, and that is to save the file in the Documents directory once it’s received. For the last time in this tutorial, let’s open theMCManager.mfile and let’s go now to thesession:didFinishReceivingResourceWithName:fromPeer:atURL:withError:
- ( void ) session : ( MCSession * ) session didFinishReceivingResourceWithName : ( NSString * ) resourceName fromPeer : ( MCPeerID * ) peerID atURL : ( NSURL * ) localURL withError : ( NSError * ) error {
NSDictionary *dict = @ { @"resourceName" : resourceName ,
@"peerID" : peerID ,
@"localURL" : localURL
} ;
[ [ NSNotificationCenter defaultCenter ] postNotificationName : @"didFinishReceivingResourceNotification"
object :nil
userInfo :dict ] ;
}
SecondViewController.mfile, and let’s make our class able to observe for this notification too inside theviewDidLoad
{
. . .
[ [ NSNotificationCenter defaultCenter ] addObserver :self
selector : @selector ( didFinishReceivingResourceWithNotification : )
name : @"didFinishReceivingResourceNotification"
object :nil ] ;
}
Following the same steps that many times followed until now, let’s declare thedidFinishReceivingResourceWithNotification:
@interface SecondViewController ( )
. . .
- ( void ) didFinishReceivingResourceWithNotification : ( NSNotification * ) notification ;
@end
arrFiles
- ( void ) didFinishReceivingResourceWithNotification : ( NSNotification * ) notification {
NSDictionary *dict = [ notification userInfo ] ;
NSURL *localURL = [ dict objectForKey : @"localURL" ] ;
NSString *resourceName = [ dict objectForKey : @"resourceName" ] ;
NSString *destinationPath = [ _documentsDirectory stringByAppendingPathComponent :resourceName ] ;
NSURL *destinationURL = [ NSURL fileURLWithPath :destinationPath ] ;
NSFileManager *fileManager = [ NSFileManager defaultManager ] ;
NSError *error ;
[ fileManager copyItemAtURL :localURL toURL :destinationURL error : & error ] ;
if ( error ) {
NSLog ( @"%@" , [ error localizedDescription ] ) ;
}
[ _arrFiles removeAllObjects ] ;
_arrFiles = nil ;
_arrFiles = [ [ NSMutableArray alloc ] initWithArray : [ self getAllDocDirFiles ] ] ;
[ _tblFiles performSelectorOnMainThread : @selector ( reloadData ) withObject :nil waitUntilDone :NO ] ;
}
The file sharing feature has been now completed, and that also means that our demo application is totally ready!
Compile And Run The App
I’d be surprised if you haven’t run the application yet. However, if that’s your case, then now it’s the best and appropriate time to do it. Try it using a couple of devices, or a device and the iPhone Simulator. Make connections, send text messages and share files.e
Summary
The Multipeer Connectivity framework is a brand-new feature on iOS 7. In this tutorial series, we walked through only from a few of its potentialities, as they don’t stop here. There are a lot of things one could still explore, such as how to take security issues under account. Further than that, possibilities for new kind of applications are now available, with just one limitation, your imagination. This framework offers nice tools, and how they’ll be used it’s up to every developer’s desire. Through this tutorial I tried to offer an intro point to all those who would like to deal with Multipeer Connectivity, and I hope you find it really useful. Happy Multipeer Connections!
For your complete reference, you candownload the Xcode project of the demo app from here. Feel free to leave me feedback and comment below.
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net
相关推荐: Generative AI 新世界 | 大型语言模型(LLMs)概述
在上一篇《Generative AI 新世界:文本生成领域论文解读》中,我带领大家一起梳理了文本生成领域(Text Generation)的主要几篇论文:InstructGPT,RLHF,PPO,GPT-3,以及 GPT-4。本期文章我将帮助大家一起梳理另一个…