/* |
File: AppController.m |
Abstract: UIApplication's delegate class, the central controller of the application. |
Version: 2.1 |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple |
Inc. ("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or redistribution of |
this Apple software constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, install, modify or |
redistribute this Apple software. |
|
In consideration of your agreement to abide by the following terms, and |
subject to these terms, Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this original Apple software (the |
"Apple Software"), to use, reproduce, modify and redistribute the Apple |
Software, with or without modifications, in source and/or binary forms; |
provided that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the following |
text and disclaimers in all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or logos of Apple Inc. may |
be used to endorse or promote products derived from the Apple Software |
without specific prior written permission from Apple. Except as |
expressly stated in this notice, no other rights or licenses, express or |
implied, are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or by other |
works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. APPLE |
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION |
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS |
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND |
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL |
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, |
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED |
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), |
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE |
POSSIBILITY OF SUCH DAMAGE. |
|
Copyright (C) 2014 Apple Inc. All Rights Reserved. |
|
*/ |
|
#import "AppController.h" |
|
#import "TapViewController.h" |
#import "PickerViewController.h" |
|
// The Bonjour service type consists of an IANA service name (see RFC 6335) |
// prefixed by an underscore (as per RFC 2782). |
// |
// <http://www.ietf.org/rfc/rfc6335.txt> |
// |
// <http://www.ietf.org/rfc/rfc2782.txt> |
// |
// See Section 5.1 of RFC 6335 for the specifics requirements. |
// |
// To avoid conflicts, you must register your service type with IANA before |
// shipping. |
// |
// To help network administrators indentify your service, you should choose a |
// service name that's reasonably human readable. |
|
static NSString * kWiTapBonjourType = @"_witap2._tcp."; |
|
@interface AppController () < |
UIApplicationDelegate, |
TapViewControllerDelegate, |
PickerDelegate, |
NSNetServiceDelegate, |
NSStreamDelegate |
> |
|
@property (nonatomic, strong, readwrite) TapViewController * tapViewController; |
@property (nonatomic, strong, readwrite) NSNetService * server; |
@property (nonatomic, assign, readwrite) BOOL isServerStarted; |
@property (nonatomic, copy, readwrite) NSString * registeredName; |
@property (nonatomic, strong, readwrite) NSInputStream * inputStream; |
@property (nonatomic, strong, readwrite) NSOutputStream * outputStream; |
@property (nonatomic, assign, readwrite) NSUInteger streamOpenCount; |
@property (nonatomic, strong, readwrite) PickerViewController * picker; |
|
@end |
|
#pragma mark - |
@implementation AppController |
|
@synthesize window = _window; |
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions |
{ |
#pragma unused(application) |
#pragma unused(launchOptions) |
|
// Get the root view controller (set up by the storyboard) |
|
self.tapViewController = (TapViewController *) self.window.rootViewController; |
assert([self.tapViewController isKindOfClass:[TapViewController class]]); |
self.tapViewController.delegate = self; |
|
// Show our window |
|
self.window.rootViewController = self.tapViewController; |
[self.window makeKeyAndVisible]; |
|
// Create and advertise our server. We only want the service to be registered on |
// local networks so we pass in the "local." domain. |
|
self.server = [[NSNetService alloc] initWithDomain:@"local." type:kWiTapBonjourType name:[UIDevice currentDevice].name port:0]; |
self.server.includesPeerToPeer = YES; |
[self.server setDelegate:self]; |
[self.server publishWithOptions:NSNetServiceListenForConnections]; |
self.isServerStarted = YES; |
|
// Set up for a new game, which presents a Bonjour browser that displays other |
// available games. |
|
[self setupForNewGame]; |
|
return YES; |
} |
|
- (void)applicationDidEnterBackground:(UIApplication *)application |
{ |
#pragma unused(application) |
|
// If there's a game playing, shut it down. Whether this is the right thing to do |
// depends on your app. In some cases it might be more sensible to leave the connection |
// in place for a short while to see if the user comes back to the app. This issue is |
// discussed in more depth in Technote 2277 "Networking and Multitasking". |
// |
// </RU/iOS/#technotes/tn2277/_index.html> |
|
if (self.inputStream) { |
[self setupForNewGame]; |
} |
|
// Quiesce the server and service browser, if any. |
|
[self.server stop]; |
self.isServerStarted = NO; |
self.registeredName = nil; |
if (self.picker != nil) { |
[self.picker stop]; |
} |
} |
|
- (void)applicationWillEnterForeground:(UIApplication *)application |
{ |
#pragma unused(application) |
|
// Quicken the server. Once this is done it will quicken the picker, if there's one up. |
|
assert( ! self.isServerStarted ); |
[self.server publishWithOptions:NSNetServiceListenForConnections]; |
self.isServerStarted = YES; |
if (self.registeredName != nil) { |
[self startPicker]; |
} |
} |
|
- (void)setupForNewGame |
{ |
// Reset our tap view state to avoid old taps appearing in the new game. |
|
[self.tapViewController resetTouches]; |
|
// If there's a connection, shut it down. |
|
[self closeStreams]; |
|
// If our server is deregistered, reregister it. |
|
if ( ! self.isServerStarted ) { |
[self.server publishWithOptions:NSNetServiceListenForConnections]; |
self.isServerStarted = YES; |
} |
|
// And show the service picker. |
|
[self presentPicker]; |
} |
|
#pragma mark - Picker management |
|
- (void)startPicker |
{ |
assert(self.registeredName != nil); |
|
// Tell the picker about our registration. It uses this to a) filter out our game |
// from the results, and b) display our game name in its table view header. |
|
self.picker.localService = self.server; |
|
// Start it up. |
|
[self.picker start]; |
} |
|
- (void)presentPicker |
{ |
if (self.picker != nil) { |
// If the picker is already on screen then we're here because of a connection failure. |
// In that case we just cancel the picker's connection UI and the user can choose another |
// service. |
|
[self.picker cancelConnect]; |
} else { |
// Create the service picker and put it up on screen. We only start the picker |
// if our server has completed its registration (the picker needs to know our |
// service name so that it can exclude us from the list). If that's not the |
// case then the picker remains stopped until -serverDidStart: runs. |
|
self.picker = [self.tapViewController.storyboard instantiateViewControllerWithIdentifier:@"picker"]; |
assert([self.picker isKindOfClass:[PickerViewController class]]); |
self.picker.type = kWiTapBonjourType; |
self.picker.delegate = self; |
if (self.registeredName != nil) { |
[self startPicker]; |
} |
|
[self.tapViewController presentViewController:self.picker animated:NO completion:nil]; |
} |
} |
|
- (void)dismissPicker |
{ |
assert(self.picker != nil); |
|
[self.tapViewController dismissViewControllerAnimated:NO completion:nil]; |
[self.picker stop]; |
self.picker = nil; |
} |
|
- (void)pickerViewController:(PickerViewController *)controller connectToService:(NSNetService *)service |
// Called by the picker when the user has chosen a service for us to connect to. |
// The picker is already displaying its connection-in-progress UI. |
{ |
BOOL success; |
NSInputStream * inStream; |
NSOutputStream * outStream; |
|
assert(controller == self.picker); |
#pragma unused(controller) |
assert(service != nil); |
|
assert(self.inputStream == nil); |
assert(self.outputStream == nil); |
|
// Create and open streams for the service. |
// |
// -getInputStream:outputStream: just creates the streams, it doesn't hit the |
// network, and thus it shouldn't fail under normal circumstances (in fact, its |
// CFNetService equivalent, CFStreamCreatePairWithSocketToNetService, returns no status |
// at all). So, I didn't spend too much time worrying about the error case here. If |
// we do get an error, you end up staying in the picker. OTOH, actual connection errors |
// get handled via the NSStreamEventErrorOccurred event. |
|
success = [service getInputStream:&inStream outputStream:&outStream]; |
if ( ! success ) { |
[self setupForNewGame]; |
} else { |
self.inputStream = inStream; |
self.outputStream = outStream; |
|
[self openStreams]; |
} |
} |
|
- (void)pickerViewControllerDidCancelConnect:(PickerViewController *)controller |
// Called by the picker when the user taps the Cancel button in its |
// connection-in-progress UI. We respond by closing our in-progress connection. |
{ |
#pragma unused(controller) |
[self closeStreams]; |
} |
|
#pragma mark - Connection management |
|
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode |
{ |
#pragma unused(stream) |
|
switch(eventCode) { |
|
case NSStreamEventOpenCompleted: { |
self.streamOpenCount += 1; |
assert(self.streamOpenCount <= 2); |
|
// Once both streams are open we hide the picker and the game is on. |
|
if (self.streamOpenCount == 2) { |
[self dismissPicker]; |
|
[self.server stop]; |
self.isServerStarted = NO; |
self.registeredName = nil; |
} |
} break; |
|
case NSStreamEventHasSpaceAvailable: { |
assert(stream == self.outputStream); |
// do nothing |
} break; |
|
case NSStreamEventHasBytesAvailable: { |
uint8_t b; |
NSInteger bytesRead; |
|
assert(stream == self.inputStream); |
|
bytesRead = [self.inputStream read:&b maxLength:sizeof(uint8_t)]; |
if (bytesRead <= 0) { |
// Do nothing; we'll handle EOF and error in the |
// NSStreamEventEndEncountered and NSStreamEventErrorOccurred case, |
// respectively. |
} else { |
// We received a remote tap update, forward it to the appropriate view |
if ( (b >= 'A') && (b < ('A' + kTapViewControllerTapItemCount))) { |
[self.tapViewController remoteTouchDownOnItem:b - 'A']; |
} else if ( (b >= 'a') && (b < ('a' + kTapViewControllerTapItemCount))) { |
[self.tapViewController remoteTouchUpOnItem:b - 'a']; |
} else { |
// Ignore the bogus input. This is important because it allows us |
// to telnet in to the app in order to test its behaviour. telnet |
// sends all sorts of odd characters, so ignoring them is a good thing. |
} |
} |
} break; |
|
default: |
assert(NO); |
// fall through |
case NSStreamEventErrorOccurred: |
// fall through |
case NSStreamEventEndEncountered: { |
[self setupForNewGame]; |
} break; |
} |
} |
|
- (void)openStreams |
{ |
assert(self.inputStream != nil); // streams must exist but aren't open |
assert(self.outputStream != nil); |
assert(self.streamOpenCount == 0); |
|
[self.inputStream setDelegate:self]; |
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; |
[self.inputStream open]; |
|
[self.outputStream setDelegate:self]; |
[self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; |
[self.outputStream open]; |
} |
|
- (void)closeStreams |
{ |
assert( (self.inputStream != nil) == (self.outputStream != nil) ); // should either have both or neither |
if (self.inputStream != nil) { |
[self.inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; |
[self.inputStream close]; |
self.inputStream = nil; |
|
[self.outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; |
[self.outputStream close]; |
self.outputStream = nil; |
} |
self.streamOpenCount = 0; |
} |
|
- (void)send:(uint8_t)message |
{ |
assert(self.streamOpenCount == 2); |
|
// Only write to the stream if it has space available, otherwise we might block. |
// In a real app you have to handle this case properly but in this sample code it's |
// OK to ignore it; if the stream stops transferring data the user is going to have |
// to tap a lot before we fill up our stream buffer (-: |
|
if ( [self.outputStream hasSpaceAvailable] ) { |
NSInteger bytesWritten; |
|
bytesWritten = [self.outputStream write:&message maxLength:sizeof(message)]; |
if (bytesWritten != sizeof(message)) { |
[self setupForNewGame]; |
} |
} |
} |
|
- (void)tapViewController:(TapViewController *)controller localTouchDownOnItem:(NSUInteger)tapItemIndex |
{ |
assert(controller == self.tapViewController); |
#pragma unused(controller) |
[self send:(uint8_t) (tapItemIndex + 'A')]; |
} |
|
- (void)tapViewController:(TapViewController *)controller localTouchUpOnItem:(NSUInteger)tapItemIndex |
{ |
assert(controller == self.tapViewController); |
#pragma unused(controller) |
[self send:(uint8_t) (tapItemIndex + 'a')]; |
} |
|
- (void)tapViewControllerDidClose:(TapViewController *)controller |
{ |
assert(controller == self.tapViewController); |
#pragma unused(controller) |
[self setupForNewGame]; |
} |
|
#pragma mark - QServer delegate |
|
- (void)netServiceDidPublish:(NSNetService *)sender |
{ |
assert(sender == self.server); |
#pragma unused(sender) |
|
self.registeredName = self.server.name; |
if (self.picker != nil) { |
// If our server wasn't started when we brought up the picker, we |
// left the picker stopped (because without our service name it can't |
// filter us out of its list). In that case we have to start the picker |
// now. |
|
[self startPicker]; |
} |
} |
|
- (void)netService:(NSNetService *)sender didAcceptConnectionWithInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream |
{ |
// Due to a bug <rdar://problem/15626440>, this method is called on some unspecified |
// queue rather than the queue associated with the net service (which in this case |
// is the main queue). Work around this by bouncing to the main queue. |
[[NSOperationQueue mainQueue] addOperationWithBlock:^{ |
assert(sender == self.server); |
#pragma unused(sender) |
assert(inputStream != nil); |
assert(outputStream != nil); |
|
assert( (self.inputStream != nil) == (self.outputStream != nil) ); // should either have both or neither |
|
if (self.inputStream != nil) { |
// We already have a game in place; reject this new one. |
[inputStream open]; |
[inputStream close]; |
[outputStream open]; |
[outputStream close]; |
} else { |
// Start up the new game. Start by deregistering the server, to discourage |
// other folks from connecting to us (and being disappointed when we reject |
// the connection). |
|
[self.server stop]; |
self.isServerStarted = NO; |
self.registeredName = nil; |
|
// Latch the input and output sterams and kick off an open. |
|
self.inputStream = inputStream; |
self.outputStream = outputStream; |
|
[self openStreams]; |
} |
}]; |
} |
|
- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary *)errorDict |
// This is called when the server stops of its own accord. The only reason |
// that might happen is if the Bonjour registration fails when we reregister |
// the server, and that's hard to trigger because we use auto-rename. I've |
// left an assert here so that, if this does happen, we can figure out why it |
// happens and then decide how best to handle it. |
{ |
assert(sender == self.server); |
#pragma unused(sender) |
#pragma unused(errorDict) |
assert(NO); |
} |
|
@end |