RSS

(root)/iphone/tappity : /tappity/source/BonjourSupport/BrowserViewController.m (revision 46)

To get this branch, use:
bzr branch /browse/iphone/tappity
Line Revision Contents
1 46
/*
2
     File: BrowserViewController.m
3
 Abstract:  View controller for the service instance list.
4
 This object manages a NSNetServiceBrowser configured to look for Bonjour
5
services.
6
 It has an array of NSNetService objects that are displayed in a table view.
7
 When the service browser reports that it has discovered a service, the
8
corresponding NSNetService is added to the array.
9
 When a service goes away, the corresponding NSNetService is removed from the
10
array.
11
 Selecting an item in the table view asynchronously resolves the corresponding
12
net service.
13
 When that resolution completes, the delegate is called with the corresponding
14
NSNetService.
15
16
  Version: 2.8
17
 
18
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
19
 Inc. ("Apple") in consideration of your agreement to the following
20
 terms, and your use, installation, modification or redistribution of
21
 this Apple software constitutes acceptance of these terms.  If you do
22
 not agree with these terms, please do not use, install, modify or
23
 redistribute this Apple software.
24
 
25
 In consideration of your agreement to abide by the following terms, and
26
 subject to these terms, Apple grants you a personal, non-exclusive
27
 license, under Apple's copyrights in this original Apple software (the
28
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
29
 Software, with or without modifications, in source and/or binary forms;
30
 provided that if you redistribute the Apple Software in its entirety and
31
 without modifications, you must retain this notice and the following
32
 text and disclaimers in all such redistributions of the Apple Software.
33
 Neither the name, trademarks, service marks or logos of Apple Inc. may
34
 be used to endorse or promote products derived from the Apple Software
35
 without specific prior written permission from Apple.  Except as
36
 expressly stated in this notice, no other rights or licenses, express or
37
 implied, are granted by Apple herein, including but not limited to any
38
 patent rights that may be infringed by your derivative works or by other
39
 works in which the Apple Software may be incorporated.
40
 
41
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
42
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
43
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
44
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
45
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
46
 
47
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
48
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
49
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
50
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
51
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
52
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
53
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
54
 POSSIBILITY OF SUCH DAMAGE.
55
 
56
 Copyright (C) 2009 Apple Inc. All Rights Reserved.
57
 
58
 */
59
60
#import "BrowserViewController.h"
61
62
#define kProgressIndicatorSize 20.0
63
64
// A category on NSNetService that's used to sort NSNetService objects by their name.
65
@interface NSNetService (BrowserViewControllerAdditions)
66
- (NSComparisonResult) localizedCaseInsensitiveCompareByName:(NSNetService*)aService;
67
@end
68
69
@implementation NSNetService (BrowserViewControllerAdditions)
70
- (NSComparisonResult) localizedCaseInsensitiveCompareByName:(NSNetService*)aService {
71
	return [[self name] localizedCaseInsensitiveCompare:[aService name]];
72
}
73
@end
74
75
76
@interface BrowserViewController()
77
@property (nonatomic, assign, readwrite) BOOL showDisclosureIndicators;
78
@property (nonatomic, retain, readwrite) NSMutableArray* services;
79
@property (nonatomic, retain, readwrite) NSNetServiceBrowser* netServiceBrowser;
80
@property (nonatomic, retain, readwrite) NSNetService* currentResolve;
81
@property (nonatomic, retain, readwrite) NSTimer* timer;
82
@property (nonatomic, assign, readwrite) BOOL needsActivityIndicator;
83
@property (nonatomic, assign, readwrite) BOOL initialWaitOver;
84
85
- (void)stopCurrentResolve;
86
- (void)initialWaitOver:(NSTimer*)timer;
87
@end
88
89
@implementation BrowserViewController
90
91
@synthesize delegate = _delegate;
92
@synthesize showDisclosureIndicators = _showDisclosureIndicators;
93
@synthesize currentResolve = _currentResolve;
94
@synthesize netServiceBrowser = _netServiceBrowser;
95
@synthesize services = _services;
96
@synthesize needsActivityIndicator = _needsActivityIndicator;
97
@dynamic timer;
98
@synthesize initialWaitOver = _initialWaitOver;
99
100
- (id)initWithTitle:(NSString*)title showDisclosureIndicators:(BOOL)show showCancelButton:(BOOL)showCancelButton {
101
	
102
	if ((self = [super initWithStyle:UITableViewStylePlain])) {
103
		self.title = title;
104
		_services = [[NSMutableArray alloc] init];
105
		self.showDisclosureIndicators = show;
106
107
		if (showCancelButton) {
108
			// add Cancel button as the nav bar's custom right view
109
			UIBarButtonItem *addButton = [[UIBarButtonItem alloc]
110
										  initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelAction)];
111
			self.navigationItem.rightBarButtonItem = addButton;
112
			[addButton release];
113
		}
114
115
		// Make sure we have a chance to discover devices before showing the user that nothing was found (yet)
116
		[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(initialWaitOver:) userInfo:nil repeats:NO];
117
	}
