一个iOS项目总结(二):界面
2016-10-04 17:13
302 查看
作者:代培
地址:http://blog.csdn.net/dp948080952/article/details/52734562
转载请注明出处
这是我项目总结的系列博文中的第二篇,此篇文章中列出了在界面编程过程中遇到的问题和一些心得体会,分享给大家,如果有错误或不妥的地方,欢迎大家在评论中指出!
如果LaunchImage缺失,在iPhone5以后的设备上会出现无法全屏的问题,在应用的上方和下方会有两条黑条,如下方图片。
而且编译时会出现警告:
当然你可以用自己的启动图片来替换这张启动图,或者是使用storyboard,而在我看来使用storyboard应该会更好,storyboard不仅易于适配不同设备,也是苹果所推荐的。
于是到网上搜索了一番,发现了将这二者结合起来的方法:
通过此法,便能得到LaunchScreen的VC,如此问题便简单多了,通过这个VC获取VC的view,用这个view便可以制作动画了,如下图。
然后在这下面两个方法里处理键盘弹起和收回的情况,在
系统发送的通知中,包含了键盘高度的信息,拿到这个高度后,便可以将登录的界面向上移动。
这里我的登录界面是放在一个UIScrollView之上,其实和微信的登录界面有些相似,可以随着你的手指上下滚动,我很同意我一个同事的说法,他说每个界面都应该是一个ScrollView,都应该能够响应用户是手势,就是用户在滑动或是拖动的时候,都应该去响应,我觉得这会让用户觉得很爽,会让人觉得比较灵动。
首先封装一个DPTabBarItem,继承自UIButton,其实想来也简单,tabbar中不同的tab其实就是几个button,此处使用button作为cell十分合适,而且button具有选中和不选中的属性,而且可以为两种状态设置不同的图片,在此处十分适合。
随后便是封装TabBar了
初始化方法只有一个,便是使用DPTabBarItem进行初始化,如果不设置最开始的selectedIndex,则默认为0,此外还有两个属性一个表示被选中的tab,另一个是代理,该代理定义如下:
当选中的Tab变化时,调用此代理方法,代理根据index来调整显示的页面。
这是一个很简单的tableView,这里面大部分cell显示的内容都是固定的东西,所以我们就当这个tableView的数据源都是固定的而不是从网上拉到的数据,所以这一部分内容是针对一个简单的tableView,如何优雅的进行组织。
Model
MVC的设计模式是十分经典的模式,在iOS开发中这种模式也是十分常见,而这个tableView也是使用MVC的设计模式,既然是MVC,首先肯定是要有Model,用于保存展示的数据
此处我的这个Model叫做DPProfileItem,而正常情况之下Model都只是在头文件中进行声明,而.m文件中很少有实现代码,而生成Model的代码却又专门放在一个Manager或dataController类似的单例中,又或是直接将生成的工作放在容纳tableView的VC中,如果需要从网络拉取数据,那么写一个dataController来生成Model倒也是很正常的写法,但是无论如何放在VC中却真是不够优雅。
那么Model生成的代码到底应该放在哪里呢?我觉得最合适的地方应是放在Model的实现文件中,首先Model的生成与Model本身联系十分紧密,放在Model中体现了较高的内聚性,就相当于Model的初始化方法;其次Model的实现文件中本就几无代码,加上这部分代码,显得Model更加饱满而不是仅仅只有头文件的声明;再者这种写法和工厂方法有异曲同工之妙:
tableViewCell
如何优雅的重写UITableViewCell
我们都知道tableView具有复用cell的机制,我们在dataSource的代理中使用此方法
然后根据ReuseIdentifier生成不同的cell。
数字的意义
在代码中经常会出现一些数字常量,而在承载tableView的VC中,数字的常量也是很多的,尤其是在tableView的代理中我们需要根据indexPath去判断section、row。
与其直接使用数字,不如定义一个枚举让数字的意义显得更加明显,除此之外其他的数字数字都可以定义为静态常量
总而言之,尽量不要在代码中直接使用数字常量,让自己的代码具有更好的可读性、维护性。拿上面中的numOfSection举例,如果在代码中多次用到该常量,如果不定义一个静态常量,当你想要修改此值时你便要去代码中各处寻找一一修改,如果你少改了一个地方那么就可能出现某些bug,而如果你定义了一个常量,当你想要修改时,只要修改此处便可,也不用担心bug的出现。
在UITableViewDelegate中
所以如果要根据内容来计算不同cell的高度,便要在此方法返回前计算出每个cell的高度,然而此时cell还未生成,而cell的高度不仅由cell显示的内容决定,还会受cell中的布局影响,所以计算高度的工作不应该由展示tableView的VC来做,而是应该交给cell去做,若是放在VC之中,那么cell和VC的耦合就太高了,一旦cell的布局改变,那么计算高度的方法就要做出改变,而一般如果要是手动计算则会用下面的方法:
但是如果cell布局复杂,那么用这个方法计算高度则要写很多代码,十分复杂,而且很不灵活,那么是否能够根据cell内容自动设定高度呢,答案是有的,在iOS7中UITableView出现了新的功能可以达到此效果:
此属性是用于估算tableView的高度,大致计算出tableView的总高度,而此tableView高度自适应要配合着iOS6中出现的autolayout使用才可以,cell中用autolayout填充满contentView的高度和宽度,然后对tableView进行一定设置,cell的高度就能自动计算,然而这种方式却有一些让人难以忍受的缺点,在tableView滑动的过程中主要是由下往上划的过程中界面会出现抖动甚至跳跃,给人一种十分卡顿的感觉,这种情况的主要原因是因为在滚动的过程中cell的高度由估算的高度变成真实的高度造成的,本身这种新特性的引入就是为了优化性能,然而这种体验还不如不要这优化的性能。
那么是否有更加优雅的方式做到UITableView高度的自适应,经过一番搜索,找到了一个合适的方案:UITableView-FDTemplateLayoutCell至于如何使用大家可以参考这篇文章:优化UITableViewCell高度计算的那些事。
这篇文章中说到此控件具有预缓存机制,而且进行了优化:利用RunLoop空闲时间执行预缓存任务。我也没有看过源码,但至少是在使用的过程中并没有发现这个功能,此控件在tableView的data source的高度方法中即对cell进行内容填充利用autolayout计算高度,进行储存后并返回给代理中的方法,在tableView初始化的过程中即计算出所有cell的高度,后面再需要某个cell的高度时,直接获取缓存中的高度,所以此控件并非对性能做出什么优化,方便的只是高度自适应和cell高度的缓存,避免反复的计算。
我们看到每个cell之间似乎是用弹簧连接,当滑动的时候,离的远的cell会有一定迟钝,给人一种很灵动的感觉,而此效果实现起来并不复杂,只要重写UICollectionViewFlowLayout即可,配合iOS7中出现的UIDynamic特性,下面我们就看具体实现。
首先重写UICollectionViewFlowLayout要重写3个方法:
首先在prepareLayout方法中用自身实例化一个UIDynamicAnimator,然后获取每个cell的attribute,用每个cell初始化一个UIAttachmentBehavior,加入到animator中。
然后将attribute的管理全都交给animator来处理,然而到这里并没有结束,还要再看一段稍长的代码
只要UICollectionView边界发生改变就会调用这个方法,在这个方法中,根据手指滑动的位置和cell的位置进行计算,调整cell中心的位置从而产生灵动的效果。
这个并不是我写的,而是喵神王巍的作品,只是对其进行了分析,这里给出其github地址:VVSpringCollectionViewFlowLayout
地址:http://blog.csdn.net/dp948080952/article/details/52734562
转载请注明出处
写在前面
今年暑假,自己独立完成了一个简单的iOS的APP,是一个bbs的客户端,叫做喻信星空。现在正在测试,准备将其上架app store。但是光做项目不做总结肯定不行,所以这里写篇博客,把项目里遇到的坑都记录下来,所以这篇博客里肯定是有干货的,所以如果你看到了这里,希望你能把它看完,并顶我一下(^-^)这是我项目总结的系列博文中的第二篇,此篇文章中列出了在界面编程过程中遇到的问题和一些心得体会,分享给大家,如果有错误或不妥的地方,欢迎大家在评论中指出!
正文
LaunchImage的缺失问题
对于iOS的应用,有一个特点就是必须要有一张LaunchImage,在工程的设置里可以进行设置,LaunchImage可以有两种选择一个是自己提供图片,另一个是LaunchScreen.storyboard。如果LaunchImage缺失,在iPhone5以后的设备上会出现无法全屏的问题,在应用的上方和下方会有两条黑条,如下方图片。
而且编译时会出现警告:
Missing "Default-568h@2x.png" launch image,当你点击这个警告时,Xcode会提示你是否添加一张启动图片,如果你选择添加,Xcode则会帮你添加一张全黑的640 × 1136的启动图片,而此时再次编译运行,你会发现应用刚启动时是黑屏,进入应用后不能全屏的问题消失了。
当然你可以用自己的启动图片来替换这张启动图,或者是使用storyboard,而在我看来使用storyboard应该会更好,storyboard不仅易于适配不同设备,也是苹果所推荐的。
启动动画
现在的应用大多都有启动动画,当然我也想加在这个客户端中,而iOS的APP又必须要有LaunchImage,那为何不将LaunchImage和启动动画结合在一起,想到这里我十分激动,这样的结合感觉十分完美。于是到网上搜索了一番,发现了将这二者结合起来的方法:
UIViewController *viewController = [[UIStoryboard storyboardWithName:@"LaunchScreen" bundle:nil] instantiateViewControllerWithIdentifier:@"LaunchScreen"];
通过此法,便能得到LaunchScreen的VC,如此问题便简单多了,通过这个VC获取VC的view,用这个view便可以制作动画了,如下图。
登录界面
登录界面看似简单,但也是有玄机在这其中,比如键盘的适配,当点击输入框,键盘便会弹出,此时就有可能遮挡住登录按键或是其他输入框,这个问题该如何解决呢,其实不难,iOS中在键盘状态改变时会发送全局通知,只要注册观察者接收这些通知即可,需要注意的是注册的通知最好是WillShow和WillHiden,这样会显得自然些,不然会有比较严重的延迟:[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeHidden) name:UIKeyboardWillHideNotification object:nil];
然后在这下面两个方法里处理键盘弹起和收回的情况,在
- (void)keyboardWasShown:(NSNotification *)notification { NSDictionary *info = [notification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.f, 0.f, kbSize.height, 0.f); self.backgroundView.contentInset = contentInsets; self.backgroundView.scrollIndicatorInsets = contentInsets; CGRect rect = self.loginButton.frame; rect.size.height += rect.size.height; [self.backgroundView scrollRectToVisible:rect animated:YES]; } - (void)keyboardWillBeHidden { self.backgroundView.contentInset = UIEdgeInsetsZero; self.backgroundView.scrollIndicatorInsets = UIEdgeInsetsZero; }
系统发送的通知中,包含了键盘高度的信息,拿到这个高度后,便可以将登录的界面向上移动。
这里我的登录界面是放在一个UIScrollView之上,其实和微信的登录界面有些相似,可以随着你的手指上下滚动,我很同意我一个同事的说法,他说每个界面都应该是一个ScrollView,都应该能够响应用户是手势,就是用户在滑动或是拖动的时候,都应该去响应,我觉得这会让用户觉得很爽,会让人觉得比较灵动。
封装简单的tabbar
由TabBar和几个VC作为主界面的应用很是常见,当然系统提供了UITabBarController和UITabBar,不过如果想要定制想要的样式的tabbar,用系统的似乎就有些困难了,像tabbar这样并不复杂的控件,自己写一个是很方便的,我想要一个只有图片没有文字的tabbar,于是就自己写了一个DPTabBar。首先封装一个DPTabBarItem,继承自UIButton,其实想来也简单,tabbar中不同的tab其实就是几个button,此处使用button作为cell十分合适,而且button具有选中和不选中的属性,而且可以为两种状态设置不同的图片,在此处十分适合。
@interface DPTabBarItem : UIButton - (instancetype)initWithImage:(UIImage *)image selectedImage:(UIImage *)selectedImage; @end
随后便是封装TabBar了
@class DPTabBarItem; @protocol DPTabBarDelegate; @interface DPTabBar : UIView @property (nonatomic, readwrite, weak) id<DPTabBarDelegate> delegate; @property (nonatomic, readwrite, assign) NSUInteger selectedIndex; - (instancetype)initWithTabBarItems:(NSArray<DPTabBarItem *> *)items; @end
初始化方法只有一个,便是使用DPTabBarItem进行初始化,如果不设置最开始的selectedIndex,则默认为0,此外还有两个属性一个表示被选中的tab,另一个是代理,该代理定义如下:
@protocol DPTabBarDelegate <NSObject> - (void)itemDidSelectAtIndex:(NSUInteger)index; @end
当选中的Tab变化时,调用此代理方法,代理根据index来调整显示的页面。
如何写出一个优雅的tableView
首先上一张图看一下这个tableView:这是一个很简单的tableView,这里面大部分cell显示的内容都是固定的东西,所以我们就当这个tableView的数据源都是固定的而不是从网上拉到的数据,所以这一部分内容是针对一个简单的tableView,如何优雅的进行组织。
Model
MVC的设计模式是十分经典的模式,在iOS开发中这种模式也是十分常见,而这个tableView也是使用MVC的设计模式,既然是MVC,首先肯定是要有Model,用于保存展示的数据
@interface DPProfileItem : NSObject @property (nonatomic, strong) UIImage *userImage; @property (nonatomic, strong) NSString *title1; @property (nonatomic, strong) NSString *title2; @property (nonatomic, assign) NSIndexPath *indexPath; @end
此处我的这个Model叫做DPProfileItem,而正常情况之下Model都只是在头文件中进行声明,而.m文件中很少有实现代码,而生成Model的代码却又专门放在一个Manager或dataController类似的单例中,又或是直接将生成的工作放在容纳tableView的VC中,如果需要从网络拉取数据,那么写一个dataController来生成Model倒也是很正常的写法,但是无论如何放在VC中却真是不够优雅。
那么Model生成的代码到底应该放在哪里呢?我觉得最合适的地方应是放在Model的实现文件中,首先Model的生成与Model本身联系十分紧密,放在Model中体现了较高的内聚性,就相当于Model的初始化方法;其次Model的实现文件中本就几无代码,加上这部分代码,显得Model更加饱满而不是仅仅只有头文件的声明;再者这种写法和工厂方法有异曲同工之妙:
+ (DPProfileItem *)itemWithType:(DPProfileItemType)type;
tableViewCell
如何优雅的重写UITableViewCell
@interface DPProfileCell : UITableViewCell @property (nonatomic, weak) id<DPProfileCellDelegate> delegate; - (void)fillDataWith:(DPProfileItem *)item indexPath:(NSIndexPath *)indexPath; @end
我们都知道tableView具有复用cell的机制,我们在dataSource的代理中使用此方法
dequeueReusableCellWithIdentifier:来获取cell然后然后对其进行数据填充,而如果tableView中如果有不同类型的cell,那是否要定义多个继承自UITableViewCell的cell,其实并不需要,我们只要重写一个方法,再定义几个ReuseIdentifier即可:
@implementation DPProfileCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { if ([reuseIdentifier isEqualToString:DPProfileNormalCellReuseIdentifier]) { self.type = DPProfileCellTypeNormal; } else if ([reuseIdentifier isEqualToString:DPProfileUserCellReuseIdentifier]) { self.type = DPProfileCellTypeUser; } else if ([reuseIdentifier isEqualToString:DPProfileSwitchCellReuseIdentifier]) { self.type = DPProfileCellTypeSwitch; } } return self; } @end
然后根据ReuseIdentifier生成不同的cell。
数字的意义
在代码中经常会出现一些数字常量,而在承载tableView的VC中,数字的常量也是很多的,尤其是在tableView的代理中我们需要根据indexPath去判断section、row。
typedef NS_ENUM(NSUInteger, DPProfileSection) { DPProfileSectionOne = 0, DPProfileSectionTwo = 1, DPProfileSectionThree = 2, DPProfileSectionFour = 3, DPProfileSectionFive = 4 }; typedef NS_ENUM(NSUInteger, DPProfileRow) { DPProfileRowOne = 0, DPProfileRowTwo = 1, DPProfileRowThree = 2, DPProfileRowFour = 3 };
与其直接使用数字,不如定义一个枚举让数字的意义显得更加明显,除此之外其他的数字数字都可以定义为静态常量
static const NSUInteger rowNumOfSectionOne = 1; static const NSUInteger rowNumOfSectionTwo = 1; static const NSUInteger rowNumOfSectionThree = 4; static const NSUInteger rowNumOfSectionFour = 2; static const NSUInteger rowNumOfSectionFive = 1; static const NSUInteger numOfSection = 5;
总而言之,尽量不要在代码中直接使用数字常量,让自己的代码具有更好的可读性、维护性。拿上面中的numOfSection举例,如果在代码中多次用到该常量,如果不定义一个静态常量,当你想要修改此值时你便要去代码中各处寻找一一修改,如果你少改了一个地方那么就可能出现某些bug,而如果你定义了一个常量,当你想要修改时,只要修改此处便可,也不用担心bug的出现。
UITableView自适应高度
UITableView可以算是iOS应用的中最常用的控件了,在我的这个应用中大部分的页面都主要由UITableView组成,对于cell高度固定的UITableView倒是很简单,但是对于高度不定的UITableView却有许多可以优化的地方。在UITableViewDelegate中
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;的方法用于返回tableView中cell的高度,而在tableView初始化时该方法便会调用数次,调用的次数便是UITableViewDataSource中
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;和
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;返回的cell数总和,用于计算tableView的总高度,随后还会调用数次,用于返回将要显示在屏幕上的cell的高度。
所以如果要根据内容来计算不同cell的高度,便要在此方法返回前计算出每个cell的高度,然而此时cell还未生成,而cell的高度不仅由cell显示的内容决定,还会受cell中的布局影响,所以计算高度的工作不应该由展示tableView的VC来做,而是应该交给cell去做,若是放在VC之中,那么cell和VC的耦合就太高了,一旦cell的布局改变,那么计算高度的方法就要做出改变,而一般如果要是手动计算则会用下面的方法:
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary<NSString *, id> *)attributes context:(nullable NSStringDrawingContext *)context NS_AVAILABLE(10_11, 7_0);
但是如果cell布局复杂,那么用这个方法计算高度则要写很多代码,十分复杂,而且很不灵活,那么是否能够根据cell内容自动设定高度呢,答案是有的,在iOS7中UITableView出现了新的功能可以达到此效果:
estimatedRowHeight。
此属性是用于估算tableView的高度,大致计算出tableView的总高度,而此tableView高度自适应要配合着iOS6中出现的autolayout使用才可以,cell中用autolayout填充满contentView的高度和宽度,然后对tableView进行一定设置,cell的高度就能自动计算,然而这种方式却有一些让人难以忍受的缺点,在tableView滑动的过程中主要是由下往上划的过程中界面会出现抖动甚至跳跃,给人一种十分卡顿的感觉,这种情况的主要原因是因为在滚动的过程中cell的高度由估算的高度变成真实的高度造成的,本身这种新特性的引入就是为了优化性能,然而这种体验还不如不要这优化的性能。
那么是否有更加优雅的方式做到UITableView高度的自适应,经过一番搜索,找到了一个合适的方案:UITableView-FDTemplateLayoutCell至于如何使用大家可以参考这篇文章:优化UITableViewCell高度计算的那些事。
这篇文章中说到此控件具有预缓存机制,而且进行了优化:利用RunLoop空闲时间执行预缓存任务。我也没有看过源码,但至少是在使用的过程中并没有发现这个功能,此控件在tableView的data source的高度方法中即对cell进行内容填充利用autolayout计算高度,进行储存后并返回给代理中的方法,在tableView初始化的过程中即计算出所有cell的高度,后面再需要某个cell的高度时,直接获取缓存中的高度,所以此控件并非对性能做出什么优化,方便的只是高度自适应和cell高度的缓存,避免反复的计算。
模仿苹果信息应用灵动效果
首先看一下效果:我们看到每个cell之间似乎是用弹簧连接,当滑动的时候,离的远的cell会有一定迟钝,给人一种很灵动的感觉,而此效果实现起来并不复杂,只要重写UICollectionViewFlowLayout即可,配合iOS7中出现的UIDynamic特性,下面我们就看具体实现。
首先重写UICollectionViewFlowLayout要重写3个方法:
- (void)prepareLayout; - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
首先在prepareLayout方法中用自身实例化一个UIDynamicAnimator,然后获取每个cell的attribute,用每个cell初始化一个UIAttachmentBehavior,加入到animator中。
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { return [_animator itemsInRect:rect]; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { return [_animator layoutAttributesForCellAtIndexPath:indexPath]; }
然后将attribute的管理全都交给animator来处理,然而到这里并没有结束,还要再看一段稍长的代码
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { UIScrollView *scrollView = self.collectionView; CGFloat scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y; CGPoint touchLocation = [scrollView.panGestureRecognizer locationInView:scrollView]; for (UIAttachmentBehavior *spring in _animator.behaviors) { CGPoint anchorPoint = spring.anchorPoint; CGFloat distanceFromTouch = fabs(touchLocation.y - anchorPoint.y); CGFloat scrollResistance = distanceFromTouch / self.resistanceFactor; UICollectionViewLayoutAttributes *item = (UICollectionViewLayoutAttributes *)[spring.items firstObject]; CGPoint center = item.center; center.y += (scrollDelta > 0) ? MIN(scrollDelta, scrollDelta * scrollResistance) : MAX(scrollDelta, scrollDelta * scrollResistance); item.center = center; [_animator updateItemUsingCurrentState:item]; } return NO; }
只要UICollectionView边界发生改变就会调用这个方法,在这个方法中,根据手指滑动的位置和cell的位置进行计算,调整cell中心的位置从而产生灵动的效果。
这个并不是我写的,而是喵神王巍的作品,只是对其进行了分析,这里给出其github地址:VVSpringCollectionViewFlowLayout
结语
界面部分的总结大致就是这样,如果你能看到这里,我想真心的对你说一声感谢,能够看完我上面的”胡言乱语”,最后再次复习一下此项目的github地址:喻信星空相关文章推荐
- 如何架构一个ios项目 个人经验总结
- 如何架构一个ios项目 个人经验总结
- iOS 自己项目中的一些总结(tableview 避免重用以及多选状态下判断数组中的bool值是不是同一个状态以及自定义控件的原因)
- iOS 简单计算文件Cache的大小(项目中用了IASKAppSettingsViewController,一个设置界面的库)
- 一个iOS项目总结(一):网络接口的封装
- 如何架构一个ios项目 个人经验总结
- 一个普通 iOS 码农的几个小项目相关知识点总结
- 如何架构一个ios项目 个人经验总结
- 如何架构一个ios项目 个人经验总结
- 【IOS开发】一个壁纸类的项目总结。
- 一个收集了502款开源iOS应用的开源项目
- 总结使人进步,可视化界面GUI应用开发总结:Android、iOS、Web、Swing、Windows开发等
- iOS开发——完整项目实战Swift篇&百思不得姐Swift版总结(三)
- iOS开发UI篇—使用xib自定义UItableviewcell实现一个简单的团购应用界面布局
- 同一个项目的不同的项目工作经验总结--程序员甲
- 献给初学iOS的小盆友们——微博app项目开发之五新特性界面
- 同一个项目的不同的项目工作经验总结--程序员乙
- 2014项目总结:一个比较成功的项目总结
- ios小项目——新浪微博客户端总结
- MVVM项目实战之路-搭建一个登录界面