Although GameKit is built on
Bonjour, it isn’t meant to provide the same kind of general use data
transfer capabilities displayed in the previous two Bonjour-only
recipes. GameKit Bluetooth prefers small data packets, preferably under
1,000 bytes each. GKSession objects cannot send data over 95 kilobytes. When you try, the sendDataToAllPeers:error: method fails, returning a Boolean value of NO.
Recipe 12-7
addresses this problem by checking for data length before queuing any
send requests. Short data can be shared; long data is denied. To provide
a test bed, this recipe works with the iPhone’s built-in pasteboard.
In the real world, you’d likely split up long data
items into short bursts and send them using reliable transfer. Reliable
transmission ensures that data arrives and does so in the same order
that it was sent. You can implement checksumming and other standard
network approaches to ensure your data arrives properly. (You might
alternatively consider programming a custom Bonjour WiFi service or
using Internet server connections for more intense data transfer needs.)
This recipe provides a
jumping off point for testing file size elements in the GameKit world.
You are welcome to expand this code to explore file decomposition and
reconstruction on your own.
Using the iPhone Pasteboard
Pasteboards, also known as clipboards,
provide a central OS feature for sharing data across applications.
Users can copy data to the pasteboard in one application, switch tasks,
and then paste that data into another application. Cut/copy/paste
features appear in most operating systems and are new to the iPhone,
having debuted in the 3.0 firmware.
The UIPasteboard class offers access to a
shared iPhone pasteboard and its contents. As with Macs and
Windows-based computers, you can use the pasteboard to share data within
an application or between applications. In addition to the general
shared system pasteboard, the iPhone offers both a name finding
pasteboard and application-specific pasteboards to better ensure data
privacy. This snippet returns the general system pasteboard, which is
appropriate for most general copy/paste use.
UIPasteboard *pb = [UIPasteboard generalPasteboard];
Storing Data
Pasteboards can store one or more entries at a
time. Each has an associated type, using the Uniform Type Identifier
(UTI) to specify what kind of data is stored. For example, you might
find public.text (and more specifically public.utf8-plain-text), public.url, and public.jpeg among common data types used on the iPhone. The dictionary that stores the type and the data is called an item, and you can retrieve an array of all available items via the pasteboard’s items property.
Query a pasteboard for its available types by sending it the pasteboardTypes message. This returns an array of types currently stored on the pasteboard.
NSArray *types = [pb pasteboardTypes];
Pasteboards are specialized for several data types. These are colors, images, strings, and URLs. The UIPasteboard class provides specialized getters and setters to simplify handling these items. Because Recipe 12-7 provides a general pasting tool, only strings are demonstrated with a specialized call, that is, setString.
Retrieving Data
Retrieve data using dataForPasteboardType:.
This returns the data from the first item whose type matches the one
sent as the parameter. Any other matching items in the pasteboard are
ignored. Should you need to retrieve all matching data, recover an itemSetWithPasteboardTypes:
and then iterate through the set to retrieve each dictionary. Recover
the data type for each item from the single dictionary key and the data
from its value.
UIPasteboard offers two approaches for pasting to the pasteboard. Use setValueForPasteboardType: for Property List objects. For general data, use setData:forPasteboardType: as is used in this recipe. When pasteboards are changed, they issue a UIPasteboardChangedNotification, which you can listen into via a default NSNotificationCenter observer.
Responsible Pasteboarding
Recipe 1
provides several checks before sending, retrieving, and copying
pasteboard data. Users must confirm that they intend to share data of a
given type. When receiving data, they must authorize the application to
copy the data to the general system pasteboard. This approach ensures
that proactive user effort must take place before performing these
actions.
Recipe 1. Sharing the iPhone Pasteboard over GameKit
@implementation TestBedViewController
- (void) sharePasteboard
{
// Construct a dictionary of the pasteboard type and data
NSMutableDictionary *md = [NSMutableDictionary dictionary];
UIPasteboard *pb = [UIPasteboard generalPasteboard];
NSString *type = [[pb pasteboardTypes] lastObject];
NSData *data = [pb dataForPasteboardType:type];
[md setObject:type forKey:@"type"];
[md setObject:data forKey:@"data"];
// Deny any requests that are too big
if (data.length > (95000))
{
[ModalAlert say:@"Too much data in pasteboard (%0.2f \
Kilobytes). GameKit can only send up to approx 90 \
Kilobytes at a time.", ((float) data.length) / 1000.0f];
return;
}
// User must confirm share
NSString *confirmString = [NSString stringWithFormat:
@"Share %d bytes of type %@?", data.length, type];
if (![ModalAlert ask:confirmString]) return;
// Serialize and send the data
NSString *errorString;
NSData *plistdata = [NSPropertyListSerialization
dataFromPropertyList:md format:NSPropertyListXMLFormat_v1_0
errorDescription:&errorString];
if (plistdata)
[GameKitHelper sendData:plistdata];
else
CFShow(errorString);
}
- (void) sentData:(NSString *) errorString
{
// Check to see if there was a problem sending data
if (errorString)
{
[ModalAlert say:@"Error sending data: %@", errorString];
return;
}
[ModalAlert say:@"Pasteboard data successfully queued for\
transmission."];
}
// On establishing the connection, allow the user to share the pasteboard
- (void) connectionEstablished
{
UIPasteboard *pb = [UIPasteboard generalPasteboard];
NSArray *types = [pb pasteboardTypes];
if (types.count == 0) return;
self.navigationItem.leftBarButtonItem = BARBUTTON(
@"Share Pasteboard", @selector(sharePasteboard));
}
// Hide the share option when the connection is lost
- (void) connectionLost
{
self.navigationItem.leftBarButtonItem = nil;
}
-(void) receivedData: (NSData *) data
{
// Deserialize the transmission
CFStringRef errorString;
NSDictionary *dict =
(NSDictionary *) CFPropertyListCreateFromXMLData(
kCFAllocatorDefault, (CFDataRef)data,
kCFPropertyListMutableContainers, &errorString);
if (!dict)
{
CFShow(errorString);
return;
}
// Retrieve the type and data
NSString *type = [dict objectForKey:@"type"];
NSData *sentdata = [dict objectForKey:@"data"];
if (!type || !sentdata) return;
// Do not copy to pasteboard unless the user permits
NSString *message = [NSString stringWithFormat:
@"Received %d bytes of type %@. Copy to pasteboard?",
sentdata.length, type];
if (![ModalAlert ask:message]) return;
// Perform the pasteboard copy
UIPasteboard *pb = [UIPasteboard generalPasteboard];
if ([type isEqualToString:@"public.text"])
{
NSString *string = [[[NSString alloc] initWithData:sentdata
encoding:NSUTF8StringEncoding] autorelease];
[pb setString:string];
}
else [pb setData:sentdata forPasteboardType:type];
}
- (void) viewDidLoad
{
// Set up the helper
[GameKitHelper sharedInstance].sessionID = @"Pasteboard Share";
[GameKitHelper sharedInstance].dataDelegate = self;
[GameKitHelper assignViewController:self];
}
@end
|