118
119
	return self;
120
}
121
122
- (NSString *)searchingForServicesString {
123
	return _searchingForServicesString;
124
}
125
126
// Holds the string that's displayed in the table view during service discovery.
127
- (void)setSearchingForServicesString:(NSString *)searchingForServicesString {
128
	if (_searchingForServicesString != searchingForServicesString) {
129
		[_searchingForServicesString release];
130
		_searchingForServicesString = [searchingForServicesString copy];
131
132
        // If there are no services, reload the table to ensure that searchingForServicesString appears.
133
		if ([self.services count] == 0) {
134
			[self.tableView reloadData];
135
		}
136
	}
137
}
138
139
// Creates an NSNetServiceBrowser that searches for services of a particular type in a particular domain.
140
// If a service is currently being resolved, stop resolving it and stop the service browser from
141
// discovering other services.
142
- (BOOL)searchForServicesOfType:(NSString *)type inDomain:(NSString *)domain {
143
	
144
	[self stopCurrentResolve];
145
	[self.netServiceBrowser stop];
146
	[self.services removeAllObjects];
147
148
	NSNetServiceBrowser *aNetServiceBrowser = [[NSNetServiceBrowser alloc] init];
149
	if(!aNetServiceBrowser) {
150
        // The NSNetServiceBrowser couldn't be allocated and initialized.
151
		return NO;
152
	}
153
154
	aNetServiceBrowser.delegate = self;
155
	self.netServiceBrowser = aNetServiceBrowser;
156
	[aNetServiceBrowser release];
157
	[self.netServiceBrowser searchForServicesOfType:type inDomain:domain];
158
159
	[self.tableView reloadData];
160
	return YES;
161
}
162
163
164
- (NSTimer *)timer {
165
	return _timer;
166
}
167
168
// When this is called, invalidate the existing timer before releasing it.
169
- (void)setTimer:(NSTimer *)newTimer {
170
	[_timer invalidate];
171
	[newTimer retain];
172
	[_timer release];
173
	_timer = newTimer;
174
}
175
176
177
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
178
	return 1;
179
}
180
181
182
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
183
	// If there are no services and searchingForServicesString is set, show one row to tell the user.
184
	NSUInteger count = [self.services count];
185
	if (count == 0 && self.searchingForServicesString && self.initialWaitOver)
186
		return 1;
187
188
	return count;
189
}
190
191
192
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
193
	static NSString *tableCellIdentifier = @"UITableViewCell";
194
	UITableViewCell *cell = (UITableViewCell *)[tableView dequeueReusableCellWithIdentifier:tableCellIdentifier];
195
	if (cell == nil) {
196
		cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tableCellIdentifier] autorelease];
197
	}
198
	
199
	NSUInteger count = [self.services count];
200
	if (count == 0 && self.searchingForServicesString) {
201
        // If there are no services and searchingForServicesString is set, show one row explaining that to the user.
202
        cell.textLabel.text = self.searchingForServicesString;
203
		cell.textLabel.textColor = [UIColor colorWithWhite:0.5 alpha:0.5];
204
		cell.accessoryType = UITableViewCellAccessoryNone;
205
		// Make sure to get rid of the activity indicator that may be showing if we were resolving cell zero but
206
		// then got didRemoveService callbacks for all services (e.g. the network connection went down).
207
		if (cell.accessoryView)
208
			cell.accessoryView = nil;
209
		return cell;
210
	}
211
	
212
	// Set up the text for the cell
213
	NSNetService* service = [self.services objectAtIndex:indexPath.row];
214
	cell.textLabel.text = [service name];
215
	cell.textLabel.textColor = [UIColor blackColor];
216
	cell.accessoryType = self.showDisclosureIndicators ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
217
	
218
	// Note that the underlying array could have changed, and we want to show the activity indicator on the correct cell
219
	if (self.needsActivityIndicator && self.currentResolve == service) {
220
		if (!cell.accessoryView) {
221
			CGRect frame = CGRectMake(0.0, 0.0, kProgressIndicatorSize, kProgressIndicatorSize);
222
			UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithFrame:frame];
223
			[spinner startAnimating];
224
			spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
225
			[spinner sizeToFit];
226
			spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin |
227
										UIViewAutoresizingFlexibleRightMargin |
228
										UIViewAutoresizingFlexibleTopMargin |
229
										UIViewAutoresizingFlexibleBottomMargin);
230
			cell.accessoryView = spinner;
231
			[spinner release];
232
		}
233
	} else if (cell.accessoryView) {
234
		cell.accessoryView = nil;
235
	}
236
	
237
	return cell;
238
}
239
240
241
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
242
	// Ignore the selection if there are no services as the searchingForServicesString cell
243
	// may be visible and tapping it would do nothing
244
	if ([self.services count] == 0)
245
		return nil;
246
	
247
	return indexPath;
