-
Notifications
You must be signed in to change notification settings - Fork 1
/
AppController.m
485 lines (388 loc) · 17.4 KB
/
AppController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
/*
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".
//
// <https://developer.apple.com/library/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