BonjourSupport/BrowserViewController.m
/* |
File: BrowserViewController.m |
Abstract: View controller for the service instance list. |
This object manages a NSNetServiceBrowser configured to look for Bonjour |
services. |
It has an array of NSNetService objects that are displayed in a table view. |
When the service browser reports that it has discovered a service, the |
corresponding NSNetService is added to the array. |
When a service goes away, the corresponding NSNetService is removed from the |
array. |
Selecting an item in the table view asynchronously resolves the corresponding |
net service. |
When that resolution completes, the delegate is called with the corresponding |
NSNetService. |
Version: 2.9 |
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) 2010 Apple Inc. All Rights Reserved. |
*/ |
#import "BrowserViewController.h" |
#define kProgressIndicatorSize 20.0 |
// A category on NSNetService that's used to sort NSNetService objects by their name. |
@interface NSNetService (BrowserViewControllerAdditions) |
- (NSComparisonResult) localizedCaseInsensitiveCompareByName:(NSNetService*)aService; |
@end |
@implementation NSNetService (BrowserViewControllerAdditions) |
- (NSComparisonResult) localizedCaseInsensitiveCompareByName:(NSNetService*)aService { |
return [[self name] localizedCaseInsensitiveCompare:[aService name]]; |
} |
@end |
@interface BrowserViewController() |
@property (nonatomic, assign, readwrite) BOOL showDisclosureIndicators; |
@property (nonatomic, retain, readwrite) NSMutableArray* services; |
@property (nonatomic, retain, readwrite) NSNetServiceBrowser* netServiceBrowser; |
@property (nonatomic, retain, readwrite) NSNetService* currentResolve; |
@property (nonatomic, retain, readwrite) NSTimer* timer; |
@property (nonatomic, assign, readwrite) BOOL needsActivityIndicator; |
@property (nonatomic, assign, readwrite) BOOL initialWaitOver; |
- (void)stopCurrentResolve; |
- (void)initialWaitOver:(NSTimer*)timer; |
@end |
@implementation BrowserViewController |
@synthesize delegate = _delegate; |
@synthesize showDisclosureIndicators = _showDisclosureIndicators; |
@synthesize currentResolve = _currentResolve; |
@synthesize netServiceBrowser = _netServiceBrowser; |
@synthesize services = _services; |
@synthesize needsActivityIndicator = _needsActivityIndicator; |
@dynamic timer; |
@synthesize initialWaitOver = _initialWaitOver; |
- (id)initWithTitle:(NSString*)title showDisclosureIndicators:(BOOL)show showCancelButton:(BOOL)showCancelButton { |
if ((self = [super initWithStyle:UITableViewStylePlain])) { |
self.title = title; |
_services = [[NSMutableArray alloc] init]; |
self.showDisclosureIndicators = show; |
if (showCancelButton) { |
// add Cancel button as the nav bar's custom right view |
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] |
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancelAction)]; |
self.navigationItem.rightBarButtonItem = addButton; |
[addButton release]; |
} |
// Make sure we have a chance to discover devices before showing the user that nothing was found (yet) |
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(initialWaitOver:) userInfo:nil repeats:NO]; |
} |
return self; |
} |
- (NSString *)searchingForServicesString { |
return _searchingForServicesString; |
} |
// Holds the string that's displayed in the table view during service discovery. |
- (void)setSearchingForServicesString:(NSString *)searchingForServicesString { |
if (_searchingForServicesString != searchingForServicesString) { |
[_searchingForServicesString release]; |
_searchingForServicesString = [searchingForServicesString copy]; |
// If there are no services, reload the table to ensure that searchingForServicesString appears. |
if ([self.services count] == 0) { |
[self.tableView reloadData]; |
} |
} |
} |
// Creates an NSNetServiceBrowser that searches for services of a particular type in a particular domain. |
// If a service is currently being resolved, stop resolving it and stop the service browser from |
// discovering other services. |
- (BOOL)searchForServicesOfType:(NSString *)type inDomain:(NSString *)domain { |
[self stopCurrentResolve]; |
[self.netServiceBrowser stop]; |
[self.services removeAllObjects]; |
NSNetServiceBrowser *aNetServiceBrowser = [[NSNetServiceBrowser alloc] init]; |
if(!aNetServiceBrowser) { |
// The NSNetServiceBrowser couldn't be allocated and initialized. |
return NO; |
} |
aNetServiceBrowser.delegate = self; |
self.netServiceBrowser = aNetServiceBrowser; |
[aNetServiceBrowser release]; |
[self.netServiceBrowser searchForServicesOfType:type inDomain:domain]; |
[self.tableView reloadData]; |
return YES; |
} |
- (NSTimer *)timer { |
return _timer; |
} |
// When this is called, invalidate the existing timer before releasing it. |
- (void)setTimer:(NSTimer *)newTimer { |
[_timer invalidate]; |
[newTimer retain]; |
[_timer release]; |
_timer = newTimer; |
} |
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { |
return 1; |
} |
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { |
// If there are no services and searchingForServicesString is set, show one row to tell the user. |
NSUInteger count = [self.services count]; |
if (count == 0 && self.searchingForServicesString && self.initialWaitOver) |
return 1; |
return count; |
} |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { |
static NSString *tableCellIdentifier = @"UITableViewCell"; |
UITableViewCell *cell = (UITableViewCell *)[tableView dequeueReusableCellWithIdentifier:tableCellIdentifier]; |
if (cell == nil) { |
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tableCellIdentifier] autorelease]; |
} |
NSUInteger count = [self.services count]; |
if (count == 0 && self.searchingForServicesString) { |
// If there are no services and searchingForServicesString is set, show one row explaining that to the user. |
cell.textLabel.text = self.searchingForServicesString; |
cell.textLabel.textColor = [UIColor colorWithWhite:0.5 alpha:0.5]; |
cell.accessoryType = UITableViewCellAccessoryNone; |
// Make sure to get rid of the activity indicator that may be showing if we were resolving cell zero but |
// then got didRemoveService callbacks for all services (e.g. the network connection went down). |
if (cell.accessoryView) |
cell.accessoryView = nil; |
return cell; |
} |
// Set up the text for the cell |
NSNetService* service = [self.services objectAtIndex:indexPath.row]; |
cell.textLabel.text = [service name]; |
cell.textLabel.textColor = [UIColor blackColor]; |
cell.accessoryType = self.showDisclosureIndicators ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone; |
// Note that the underlying array could have changed, and we want to show the activity indicator on the correct cell |
if (self.needsActivityIndicator && self.currentResolve == service) { |
if (!cell.accessoryView) { |
CGRect frame = CGRectMake(0.0, 0.0, kProgressIndicatorSize, kProgressIndicatorSize); |
UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithFrame:frame]; |
[spinner startAnimating]; |
spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; |
[spinner sizeToFit]; |
spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | |
UIViewAutoresizingFlexibleRightMargin | |
UIViewAutoresizingFlexibleTopMargin | |
UIViewAutoresizingFlexibleBottomMargin); |
cell.accessoryView = spinner; |
[spinner release]; |
} |
} else if (cell.accessoryView) { |
cell.accessoryView = nil; |
} |
return cell; |
} |
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { |
// Ignore the selection if there are no services as the searchingForServicesString cell |
// may be visible and tapping it would do nothing |
if ([self.services count] == 0) |
return nil; |
return indexPath; |
} |
- (void)stopCurrentResolve { |
self.needsActivityIndicator = NO; |
self.timer = nil; |
[self.currentResolve stop]; |
self.currentResolve = nil; |
} |
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { |
// If another resolve was running, stop it & remove the activity indicator from that cell |
if (self.currentResolve) { |
// Get the indexPath for the active resolve cell |
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:[self.services indexOfObject:self.currentResolve] inSection:0]; |
// Stop the current resolve, which will also set self.needsActivityIndicator |
[self stopCurrentResolve]; |
// If we found the indexPath for the row, reload that cell to remove the activity indicator |
if (indexPath.row != NSNotFound) |
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; |
} |
// Then set the current resolve to the service corresponding to the tapped cell |
self.currentResolve = [self.services objectAtIndex:indexPath.row]; |
[self.currentResolve setDelegate:self]; |
// Attempt to resolve the service. A value of 0.0 sets an unlimited time to resolve it. The user can |
// choose to cancel the resolve by selecting another service in the table view. |
[self.currentResolve resolveWithTimeout:0.0]; |
// Make sure we give the user some feedback that the resolve is happening. |
// We will be called back asynchronously, so we don't want the user to think we're just stuck. |
// We delay showing this activity indicator in case the service is resolved quickly. |
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(showWaiting:) userInfo:self.currentResolve repeats:NO]; |
} |
// If necessary, sets up state to show an activity indicator to let the user know that a resolve is occuring. |
- (void)showWaiting:(NSTimer*)timer { |
if (timer == self.timer) { |
NSNetService* service = (NSNetService*)[self.timer userInfo]; |
if (self.currentResolve == service) { |
self.needsActivityIndicator = YES; |
NSIndexPath* indexPath = [NSIndexPath indexPathForRow:[self.services indexOfObject:self.currentResolve] inSection:0]; |
if (indexPath.row != NSNotFound) { |
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; |
// Deselect the row since the activity indicator shows the user something is happening. |
[self.tableView deselectRowAtIndexPath:indexPath animated:YES]; |
} |
} |
} |
} |
- (void)initialWaitOver:(NSTimer*)timer { |
self.initialWaitOver= YES; |
if (![self.services count]) |
[self.tableView reloadData]; |
} |
- (void)sortAndUpdateUI { |
// Sort the services by name. |
[self.services sortUsingSelector:@selector(localizedCaseInsensitiveCompareByName:)]; |
[self.tableView reloadData]; |
} |
- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveService:(NSNetService*)service moreComing:(BOOL)moreComing { |
// If a service went away, stop resolving it if it's currently being resolved, |
// remove it from the list and update the table view if no more events are queued. |
if (self.currentResolve && [service isEqual:self.currentResolve]) { |
[self stopCurrentResolve]; |
} |
[self.services removeObject:service]; |
// 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. |
// When moreComing is set, we don't update the UI so that it doesn't 'flash'. |
if (!moreComing) { |
[self sortAndUpdateUI]; |
} |
} |
- (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing { |
// If a service came online, add it to the list and update the table view if no more events are queued. |
[self.services addObject:service]; |
// 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. |
// When moreComing is set, we don't update the UI so that it doesn't 'flash'. |
if (!moreComing) { |
[self sortAndUpdateUI]; |
} |
} |
// This should never be called, since we resolve with a timeout of 0.0, which means indefinite |
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict { |
[self stopCurrentResolve]; |
[self.tableView reloadData]; |
} |
- (void)netServiceDidResolveAddress:(NSNetService *)service { |
assert(service == self.currentResolve); |
[service retain]; |
[self stopCurrentResolve]; |
[self.delegate browserViewController:self didResolveInstance:service]; |
[service release]; |
} |
- (void)cancelAction { |
[self.delegate browserViewController:self didResolveInstance:nil]; |
} |
- (void)dealloc { |
// Cleanup any running resolve and free memory |
[self stopCurrentResolve]; |
self.services = nil; |
[self.netServiceBrowser stop]; |
self.netServiceBrowser = nil; |
[_searchingForServicesString release]; |
[super dealloc]; |
} |
@end |
Copyright © 2010 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-06-16