Ray的提示:这是我们新书iOS 6 Feast的第四篇教程!这篇教程出自我们的新书iOS 6 By Tutorials。写这篇教程的作者是Brandon Trebitowski – 他是我们教程团队的新成员并且也是我的好朋友。让我们开始吧!
这篇教程发自我们的教程团队成员 Brandon Trebitowski, 他是一名软件工程师,他会定期在 brandontreb.com发表文章。
在这教程的第一部分,你已经看到如何使用一个UICollectionView来展示一组在网格视图下的照片。
第二部分也是最后一部分,你将继续学习的旅程。我们将讲到如何来整合一个collection view和自定义它的headers。
现在让我们来使这个app变的更酷。如果我们给每一个搜索结果添加一个header的话,是不是会更好呢,这样就可以给用户他们在搜索什么的一个大致了解了。
你将会这个header创建一个新的类叫做UICollectionReusableView。这个类虽然是一个colletion view cell,但它却是用来当header或者footer的。
这个view将被建立在你的storyboard里面,然后将它与之对应的类相连。那我们先添加一个新文件FileNewFile…,然后选择 iOSCocoa TouchObjective-C class template,完了后点击Next。把这个类命名为FlickrPhotoHeaderView并让它变为UICollectionReusableView的一个子类。点击 Next然后Create来完成创建。
有两个outlets你必须先创建。打开FlickrPhotoHeaderView.m 然后加入如下代码:
@interface FlickrPhotoHeaderView () @property(weak) IBOutlet UIImageView *backgroundImageView; @property(weak) IBOutlet UILabel *searchLabel; @end |
这个类里面你定义了两个IBOutlets。UILabel是用来显示你的查询字符的,image view是文字的背景。为什么imageview需要用一个outlet,因为做为UILabel的背景,它将动态调整尺寸大小。
接下来,打开MainStoryboard.storyboard在左边Scene Inspector里面选择cellection view。打开它的Attributes Inspector然后在 Section Header框下方勾选Accessories:
如果你在左边的 scene inspector 仔细观察,你会发现有一个UICollectionReusableView 自动被加到了Collection View下方。选中UICollectionReusableView ,现在你可以开始添加subview了。把view设置到90 pixels。(或者,你可以直接在Size Inspector把view设置这个尺寸。)
拖一个image view到你的UICollectionReusableView 并且把它居中。目前来说这个image view的尺寸设置不是非常重要(但至少先把它设到400points或者更宽)。居中方式的话,你可以使用guides来确保。或者,你可以通过使用菜单里的EditorAlign然后选中horizontal and vertical centering。
接下来,把一个label直接拖到image view的上方并让它居中。把它的字体尺寸设为System 32.0,并且设置文字居中,然后设置它的字体颜色为蓝色。做完这些后,你的view将看上去这样:
我们要做的最后一步要通知UICollectionReusableView ,它是一个FlickrPhotoHeaderView 的子类并且把我们的outlets都连接起来。
在scene inspector点击Collection Reusable View然后打开它的Identity Inspector。把它的类设置为FlickrPhotoHeaderView。然后打开Attributes Inspector设置它的Identifier 为FlickrPhotoHeaderView。这个identifier 作用是让FlickrPhotoHeaderView在view上重复显示的。
同样,到Attributes inspector把FlickrPhotoHeaderView设为Reuse Identifier。这个是用来鉴别你的header view的,打开 Outlet Inspector然后把每个outlets与之对应的界面元素连接起来(backgroundImageView 和searchLabel)。
如果现在你编译并运行程序,你还是不会看到一个header的。因为你之前把这句话collectionView:viewForSupplementaryElementOfKind:atIndexPath:注释掉了。
让我们来修复此,打开ViewController.m 做如下操作:
#import "FlickrPhotoHeaderView.h" |
接下来取消下面这句话的注释:
- (UICollectionReusableView *)collectionView: (UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { FlickrPhotoHeaderView *headerView = [collectionView dequeueReusableSupplementaryViewOfKind: UICollectionElementKindSectionHeader withReuseIdentifier:@"FlickrPhotoHeaderView" forIndexPath:indexPath]; NSString *searchTerm = self.searches[indexPath.section]; [headerView setSearchText:searchTerm]; return headerView; } |
在上面的这段代码中,你为每个section排列了header view同时设置了 search text。这将通知collection view,哪个header是用来显示每个section的。setSearchText 这个方法明显你还没有具体写,所以你会得到一个编译错误。现在让我们来写它吧!
打开FlickrPhotoHeaderView.h 在@end 前添加如下代码:
-(void)setSearchText:(NSString *)text; |
然后切换到 FlickrPhotoHeaderView.m 并添加如下代码:
-(void)setSearchText:(NSString *)text { self.searchLabel.text = text; UIImage *shareButtonImage = [[UIImage imageNamed:@"header_bg.png"] resizableImageWithCapInsets: UIEdgeInsetsMake(68, 68, 68, 68)]; self.backgroundImageView.image = shareButtonImage; self.backgroundImageView.center = self.center; } |
setSearchText 为了背景图添加了一张新图片,并且设置了label上的字,然后把文字设到label的中心处。
现在让我们来运行看看。你会看到你的UI已经更新了。
这部教程的最后一部分我将向你展示的是通过触碰以及点击屏幕来整合你的collection view。有两种方式,第一种是带来一个modal view在里面把image显示在一个更大的window里。这个部分将展示如何通过email分享图片来添加对图片的多选。
那么现在我们的任务是先创建一个用户点击cell后需要弹出的modal view controller。
到 FileNewFile…, 选择 iOSCocoa TouchObjective-C class template 然后点击 Next。接下来,将之命名为FlickrPhotoViewController,并使它成为UIViewController的子类,把 Targeted for iPad的勾子选上。因为你将在storyboard里面来布局这个类,所以请确认不要选择同时生产xib文件。点击 Next 然后 Create 创建这个类。
打开FlickrPhotoViewController.h 写入以下代码:
@class FlickrPhoto; @interface FlickrPhotoViewController : UIViewController @property(nonatomic, strong) FlickrPhoto *flickrPhoto; @end |
这个对FlickrPhoto 添加了一个public 属性。这个属性负责在弹出的modal里面被展示。
现在,打开FlickrPhotoViewController.m然后添加如下代码:
#import "Flickr.h" #import "FlickrPhoto.h" |
在.h文件中的 @interface 部分添加如下代码:
@property (weak) IBOutlet UIImageView *imageView; -(IBAction)done:(id) sender; |
这image的outlet是用来展示图片的,这个IBAction是用来关闭你点击Done按钮后弹出的view的。
我们先把done:方法空白:
- (IBAction)done:(id)sender { // TODO } |
现在打开MainStoryboard.storyboard。然后拖一个view controller对象到main window上。选中新来的view controller,然后切换到Identitiy Inspector里,把它的类命名为FlickrPhotoViewController。
然后,从你的main view controller拖一个线到到你新的view controller上,在这上面会弹出一个菜单,你选择modal 来创建我们的这个segue。
接下来是配置segue。点击segue然后打开Attributes Inspector。把它的Identifier设为ShowFlickrPhoto然后presentation设为From Sheet。与此同时,你应该看到你的Flickr Photo View Controller会缩小到一个form sheet的大小。
现在,拖一个toolbar和一个image view到你的Flickr Photo View Controller的main view。把toolbar按钮的文字改为“Done”然后在Scene Inspector连接它到Flickr Photo View Controller并在弹出的菜单里选择done:。
接下来,把 Flickr Photo View Controller和它里面的image view的outlet连接起来。
打开ViewController.m然后在.h文件里添加如下属性:
@property (nonatomic) BOOL sharing; |
这个boolean属性的作用是:当它的值为true的时候,用户是在使用多选来分享图片(待会你就会写到了),但当它的值是false的时候(这意味着用户点击图片后它会显示在弹出的modal view中)。
用以下代码来代替collectionView:didSelectItemAtIndexPath:里现存的(这个是你对某个collection view里的row被选中后的所调用的回调函数):
if (!self.sharing) { NSString *searchTerm = self.searches[indexPath.section]; FlickrPhoto *photo = self.searchResults[searchTerm][indexPath.row]; [self performSegueWithIdentifier:@"ShowFlickrPhoto" sender:photo]; [self.collectionView deselectItemAtIndexPath:indexPath animated:YES]; } else { // Todo: Multi-Selection } |
如果用户不是在sharing 模式下(目前他们还不是),你将取得他们点击的照片并且通过ShowFlickrPhoto segue来展示他们。请注意一下,你它照片是通过sender来传送的。这将允许你决定使用哪个照片来展示在modal view中。最后,我们把cell 设为deselected,它就不会保持高亮状态了。
另外还有一个方法你必须写完,那么整个presetation才会正常工作。在一个segue执行之前,prepareForSegue:sender这个方法先要写好。
确认你在ViewController.m的顶部先导入了FlickrPhotoViewController :
#import "FlickrPhotoViewController.h" |
然后,在文件最后添加如下代码:
#pragma mark - Segue - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"ShowFlickrPhoto"]) { FlickrPhotoViewController *flickrPhotoViewController = segue.destinationViewController; flickrPhotoViewController.flickrPhoto = sender; } } |
这个方法设置了segue的触发(这里是用户点击photo),并且把sender传递为目标view controller的属性(这里目标view controller是FlickrPhotoViewController )。现在万事俱备了。
编译并运行程序,提交一个搜索,然后点击一个照片。你却只看到弹出一个空白的image view。
这是为什么呢?为什么你的照片还不显示呢?这是因为你还没有在FlickrPhotoViewController 里面写到设置image view的代码。
让我们修复它,打开FlickrPhotoViewController.m然后添加如下代码:
-(void)viewDidAppear:(BOOL)animated { // 1 if(self.flickrPhoto.largeImage) { self.imageView.image = self.flickrPhoto.largeImage; } else { // 2 self.imageView.image = self.flickrPhoto.thumbnail; // 3 [Flickr loadImageForPhoto:self.flickrPhoto thumbnail:NO completionBlock:^(UIImage *photoImage, NSError *error) { if(!error) { // 4 dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = self.flickrPhoto.largeImage; }); } }]; } }  |
让我们来一步步剖析之。
再次编译并运行。提交一个查询并且点击一个结果。你将看到image在一个弹出的modal view里面显示了。它开始时可能会有点模糊,但是由于它很快会被一个大像素照片来代替,所以它很快就会变清晰了。
提示: 如果你有以下这个问题,thumbnail image显示得很小在full image加载完之前,请尝试设置UIImageView的Content Hugging Priority和 Content Compression Resistance Priority到一个非常小的数量。
酷!当然,如果你想通过点击Done按钮来是弹出的modal view消失掉,你会发现done按钮没有用嘛。噢,我们忘记写这个done:方法了。
把如下代码加入到FlickrPhotoViewController.m:
-(void)done:(id)sender { [self.presentingViewController dismissViewControllerAnimated:YES completion:^{}]; } |
现在,当用户点击Done按钮后,view会自动消失了。
这部教程你最后的任务就是允许用户来多选他们的图片然后分享给他们的朋友。对于设置UICollectionView的多选过程与设置UITableView的非常相似。唯一的把戏就是通知collection view来允许多选。
这个过程我们可以分析为如下几步:
让我们先从创建储存选中照片的array开始。
打开ViewController.m文件并在@interface 下添加如下属性声明:
@property(nonatomic, strong) NSMutableArray *selectedPhotos; |
在viewDidLoad中添加如下代码:
self.selectedPhotos = [@[] mutableCopy]; |
现在我们的array已经被设置了,那么是时候添加一些内容进去了。在collectionView:didSelectItemAtIndexPath中“ToDo”下面添加如下代码:
NSString *searchTerm = self.searches[indexPath.section]; FlickrPhoto *photo = self.searchResults[searchTerm][indexPath.row]; [self.selectedPhotos addObject:photo]; |
这段代码表示当用户选择照片时,照片会被加到selectedPhotos array里。
接下来,更新collectionView:didDeselectItemAtIndexPath:里面的代码:
if (self.sharing) { NSString *searchTerm = self.searches[indexPath.section]; FlickrPhoto *photo = self.searchResults[searchTerm][indexPath.row]; [self.selectedPhotos removeObject:photo]; } |
这将处理用户不小心取消选择照片后的情况。
现在,来写shareButtonTapped:方法里面的代码:
-(IBAction)shareButtonTapped:(id)sender { UIBarButtonItem *shareButton = (UIBarButtonItem *)sender; // 1 if (!self.sharing) { self.sharing = YES; [shareButton setStyle:UIBarButtonItemStyleDone]; [shareButton setTitle:@"Done"]; [self.collectionView setAllowsMultipleSelection:YES]; } else { // 2 self.sharing = NO; [shareButton setStyle:UIBarButtonItemStyleBordered]; [shareButton setTitle:@"Share"]; [self.collectionView setAllowsMultipleSelection:NO]; // 3 if ([self.selectedPhotos count] > 0) { [self showMailComposerAndSend]; } // 4 for(NSIndexPath *indexPath in self.collectionView.indexPathsForSelectedItems) { [self.collectionView deselectItemAtIndexPath:indexPath animated:NO]; } [self.selectedPhotos removeAllObjects]; } } |
让我们来逐一剖析之:
你现在还不能运行程序因为你还没有写showMailComposerAndSend这个方法。一会我们就做这个。
因为这个项目要使用到MFMailComposeViewController,所以你必须导入MessageUI 框架到你的工程。点击项目Navigator ,然后选中Flickr Search 目标。然后点击Build Phases选项卡来扩展Binary With Libraries menu。点击(+)按钮,搜索MessageUI框架然后点击Add来完成添加。
添加完后,请到ViewController.m:顶部导入MessageUI 框架。
#import 《MessageUI/MessageUI.h》; |
(这里应该是左右方括号,这是html代码问题)
并且使ViewController 支持MFMailComposeViewControllerDelegate 协议的方法通过加入如下代码:
@interface ViewController ()<UITextFieldDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, MFMailComposeViewControllerDelegate> |
做了这些基本措施后,让我们在.m文件中添加showMailComposerAndSend 这个方法:
-(void)showMailComposerAndSend { if ([MFMailComposeViewController canSendMail]) { MFMailComposeViewController *mailer = [[MFMailComposeViewController alloc] init]; mailer.mailComposeDelegate = self; [mailer setSubject:@"Check out these Flickr Photos"]; NSMutableString *emailBody = [NSMutableString string]; for(FlickrPhoto *flickrPhoto in self.selectedPhotos) { NSString *url = [Flickr flickrPhotoURLForFlickrPhoto: flickrPhoto size:@"m"]; [emailBody appendFormat:@"《div》《img src='%@'》《/div》《br》",url];(同样也是方括号,HTML代码蛋疼) } [mailer setMessageBody:emailBody isHTML:YES]; [self presentViewController:mailer animated:YES completion:^{}]; } else { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Mail Failure" message:@"Your device doesn't support in-app email" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil]; [alert show]; } } |
这段代码首先确认用户是不是能够发送邮件。如果他在他的设备上还没设置任何邮件帐号的话,我们就要发出alert来通知用户了。
另外邮件的body应该是一些基础的HTML代码来允许你展示照片而不是通过把他们设为附件。一旦mail 的主题和body被设置好了以后,邮件编辑器就会展示给用户了。
你也同样需要处理当用户取消他们邮件时候的操作,在邮件的delegate里执行如下代码:
- (void)mailComposeController: (MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { [controller dismissViewControllerAnimated:YES completion:^{}]; } |
现在当用户点击done的时候,它会把邮件编辑器消散掉了。让我们来运行一下,并且测试一下多选照片。
有一个问题 – 当用户选择一个照片的时候,它没有视觉效果啊。这样用户就不知道他到底选了哪些照片了。这个问题我们可以简单通过设置你FlickrPhotoCell的背景颜色来修复掉。
打开FlickrPhotoCell.m,删除 initWithFrame:,用如下代码来替代它:
-(id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { UIView *bgView = [[UIView alloc] initWithFrame:self.backgroundView.frame]; bgView.backgroundColor = [UIColor blueColor]; bgView.layer.borderColor = [[UIColor whiteColor] CGColor]; bgView.layer.borderWidth = 4; self.selectedBackgroundView = bgView; } return self; } |
当view被从一个xib文件初始化的时候,它会触发initWithCoder:这个方法。这段代码创建了一个cell的selectedBackgroundView ,并将之设为蓝色的背景和白色的边。那么当我们再此选中cell的时候,cell的背景色会自动选用selectedBackgroundView 来填满。
编译并运行程序,点击share,然后选中一些照片,应该看上去如下所示:
哇噢!非常好看的选中显示。请确认取消选择也同样正常运行。看看我们的蓝色高亮会不会消失掉。
这是我们工程的 完整版。
恭喜,你已经完成创建你自己的非常酷和新颖的Flickr照片游览器,通过把它们显示在一个基于网格视图的UICollectionView 里!
在这个过程中,你学到了如何自定义UICollectionViewCells,创建UICollectionReusableView的headers,以及推测何时rows被选中了,支持多选操作等等!
如果在iOS 6之前,我们的app要做到这个效果,那将会是一件挺麻烦的事情。你可能需要找一个第三方的类库来解决。现在我们可以像使用UITableView 一样来happy的使用UICollectionView了!
但是,最后的但是了!我可以这么说,目前你还只是掌握了UICollectionView 的皮毛。在我们的iOS 6 By Tutorials的下一章节,你会将你现在的程序扩展到能支持自定义照片布局,比如一个“照片堆”布局啊,一个“瀑布式(Pinterest样式)”布局啊, 以及“折叠”流布局样式。你还会学到,如何删除collection view里面的元素等等!
但愿这篇教程能让你为iOS 6的新东西collection view感到兴奋, 我也希望能向你展示一些你能做到的更酷的东西!
如果你有任何的问题或者意见,请加入到下方的论坛讨论吧!