本开源项目讲解了一些App常见功能界面的搭建以及实现思路,适合新手。
一个月前在简书上看到一篇ManoBoo小神发表的Swift-高仿半糖App,然后又顺藤摸瓜看到了维尼的小熊大神发表的iOS高仿爱鲜蜂,第一想法就是:太棒了!讲解了一些常见界面、效果的实现思路,作为正在自学iOS的我顿感崇拜,同时也在想:我能不能也写出一个App来?于是带着恐惧(这么难我应该写不出来吧…)我打开了Charles,抓了下厨房的数据,就这样八年的抗战 就这样一个月的仿写之旅开始了。
为什么是下厨房?
下厨房:一个集合了工具、社区与平台电商属性的家庭美食入口。很棒的一个平台,App界面也很好看!
关于项目(Github地址在文章结尾)
- 开发环境:Xcode 7.2,语言:Objective-C
- 用到的工具:Charles抓包工具
- 仿写程度:耗时一个月,每天12h+,能实现的基本都实现了
- 刚开始写的时候控件是用纯代码写的,后来发现太耗时间就改用Xib了
- 因为此前没看过谁的代码(ManoBoo跟维尼的小熊两位前辈的都是swift…我还没学swift就没看了),所以代码如果有不合理的地方(如:命名),请见谅
- 这个开源项目适合新手,基本的界面布局以及业务逻辑都有,看完这个基本也会写简单的App啦,但是因为我也是新手,代码很多不规范,所以学习实现思路就好,代码请不要借鉴。
- 非科班自学,请大神勿喷。
效果预览
一、首页
布局
- 如图,首页tableView就可以搞定
- 日期标题为sectionHeader
- 下面的就是cell了,下厨房返回的数据中cell有6种模板,通过自定义cell,根据不同模板显示不同效果,很简单就不描述了
思路:
顶部导航部分点击事件,通过给每个控件绑定tag
,然后定义对应的枚举变量,通过闭包(Block)将事件传递到控制器后,控制器判断枚举值即可。
1. 跳转的界面控制器
① 菜谱
布局
- 整个界面是一个UIViewController,放上一个tableView,然后添加底部的
收藏、丢进菜篮子
自定义view - 用料、做法、小贴士、被加入的菜单这四个标题是sectionHeader,其内容对应为一组,每组cell自定义即可
- 作品展示:
- 这里实现的方法跟上面的
用料、做法...
一样,独立为一组,组内只有一个cell,cell的contentView里从上至下添加:作品个数Label
、作品展示CollectionView
、所有作品Button
即可 - collectionView手势左滑,松开会加载更多作品数据
这里通过实现scrollView的代理方法,判断contentOffset
的值是否达到预定的数值,达到即调用block,然后控制器发送网络请求加载更多数据,刷新界面即可。(这里我只是实现了一个需求,并没有进一步优化调整)
- 这里实现的方法跟上面的
- 布局:
如图即可,底部加入菜单button
也可以是sectionFooter,
虽然下厨房几乎没有边框(有分割线),但仔细分析还是很好划分的
② 作品
布局
- 因为
关注动态
、买买买
界面跟这个差不多,需要复用到这个界面的内容,所以这整个界面是只有一个cell
的tableView - 上面是个图片轮播器,下面添加控件即可,
描述Label
以及用户评论Label
的高度是有内容决定的,这个只需要在模型中添加一个labelHeight
属性,然后在内部计算好高度,直接返回给控件就可以了。控件部分可以根据不同界面显示的不同效果,分割成若干部分,然后给cell添加一个type
属性(枚举类型),创建的时候告诉cell属于哪一种type
,然后根据不同type
进行调整即可。
2. 导航
① 关注动态
布局
- 整个界面就一个tableView,一个动态为
作品界面的cell
遇到的问题
- 图片轮播器会受
tableViewCell的复用机制
影响,导致错乱(点赞按钮的状态是由服务器返回的数据决定的,这里我就不模拟了)
解决办法:- 在控制器中添加一个记录图片轮播器滚动位置的数组属性
imageViewCurrentLocationArray
- 在图片轮播器里实现scrollView代理方法,监听记录最终的位移
contentOffset.x
,停止滚动后通过闭包/代理将位置数据
传递到控制器,控制器将位置数据
添加到记录数组
中即可,此方法应该同样适用其他因cell复用机制
导致的数据显示混乱问题,(关于数组的操作,应考虑到:下拉刷新,上拉加载更多数据以及其他情况,详细代码见工程)
至于实现哪个代理方法最为合理,应该视实际的业务需求以及界面效果而定,下面的textField代理
也是如此
- 在控制器中添加一个记录图片轮播器滚动位置的数组属性
1 2 3 4 |
// scrollView停止滚动后记录contentOffset.x - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { !self.imageViewDidScrolledBlock ? : self.imageViewDidScrolledBlock(scrollView.contentOffset.x); } |
最后图片轮播器添加一个属性接口,接收位置数据
,然后在构造方法里设置图片轮播器的contentOffset
即可
1 2 3 4 5 6 7 8 9 10 11 12 |
- (void)setImageViewCurrentLocation:(CGFloat)imageViewCurrentLocation { _imageViewCurrentLocation = imageViewCurrentLocation; // 恢复显示collectionView滚动的位置 [self.collectionView setContentOffset:CGPointMake(imageViewCurrentLocation, 0)]; // 恢复显示pageLabel的下标 if (!self.pageLabel.hidden && self.imageArray.count) { NSInteger currentIndex = imageViewCurrentLocation / self.collectionView.frame.size.width + 1; self.pageLabel.text = [NSString stringWithFormat:@"%zd/%zd", currentIndex, self.imageArray.count]; } } |
② 三餐
布局
- 如图,整个控制器是
ViewController
,将CollectionView
以及上传button
添加到viewController.view
即可,比较简单 - 导航栏的标题是自定义的view,然后
self.navigationItem.titleView = view;
即可
这个界面的接口号称“时时死”,如果想看效果的童鞋可以自己重新抓包
3. 功能界面
① 菜谱草稿(整个项目最难的界面)
布局
如图所示即可,需要注意的是:因为这个界面是操作本地数据,所以要时刻根据数据得变化判断控件是否显示、如何显示
思路
- 因为是创建以及草稿功能的界面,所以操作的是本地数据,我写了一个
菜谱草稿数据工具类
,用来增删改查,非常方便 - 照片上传:
点击弹出ActionSheet
让用户选择是相机
、还是相册
,然后通过UIImagePickerController
的代理方法- imagePickerController:didFinishPickingMediaWithInfo:
选取照片即可- 因为
顶部
跟做法
都有上传图片的需求,如果不做判断是谁需要设置图片,会导致数据显示错乱,这个在代理方法中通过判断代理的调用者即可解决图片错乱
- 因为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info { // 如果是顶部大图 if (picker == self.headerPicker) { self.createRecipe.image = info[UIImagePickerControllerEditedImage]; } // 如果是步骤图 else if (picker == self.instructPicker) { self.instructionArray[self.setImageIndex].image = info[UIImagePickerControllerEditedImage]; } [self.tableView reloadData]; [picker dismissViewControllerAnimated:YES completion:^{ // 选取完成后更新本地数据 [self updateDarft]; }]; } |
做法步骤:
- 点击
添加步骤
,往tableView对应位置插入一行步骤cell
,并且在模型数据数组对应位置插入一个数据为空的做法数据
,如果不添加的话,点击编辑就会因为数据越界导致崩溃
12345678910// 增加一行点击回调instructionFooter.addInstructionBlock = ^{// 添加一个空的本地数据[weakSelf.instructionArray addObject:[[XCFCreateInstruction alloc] init]];NSInteger row = weakSelf.instructionArray.count - 1;NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:1];// 插入cell[weakSelf.tableView insertRowsAtIndexPaths:@[indexPath]withRowAnimation:UITableViewRowAnimationBottom];};