2
File: BrowserViewController.m
3
Abstract: View controller for the service instance list.
4
This object manages a NSNetServiceBrowser configured to look for Bonjour
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
11
Selecting an item in the table view asynchronously resolves the corresponding
13
When that resolution completes, the delegate is called with the corresponding
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.
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.
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.
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.
56
Copyright (C) 2009 Apple Inc. All Rights Reserved.
60
#import "BrowserViewController.h"
62
#define kProgressIndicatorSize 20.0
64
// A category on NSNetService that's used to sort NSNetService objects by their name.
65
@interface NSNetService (BrowserViewControllerAdditions)
66
- (NSComparisonResult) localizedCaseInsensitiveCompareByName:(NSNetService*)aService;
69
@implementation NSNetService (BrowserViewControllerAdditions)
70
- (NSComparisonResult) localizedCaseInsensitiveCompareByName:(NSNetService*)aService {
71
return [[self name] localizedCaseInsensitiveCompare:[aService name]];
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;
85
- (void)stopCurrentResolve;
86
- (void)initialWaitOver:(NSTimer*)timer;
89
@implementation BrowserViewController
91
@synthesize delegate = _delegate;
92
@synthesize showDisclosureIndicators = _showDisclosureIndicators;
93
@synthesize currentResolve = _currentResolve;
94
@synthesize netServiceBrowser = _netServiceBrowser;
95
@synthesize services = _services;
96
@synthesize needsActivityIndicator = _needsActivityIndicator;
98
@synthesize initialWaitOver = _initialWaitOver;
100
- (id)initWithTitle:(NSString*)title showDisclosureIndicators:(BOOL)show showCancelButton:(BOOL)showCancelButton {
102
if ((self = [super initWithStyle:UITableViewStylePlain])) {
104
_services = [[NSMutableArray alloc] init];
105
self.showDisclosureIndicators = show;
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;
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];
122
- (NSString *)searchingForServicesString {
123
return _searchingForServicesString;
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];
132
// If there are no services, reload the table to ensure that searchingForServicesString appears.
133
if ([self.services count] == 0) {
134
[self.tableView reloadData];
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 {
144
[self stopCurrentResolve];
145
[self.netServiceBrowser stop];
146
[self.services removeAllObjects];
148
NSNetServiceBrowser *aNetServiceBrowser = [[NSNetServiceBrowser alloc] init];
149
if(!aNetServiceBrowser) {
150
// The NSNetServiceBrowser couldn't be allocated and initialized.
154
aNetServiceBrowser.delegate = self;
155
self.netServiceBrowser = aNetServiceBrowser;
156
[aNetServiceBrowser release];
157
[self.netServiceBrowser searchForServicesOfType:type inDomain:domain];
159
[self.tableView reloadData];
168
// When this is called, invalidate the existing timer before releasing it.
169
- (void)setTimer:(NSTimer *)newTimer {
177
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
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)
192
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
193
static NSString *tableCellIdentifier = @"UITableViewCell";
194
UITableViewCell *cell = (UITableViewCell *)[tableView dequeueReusableCellWithIdentifier:tableCellIdentifier];
196
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tableCellIdentifier] autorelease];
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;
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;
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;
226
spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin |
227
UIViewAutoresizingFlexibleRightMargin |
228
UIViewAutoresizingFlexibleTopMargin |
229
UIViewAutoresizingFlexibleBottomMargin);
230
cell.accessoryView = spinner;
233
} else if (cell.accessoryView) {
234
cell.accessoryView = nil;
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)
251
- (void)stopCurrentResolve {
252
self.needsActivityIndicator = NO;
255
[self.currentResolve stop];
256
self.currentResolve = nil;
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];
266
// Stop the current resolve, which will also set self.needsActivityIndicator
267
[self stopCurrentResolve];
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];
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];
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];
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];
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;
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];
307
- (void)initialWaitOver:(NSTimer*)timer {
308
self.initialWaitOver= YES;
309
if (![self.services count])
310
[self.tableView reloadData];
314
- (void)sortAndUpdateUI {
315
// Sort the services by name.
316
[self.services sortUsingSelector:@selector(localizedCaseInsensitiveCompareByName:)];
317
[self.tableView reloadData];
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];
327
[self.services removeObject:service];
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'.
332
[self sortAndUpdateUI];
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];
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'.
344
[self sortAndUpdateUI];
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];
356
- (void)netServiceDidResolveAddress:(NSNetService *)service {
357
assert(service == self.currentResolve);
360
[self stopCurrentResolve];
362
[self.delegate browserViewController:self didResolveInstance:service];
367
- (void)cancelAction {
368
[self.delegate browserViewController:self didResolveInstance:nil];
373
// Cleanup any running resolve and free memory
374
[self stopCurrentResolve];
376
[self.netServiceBrowser stop];
377
self.netServiceBrowser = nil;
378
[_searchingForServicesString release];