bzr branch
/browse/iphone/common
| 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 | } |
|
| 113 | ||
| 114 | // Make sure we have a chance to discover devices before showing the user that nothing was found (yet) |
|
| 115 | [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(initialWaitOver:) userInfo:nil repeats:NO]; |
|
| 116 | } |
|
| 117 | ||
| 118 | return self; |
|
| 119 | } |
|
| 120 | ||
| 121 | - (NSString *)searchingForServicesString { |
|
| 122 | return _searchingForServicesString; |
|
| 123 | } |
|
| 124 | ||
| 125 | // Holds the string that's displayed in the table view during service discovery. |
|
| 126 | - (void)setSearchingForServicesString:(NSString *)searchingForServicesString { |
|
| 127 | if (_searchingForServicesString != searchingForServicesString) { |
|
| 128 | 94 | |
| 129 | 46 | _searchingForServicesString = [searchingForServicesString copy]; |
| 130 | ||
| 131 | // If there are no services, reload the table to ensure that searchingForServicesString appears. |
|
| 132 | if ([self.services count] == 0) { |
|
| 133 | [self.tableView reloadData]; |
|
| 134 | } |
|
| 135 | } |
|
| 136 | } |
|
| 137 | ||
| 138 | // Creates an NSNetServiceBrowser that searches for services of a particular type in a particular domain. |
|
| 139 | // If a service is currently being resolved, stop resolving it and stop the service browser from |
|
| 140 | // discovering other services. |
|
| 141 | - (BOOL)searchForServicesOfType:(NSString *)type inDomain:(NSString *)domain { |
|
| 142 | |
|
| 143 | [self stopCurrentResolve]; |
|
| 144 | [self.netServiceBrowser stop]; |
|
| 145 | [self.services removeAllObjects]; |
|
| 146 | ||
| 147 | NSNetServiceBrowser *aNetServiceBrowser = [[NSNetServiceBrowser alloc] init]; |
|
| 148 | if(!aNetServiceBrowser) { |
|
| 149 | // The NSNetServiceBrowser couldn't be allocated and initialized. |
|
| 150 | return NO; |
|
| 151 | } |
|
| 152 | ||
| 153 | aNetServiceBrowser.delegate = self; |
|
| 154 | self.netServiceBrowser = aNetServiceBrowser; |
|
| 155 | 94 | |
| 156 | 46 | [self.netServiceBrowser searchForServicesOfType:type inDomain:domain]; |
| 157 | ||
| 158 | [self.tableView reloadData]; |
|
| 159 | return YES; |
|
| 160 | } |
|
| 161 | ||
| 162 | ||
| 163 | - (NSTimer *)timer { |
|
| 164 | return _timer; |
|
| 165 | } |
|
| 166 | ||
| 167 | // When this is called, invalidate the existing timer before releasing it. |
|
| 168 | - (void)setTimer:(NSTimer *)newTimer { |
|
| 169 | [_timer invalidate]; |
|
| 170 | _timer = newTimer; |
|
| 171 | } |
|
| 172 | ||
| 173 | ||
| 174 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { |
|
| 175 | return 1; |
|
| 176 | } |
|
| 177 | ||
| 178 | ||
| 179 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { |
|
| 180 | // If there are no services and searchingForServicesString is set, show one row to tell the user. |
|
| 181 | NSUInteger count = [self.services count]; |
|
| 182 | if (count == 0 && self.searchingForServicesString && self.initialWaitOver) |
|
| 183 | return 1; |
|
| 184 | ||
| 185 | return count; |
|
| 186 | } |
|
| 187 | ||
| 188 | ||
| 189 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { |
|
| 190 | static NSString *tableCellIdentifier = @"UITableViewCell"; |
|
| 191 | UITableViewCell *cell = (UITableViewCell *)[tableView dequeueReusableCellWithIdentifier:tableCellIdentifier]; |
|
| 192 | if (cell == nil) { |
|
| 193 | 94 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tableCellIdentifier]; |
| 194 | 46 | } |
| 195 | |
|
| 196 | NSUInteger count = [self.services count]; |
|
| 197 | if (count == 0 && self.searchingForServicesString) { |
|
| 198 | // If there are no services and searchingForServicesString is set, show one row explaining that to the user. |
|
| 199 | cell.textLabel.text = self.searchingForServicesString; |
|
| 200 | cell.textLabel.textColor = [UIColor colorWithWhite:0.5 alpha:0.5]; |
|
| 201 | cell.accessoryType = UITableViewCellAccessoryNone; |
|
| 202 | // Make sure to get rid of the activity indicator that may be showing if we were resolving cell zero but |
|
| 203 | // then got didRemoveService callbacks for all services (e.g. the network connection went down). |
|
| 204 | if (cell.accessoryView) |
|
| 205 | cell.accessoryView = nil; |
|
| 206 | return cell; |
|
| 207 | } |
|
| 208 | |
|
| 209 | // Set up the text for the cell |
|
| 210 | NSNetService* service = [self.services objectAtIndex:indexPath.row]; |
|
| 211 | cell.textLabel.text = [service name]; |
|
| 212 | cell.textLabel.textColor = [UIColor blackColor]; |
|
| 213 | cell.accessoryType = self.showDisclosureIndicators ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone; |
|
| 214 | |
|
| 215 | // Note that the underlying array could have changed, and we want to show the activity indicator on the correct cell |
|
| 216 | if (self.needsActivityIndicator && self.currentResolve == service) { |
|
| 217 | if (!cell.accessoryView) { |
|
| 218 | CGRect frame = CGRectMake(0.0, 0.0, kProgressIndicatorSize, kProgressIndicatorSize); |
|
| 219 | UIActivityIndicatorView* spinner = [[UIActivityIndicatorView alloc] initWithFrame:frame]; |
|
| 220 | [spinner startAnimating]; |
|
| 221 | spinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; |
|
| 222 | [spinner sizeToFit]; |
|
| 223 | spinner.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | |
|
| 224 | UIViewAutoresizingFlexibleRightMargin | |
|
| 225 | UIViewAutoresizingFlexibleTopMargin | |
|
| 226 | UIViewAutoresizingFlexibleBottomMargin); |
|
| 227 | cell.accessoryView = spinner; |
|
| 228 | } |
|
| 229 | } else if (cell.accessoryView) { |
|
| 230 | cell.accessoryView = nil; |
|
| 231 | } |
|
| 232 | |
|
| 233 | return cell; |
|
| 234 | } |
|
| 235 | ||
| 236 | ||
| 237 | - (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { |
|
| 238 | // Ignore the selection if there are no services as the searchingForServicesString cell |
|
| 239 | // may be visible and tapping it would do nothing |
|
| 240 | if ([self.services count] == 0) |
|
| 241 | return nil; |
|
| 242 | |
|
| 243 | return indexPath; |
|
| 244 | } |
|
| 245 | ||
| 246 | ||
| 247 | - (void)stopCurrentResolve { |
|
| 248 | self.needsActivityIndicator = NO; |
|
| 249 | self.timer = nil; |
|
| 250 | ||
| 251 | [self.currentResolve stop]; |
|
| 252 | self.currentResolve = nil; |
|
| 253 | } |
|
| 254 | ||
| 255 | ||
| 256 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { |
|
| 257 | // If another resolve was running, stop it & remove the activity indicator from that cell |
|
| 258 | if (self.currentResolve) { |
|
| 259 | // Get the indexPath for the active resolve cell |
|
| 260 | NSIndexPath* indexPath = [NSIndexPath indexPathForRow:[self.services indexOfObject:self.currentResolve] inSection:0]; |
|
| 261 | |
|
| 262 | // Stop the current resolve, which will also set self.needsActivityIndicator |
|
| 263 | [self stopCurrentResolve]; |
|
| 264 | |
|
| 265 | // If we found the indexPath for the row, reload that cell to remove the activity indicator |
|
| 266 | if (indexPath.row != NSNotFound) |
|
| 267 | [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; |
|
| 268 | } |
|
| 269 | |
|
| 270 | // Then set the current resolve to the service corresponding to the tapped cell |
|
| 271 | self.currentResolve = [self.services objectAtIndex:indexPath.row]; |
|
| 272 | [self.currentResolve setDelegate:self]; |
|
| 273 | |
|
| 274 | // Attempt to resolve the service. A value of 0.0 sets an unlimited time to resolve it. The user can |
|
| 275 | // choose to cancel the resolve by selecting another service in the table view. |
|
| 276 | [self.currentResolve resolveWithTimeout:0.0]; |
|
| 277 | |
|
| 278 | // Make sure we give the user some feedback that the resolve is happening. |
|
| 279 | // We will be called back asynchronously, so we don't want the user to think we're just stuck. |
|
| 280 | // We delay showing this activity indicator in case the service is resolved quickly. |
|
| 281 | self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(showWaiting:) userInfo:self.currentResolve repeats:NO]; |
|
| 282 | } |
|
| 283 | ||
| 284 | ||
| 285 | // If necessary, sets up state to show an activity indicator to let the user know that a resolve is occuring. |
|
| 286 | - (void)showWaiting:(NSTimer*)timer { |
|
| 287 | if (timer == self.timer) { |
|
| 288 | NSNetService* service = (NSNetService*)[self.timer userInfo]; |
|
| 289 | if (self.currentResolve == service) { |
|
| 290 | self.needsActivityIndicator = YES; |
|
| 291 | ||
| 292 | NSIndexPath* indexPath = [NSIndexPath indexPathForRow:[self.services indexOfObject:self.currentResolve] inSection:0]; |
|
| 293 | if (indexPath.row != NSNotFound) { |
|
| 294 | [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; |
|
| 295 | // Deselect the row since the activity indicator shows the user something is happening. |
|
| 296 | [self.tableView deselectRowAtIndexPath:indexPath animated:YES]; |
|
| 297 | } |
|
| 298 | } |
|
| 299 | } |
|
| 300 | } |
|
| 301 | ||
| 302 | ||
| 303 | - (void)initialWaitOver:(NSTimer*)timer { |
|
| 304 | self.initialWaitOver= YES; |
|
| 305 | if (![self.services count]) |
|
| 306 | [self.tableView reloadData]; |
|
| 307 | } |
|
| 308 | ||
| 309 | ||
| 310 | - (void)sortAndUpdateUI { |
|
| 311 | // Sort the services by name. |
|
| 312 | [self.services sortUsingSelector:@selector(localizedCaseInsensitiveCompareByName:)]; |
|
| 313 | [self.tableView reloadData]; |
|
| 314 | } |
|
| 315 | ||
| 316 | ||
| 317 | - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didRemoveService:(NSNetService*)service moreComing:(BOOL)moreComing { |
|
| 318 | // If a service went away, stop resolving it if it's currently being resolved, |
|
| 319 | // remove it from the list and update the table view if no more events are queued. |
|
| 320 | if (self.currentResolve && [service isEqual:self.currentResolve]) { |
|
| 321 | [self stopCurrentResolve]; |
|
| 322 | } |
|
| 323 | [self.services removeObject:service]; |
|
| 324 | |
|
| 325 | // 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. |
|
| 326 | // When moreComing is set, we don't update the UI so that it doesn't 'flash'. |
|
| 327 | if (!moreComing) { |
|
| 328 | [self sortAndUpdateUI]; |
|
| 329 | } |
|
| 330 | } |
|
| 331 | ||
| 332 | ||
| 333 | - (void)netServiceBrowser:(NSNetServiceBrowser*)netServiceBrowser didFindService:(NSNetService*)service moreComing:(BOOL)moreComing { |
|
| 334 | // If a service came online, add it to the list and update the table view if no more events are queued. |
|
| 335 | [self.services addObject:service]; |
|
| 336 | ||
| 337 | // 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. |
|
| 338 | // When moreComing is set, we don't update the UI so that it doesn't 'flash'. |
|
| 339 | if (!moreComing) { |
|
| 340 | [self sortAndUpdateUI]; |
|
| 341 | } |
|
| 342 | } |
|
| 343 | ||
| 344 | ||
| 345 | // This should never be called, since we resolve with a timeout of 0.0, which means indefinite |
|
| 346 | - (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict { |
|
| 347 | [self stopCurrentResolve]; |
|
| 348 | [self.tableView reloadData]; |
|
| 349 | } |
|
| 350 | ||
| 351 | ||
| 352 | - (void)netServiceDidResolveAddress:(NSNetService *)service { |
|
| 353 | assert(service == self.currentResolve); |
|
| 354 | |
|
| 355 | [self stopCurrentResolve]; |
|
| 356 | |
|
| 357 | [self.delegate browserViewController:self didResolveInstance:service]; |
|
| 358 | } |
|
| 359 | ||
| 360 | ||
| 361 | - (void)cancelAction { |
|
| 362 | [self.delegate browserViewController:self didResolveInstance:nil]; |
|
| 363 | } |
|
| 364 | ||
| 365 | ||
| 366 | - (void)dealloc { |
|
| 367 | // Cleanup any running resolve and free memory |
|
| 368 | [self stopCurrentResolve]; |
|
| 369 | self.services = nil; |
|
| 370 | [self.netServiceBrowser stop]; |
|
| 371 | self.netServiceBrowser = nil; |
|
| 372 | |
|
| 373 | } |
|
| 374 | ||
| 375 | ||
| 376 | @end |
Loggerhead 1.17 is a web-based interface for Bazaar branches