248
}
249
250
251
- (void)stopCurrentResolve {
252
	self.needsActivityIndicator = NO;
253
	self.timer = nil;
254
255
	[self.currentResolve stop];
256
	self.currentResolve = nil;
257
}
258
259
260
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
261
	// If another resolve was running, stop it & remove the activity indicator from that cell
262
	if (self.currentResolve) {
263
		// Get the indexPath for the active resolve cell
264
		NSIndexPath* indexPath = [NSIndexPath indexPathForRow:[self.services indexOfObject:self.currentResolve] inSection:0];
265
		
266
		// Stop the current resolve, which will also set self.needsActivityIndicator
267
		[self stopCurrentResolve];
268
		
269
		// If we found the indexPath for the row, reload that cell to remove the activity indicator
270
		if (indexPath.row != NSNotFound)
271
			[self.tableView reloadRowsAtIndexPaths:[NSArray	arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
272
	}
273
	
274
	// Then set the current resolve to the service corresponding to the tapped cell
275
	self.currentResolve = [self.services objectAtIndex:indexPath.row];
276
	[self.currentResolve setDelegate:self];
277
	
278
	// Attempt to resolve the service. A value of 0.0 sets an unlimited time to resolve it. The user can
279
	// choose to cancel the resolve by selecting another service in the table view.
280
	[self.currentResolve resolveWithTimeout:0.0];
281
	
282
	// Make sure we give the user some feedback that the resolve is happening.
283
	// We will be called back asynchronously, so we don't want the user to think we're just stuck.
284
	// We delay showing this activity indicator in case the service is resolved quickly.
285
	self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(showWaiting:) userInfo:self.currentResolve repeats:NO];
286
}
287
288
289
// If necessary, sets up state to show an activity indicator to let the user know that a resolve is occuring.
290
- (void)showWaiting:(NSTimer*)timer {
291
	if (timer == self.timer) {
292
		NSNetService* service = (NSNetService*)[self.timer userInfo];
293
		if (self.currentResolve == service) {
294
			self.needsActivityIndicator = YES;
295
296
			NSIndexPath* indexPath = [NSIndexPath indexPathForRow:[self.services indexOfObject:self.currentResolve] inSection:0];
297
			if (indexPath.row != NSNotFound) {
298
				[self.tableView reloadRowsAtIndexPaths:[NSArray	arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
299
				// Deselect the row since the activity indicator shows the user something is happening.
300
				[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
301
			}
302
		}
303
	}
304
}
305
306
307
- (void)initialWaitOver:(NSTimer*)timer {
308
	self.initialWaitOver= YES;
309
	if (![self.services count])
310
		[self.tableView reloadData];
311
}
312
313
314
- (void)sortAndUpdateUI {
315
	// Sort the services by name.
316
	[self.services sortUsingSelector:@selector(localizedCaseInsensitiveCompareByName:)];
317
	[self.tableView reloadData];
318
}
319
320
321
- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveService:(NSNetService*)service moreComing:(BOOL)moreComing {
322
	// If a service went away, stop resolving it if it's currently being resolved,
323
	// remove it from the list and update the table view if no more events are queued.
324
	if (self.currentResolve && [service isEqual:self.currentResolve]) {
325
		[self stopCurrentResolve];
326
	}
327
	[self.services removeObject:service];
328
	
329
	// If moreComing is NO, it means that there are no more messages in the queue from the Bonjour daemon, so we should update the UI.
330
	// When moreComing is set, we don't update the UI so that it doesn't 'flash'.
331
	if (!moreComing) {
332
		[self sortAndUpdateUI];
333
	}
334
}	
335
336
337
- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing {
338
	// If a service came online, add it to the list and update the table view if no more events are queued.
339
	[self.services addObject:service];
340
341
	// If moreComing is NO, it means that there are no more messages in the queue from the Bonjour daemon, so we should update the UI.
342
	// When moreComing is set, we don't update the UI so that it doesn't 'flash'.
343
	if (!moreComing) {
344
		[self sortAndUpdateUI];
345
	}
346
}	
347
348
349
// This should never be called, since we resolve with a timeout of 0.0, which means indefinite
350
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
351
	[self stopCurrentResolve];
352
	[self.tableView reloadData];
353
}
354
355
356
- (void)netServiceDidResolveAddress:(NSNetService *)service {
357
	assert(service == self.currentResolve);
358
	
359
	[service retain];
360
	[self stopCurrentResolve];
361
	
362
	[self.delegate browserViewController:self didResolveInstance:service];
363
	[service release];
364
}
365
366
367
- (void)cancelAction {
368
	[self.delegate browserViewController:self didResolveInstance:nil];
369
}
370
371
372
- (void)dealloc {
373
	// Cleanup any running resolve and free memory
374
	[self stopCurrentResolve];
375
	self.services = nil;
376
	[self.netServiceBrowser stop];
377
	self.netServiceBrowser = nil;
378
	[_searchingForServicesString release];
379
	
380
	[super dealloc];
381
}
382
383
384
@end

Loggerhead 1.17 is a web-based interface for Bazaar branches