Maka上周三发布了iOS客户端1.0。于是做了竞品分析与模仿实现。
工具:iPhone,MindNode。
以下是主要的功能结构分解。从功能与模块来看,实现比较完整,工作量分布也比较平衡。非重要模块都采用最简单的实现方式。
工具:Charles Proxy
以下是主要的API。
模块 | 名称 | URL | Method | 说明 |
用户 | 注册 | /app/user/register | POST | http://api.maka.im |
登陆 | /app/user/login | POST | ||
忘记密码 | /app/user/forgetpassword | POST | ||
用户信息 | /app/user/{$user_id} | GET | ||
修改用户信息 | /app/user/{$user_id} | PUT | ||
事件 | 我的事件列表 | /app/events | GET | |
创建 | /app/event | POST | ||
更新 | /app/event/{$event_id} | PUT | ||
发布 | /app/event/{$event_id} | POST | ||
创作 | 主分类 | /app/specialCategories | GET | |
模板分类 | /app/templates | GET | ||
模板页 | /app/template/{$template_id} | GET | ||
图集分类 | /app/pictureIndex | GET | ||
图片列表 | /app/pictures | GET | ||
热门 | 公开事件 | /app/publicEvents | GET | |
分类 | /app/tagCategories | GET |
通过数据分析,与客户端通信的主要服务器有
4.1 iOS架构
按照功能模块划分。
4.2 工程结构
二层设计 + 按模块划分 + MVVM
4.3 API层设计
使用网络库:AFNetworking-RACExtensions。
使用单件模式 + OC扩展的方式实现接口的模块划分。采用响应式编程方式。显式参数+信号返回。
用户模块接口定义
1 @interface MKAPIClient (User) 2 3 /** 4 * 用户注册 5 * 6 * @param email 邮箱 7 * @param password 密码 8 * 9 * @return 信号 10 */ 11 - (RACSignal *)registWithEmail:(NSString *)email password:(NSString *)password; 12 13 14 /** 15 * 用户登陆 16 * 17 * @param email 邮箱 18 * @param password 密码 19 * 20 * @return 信号 21 */ 22 - (RACSignal *)loginWithEmail:(NSString *)email password:(NSString *)password; 23 24 25 /** 26 * 忘记密码 27 * 28 * @param email 邮箱 29 * 30 * @return 信号 31 */ 32 - (RACSignal *)forgetPasswordWithEmail:(NSString *)email; 33 34 35 /** 36 * 用户信息 37 * 38 * @return 信号 39 */ 40 - (RACSignal *)userInfo; 41 42 /** 43 * 修改用户信息 44 * 45 * @param key 字段 46 * @param value 值 47 * 48 * @return 信号 49 */ 50 - (RACSignal *)updateUserInfoWithKey:(NSString *)key value:(NSString *)value; 51 52 @end
eg. 登陆接口实现
1 /** 2 * 用户登陆 3 * 4 * @param email 邮箱 5 * @param password 密码 6 * 7 * @return 信号 8 */ 9 - (RACSignal *)loginWithEmail:(NSString *)email password:(NSString *)password { 10 NSParameterAssert(email); 11 NSParameterAssert(password); 12 13 NSDictionary *params = @{@"email" : email, @"password" : password}; 14 15 @weakify(self); 16 return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 17 @strongify(self); 18 return [[self.client rac_POST:@"/app/user/login" parameters:params] subscribeNext:^(RACTuple *x) { 19 NSDictionary *result = x.first; 20 21 @try { 22 if ([result[@"code"] intValue] == 200) { 23 NSDictionary *data = result[@"data"]; 24 self.uid = [data[@"uid"] intValue]; 25 self.token = data[@"token"]; 26 27 [subscriber sendNext:data]; 28 } else { 29 NSError *err = [NSError errorWithDomain:MKAPIClientErrorDomain code:[result[@"code"] intValue] userInfo:nil]; 30 [subscriber sendError:err]; 31 } 32 } @catch (NSException *exception) { 33 NSError *err = [NSError errorWithDomain:MKAPIClientErrorDomain code:10001 userInfo:nil]; 34 [subscriber sendError:err]; 35 } 36 } error:^(NSError *error) { 37 [subscriber sendError:error]; 38 } completed:^{ 39 [subscriber sendCompleted]; 40 }]; 41 }] setNameWithFormat:@"%s", __FUNCTION__]; 42 }
4.3 业务逻辑层
这层还没想好怎么做比较好。暂时使用MVVM的VM来替代业务逻辑层。
4.4 UI层实现
主要采用MVVM模式,简单界面还是使用MVC实现。
说明:
1. 下图中的MKPublicEventItem为MKPublicEventCell的属性,不是Domain。参考:UINavigationItem设计。
2. Domain与Item关系。Item为PL层数据。
说明:MKItem为所有表现层数据的基类,提供与Domain映射的基本功能。 参考Three20的Item设计和UIView tag值设计。
1 @interface MKItem : NSObject 2 3 @property(nonatomic, weak)NSObject *weakRef; 4 @property(nonatomic, strong)NSObject *ref; 5 @property(nonatomic, strong)NSIndexPath *indexPath; 6 @property(nonatomic, assign)int tag; 7 8 @end
XXXItem只提供UI显示的数据。属于贫血模型。
1 @interface MKPublicEventItem : MKItem 2 3 @property(nonatomic, copy)NSString *title; 4 @property(nonatomic, copy)NSString *cover; 5 @property(nonatomic, copy)NSString *username; 6 @property(nonatomic, copy)NSString *publishTime; 7 8 @end
MKPublicEventItem+Event。该扩展用于从Domain创建Item方法。功能与reformer相同。参考: iOS应用架构谈 网络层设计方案
1 @implementation MKPublicEventItem (Event) 2 3 4 + (instancetype)itemWithDictionary:(NSDictionary *)event { 5 MKPublicEventItem *item = [[MKPublicEventItem alloc] init]; 6 item.title = event[@"title"]; 7 item.cover = event[@"firstImgUrl"]; 8 item.username = event[@"author"]; 9 item.publishTime = event[@"publishTime"]; 10 item.ref = event; 11 12 return item; 13 } 14 15 - (NSString *)eventId { 16 return [(NSDictionary *)self.ref objectForKey:@"id"]; 17 } 18 19 @end
MKPublicEventCell
说明:
1. 属性使用lazy load方式创建。
1 @interface MKPublicEventCell : UICollectionViewCell 2 3 4 @property(nonatomic, strong)MKPublicEventItem *item; 5 6 + (float)cellHeightWithWidth:(float)width; 7 8 @end 9 10 11 @interface MKPublicEventCell () 12 13 @property(nonatomic, strong)UIImageView *imageView; 14 @property(nonatomic, strong)MKPublicEventToolbar *toolbar; 15 16 @end 17 18 @implementation MKPublicEventCell 19 20 + (float)cellHeightWithWidth:(float)width { 21 return width * 504/320 + [MKPublicEventToolbar toolbarHeight]; 22 } 23 24 - (instancetype)initWithFrame:(CGRect)frame { 25 if (self = [super initWithFrame:frame]) { 26 [self setup]; 27 } 28 29 return self; 30 } 31 32 - (UIImageView *)imageView { 33 if (!_imageView) { 34 UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectZero]; 35 imageView.backgroundColor = [UIColor randomLightColor]; 36 imageView.contentMode = UIViewContentModeScaleAspectFill; 37 imageView.clipsToBounds = YES; 38 _imageView = imageView; 39 } 40 41 return _imageView; 42 } 43 44 - (MKPublicEventToolbar *)toolbar { 45 if (!_toolbar) { 46 MKPublicEventToolbar *toolbar = [[MKPublicEventToolbar alloc] initWithFrame:CGRectZero]; 47 _toolbar = toolbar; 48 } 49 50 return _toolbar; 51 } 52 53 - (void)setup { 54 [self.contentView addSubview:self.imageView]; 55 [self.contentView addSubview:self.toolbar]; 56 } 57 58 - (void)layoutSubviews { 59 [super layoutSubviews]; 60 // h'/w' = h/w 61 self.imageView.frame = CGRectMake(0, 0, self.bounds.size.width, [MKPublicEventCell cellHeightWithWidth:self.bounds.size.width] - [MKPublicEventToolbar toolbarHeight]); 62 self.toolbar.frame = CGRectMake(0, self.imageView.bounds.size.height, self.bounds.size.width, [MKPublicEventToolbar toolbarHeight]); 63 } 64 65 - (void)setItem:(MKPublicEventItem *)item { 66 _item = item; 67 68 [self.imageView sd_setImageWithURL:[NSURL URLWithString:item.cover] placeholderImage:nil]; 69 self.toolbar.usernameLabel.text = item.username; 70 self.toolbar.titleLabel.text = item.title; 71 self.toolbar.dateLabel.text = item.publishTime; 72 } 73 74 @end
使用Specta + Expecta+ReactiveCocoa
1 SpecBegin(User) 2 3 describe(@"用户", ^{ 4 5 __block MKAPIClient *client; 6 beforeAll(^{ 7 client = [MKAPIClient defaultClient]; 8 }); 9 10 beforeEach(^{ 11 12 }); 13 14 context(@"当登陆", ^{ 15 it(@"应该成功", ^{ 16 RACSignal *signal = [client loginWithEmail:@"test@test.com" password:@"password"]; 17 expect(signal).will.complete(); 18 }); 19 }); 20 21 afterEach(^{ 22 23 }); 24 25 afterAll(^{ 26 27 }); 28 }); 29 30 SpecEnd
周末花了2天时间做分析并且实现。
1. API层对接完毕。
2. 基础框架搭建完毕。
3. 实现热门基本UI。
由于时间比较仓促。还没有对创作模块做详细分析。后续在补上。