极客班iOS应用开发实战测试题二(8月31日)

习题:同学录

项目源码见 GitHub

v1.5以后使用 CocoaPods,.gitignore包含/pod,下载后请 pod install

1. 题目要求:

  • 实现一个同学录列表,包含姓名、简介和头像等信息。
  • 可以添加新的同学,支持拍照添加头像。
  • 可以查看每一个的同学详情页。
  • 实现存储所有同学的信息。

Classmates

2. 实际开发遇到的点:

1
2
3
// Updating Objects with primary keys
// This will make properties no settled be NULL
[GWClassmate createOrUpdateInDefaultRealmWithValue:newClassmate];
  • 两种编辑保存界面: 微信进入编辑的界面,Back 有个Alert确认保存; Phone.app 则是禁用右滑和 Back,显示 Cancel & Done,也是完善的方案。
    Edit View

  • Estimating default values for properties of Classmate

    • In Domain Logic: ClassmateDB.h/m (BEST Solution)
    • In Model: Classmate.h/m, Specify default values. BUT Realm isn’t support nil property.
    • In View Controller: if/esle
    • In Interface Builder: Setting a default value. Especially giving a default avatar is a nice solution for user.

    RLMPropertyType Constants Reference

  • “To use AVAsset to extract metadata informations, this post is useful.” http://stackoverflow.com/questions/16318821/extracting-mp3-album-artwork-in-ios

  • Playing video in iOS

  • Use Clang-Format -> LLVM (default)

3. 项目完成情况:

完成1.0:

  • 基本功能完成
  • 列表使用了缩略图,待优化直接保存数据库
  • 输入键盘时,View 自动升降一个键盘高度,以保持可见
  • 保存是检验用户名,不能为空
  • 使用 Realm 数据库
  • classmate.h/m 为 Model,四个属性分别为:uuid、name、info & avator
  • Using GWDetailViewController as super class to GWShowDetailViewController can easy to create a new view.

2015-08-31 20:17:00

完成1.1:

  • 使用 FMDB

把习题同学录的存储 Realm 换成 FMDB,午饭到16:00还没吃,我的学习新的知识的能力还比较低的。需要投入更多时间来学习了。

2015-09-01 16:09:00

完成1.2:

  • 换回 Realm
  • 增加编辑和删除功能

2015-09-01 20:00:00

完成1.3:

  • 随机添加修改和删除10000个classmate,以测试性能
  • Add a tab bar view as root view
  • UI layout like Wechat
  • Set UUID as parimary key
  • Updating Objects With Primary Keys
  • 把取消按钮改为 Button,并把 Segue to GWDetailViewController & GWShowDetailViewController, 改为 Modle
  • New a manaager class: GWClassmateDB, to abstract most useful SUQD methods and testing methods.

2015-09-02 18:20:47

完成1.4:

  • Change cell’s Accessory form Detail to Disclosure Indicator
  • Specify default values for properties of Classmate.h/m
  • Add Music Tab
  • Add a “red dot badge” on avatars’ view for test.
  • Change the classmates display style. Just looks like WeChat.app.

2015-09-06 11:43:19

Classmates v1.4

进行中1.5:

  • Adapting to MVVM
  • XCTest
  • Add a ‘Edit’ Button, and after taped the ‘back’ button in navigation change to “cancel”, add a “Done” in right. Just looks like Phone.app.
  • Add Sort by name, and index by initial
  • Add Search

4. BUG

以下的代码提示越界错误:Terminating app due to uncaught exception ‘RLMException’, reason: ‘Index is out of bounds.’可能的原因:

需要注意的是,写入操作会相互阻塞,而且其相对应的进程也会受到影响。这和其他的永久数据存储解决方案是一样的,所以我们建议你使用常用的,也是最有效的方案, 将所有写入放到一个单独的进程中。https://realm.io/cn/docs/objc/latest/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:@"showDetail"]) {

NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
GWCustomTableViewCell *cell = (GWCustomTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];

GWDetailViewController *vc = (GWDetailViewController *)segue.destinationViewController;

// vc.userid = cell.userid;
vc.name.text = cell.name.text;

}
}

最终解决方法是修改 Custom Cell 中添加一个 UILabel 的 text 来存储 UUID,并设置为隐藏。并添加使用 Detail Disclosure 测试 NSLog UUID。具体的越界问题,还需要在研究。

5. Further Reading

按照@杨武老师的学习和复习方法,我整理的 iOS Map。

回家从头开始做了这个 Map,确实对梳理知识点和复习记忆有很多的帮助。其中 Blocks、Network & Persistence 都还有待学习加强的点。以后学习新的还需要更新。

iOS Map

@杨武老师全天辅导

1. 学习,不仅要学,还要习:

  • 作业,不仅仅是作业。有信心拿给未来的面试官看不?

  • 作业是一个机会,做完基本功能后,琢磨着如何完善优化。要求自己的代码容易别人看懂。看我做的多牛逼,风格好。

  • 学习要复习,主动回忆+分散重复。尤其凭空去做。

    被动学习有个线索如书和 Demo,才有思路。只有做练习时,才能主动回忆知识。回忆方法:Mind Map

2. 课堂上共同整理了一份 iOS Map,涉及到的一些知识点:

  • NARC (new alloc retain copy)
  • ARC 不是垃圾收集,只是一个语法糖
  • Xcode Preprocess, Assembly and Disassembly
  • LLDB: help
  • 调试
  • IB: Outlets 直接访问 UI
  • 设计师设计一个功能的难度和程序员实现的难度
  • 常用 UI 文档介绍,追根究底
  • 单元测试等

3. 习题很重要:

  • 想学好,每天都要投入2个小时到开发中
  • 刷题提高技术

段老师的视频教程,采用都是比较通用是 Demo,方便以后代码复用。也是从项目实践经验来教授相关知识如 FMDB,这几节课获益匪浅。

Demo 源码见 Github,并添加了相关注释。

28. 沙盒机制

介绍几种数据持久化的方案,选择合适项目的方案:

  • 沙盒机制
  • NSFileManager
  • Plist、Archive数据归档 * SQLite3应用
  • NSUserDefaults

沙盒目录结构 NSDocumentDirectory

  • Application Bundle/
  • Documents/
  • Library/Caches/
  • Library/Perferences/
  • tmp/

29. NSFileManager & Plist

存取文档的方法封装一个通用的类BLUtility.h/m,方便调用。以下代码是测试保存文本文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//
// BLUtility.m

/**
* 创建目录
*
* @param dirPath 待创建的目录
* @param isDir 结尾是否是目录
*
* @return 是否创建成功
*/
+ (NSString*) getPathWithinDocumentDir:(NSString*)aPath {
NSString *fullPath = nil;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

if ([paths count] > 0) {
fullPath = (NSString *)[paths objectAtIndex:0];
if ([aPath length] > 0) {
fullPath = [fullPath stringByAppendingPathComponent:aPath];
}
}
return fullPath;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// BLArchiveViewController.m

/**
* 测试保存文档和 plist 的按钮关联的 Action 方法
*
* @param sender <#sender description#>
*/
- (void)writeFileButtonClicked:(id)sender {
// 1 - 测试字符串文档,保存到 Documents
// 1.1 - 生成并保存
NSString *string = @"ABC副经理";
NSString *stringFilePath = [BLUtility getPathWithinDocumentDir:@"string.text"];
[BLUtility createDirectory:stringFilePath lastComponentIsDirectory:NO];
[string writeToFile:stringFilePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

30. Archive 数据归档和 NSUserDefaults

转化为 NSData,二进制的数据,利于存储和传输。

遵循 NSCoding 协议,即可实现编码和解码的类,进一步对其归档和解档。也用到了封装的类BLUtility.h/m。本例中文档保存到 NSUserDefaults:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//
// BLUser.m

/**
* 解码
*
* @param aDecoder 编码类型
*
* @return 解码得到的对象
*/
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
self.userName = [aDecoder decodeObjectForKey:@"userName"];
self.email = [aDecoder decodeObjectForKey:@"email"];
self.password = [aDecoder decodeObjectForKey:@"password"];
self.age = [aDecoder decodeIntForKey:@"age"];
}
return self;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//
// BLUtility.m

/**
* 解档
*
* @param archivedData 待解档的 NSData
* @param key key
*
* @return 解档完成的对象
*/
+ (NSObject *) unarchiverObject:(NSData *)archivedData withKey:(NSString *)key {
if(archivedData == nil) {
return nil;
}

NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:archivedData];
NSObject *object = [unarchiver decodeObjectForKey:key];
[unarchiver finishDecoding];
return object;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// BLArchiveViewController.m

/**
* 读取按钮关联的 Action 方法
*
* @param sender <#sender description#>
*/
- (void) readButtonClicked:(id)sender {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSInteger test = [userDefaults integerForKey:@"integer"];
NSLog(@"test = %li", test);

NSString *userDataPath = [BLUtility getPathWithinDocumentDir:UserDataName];
if ([userDataPath length] > 0 && [[NSFileManager defaultManager] fileExistsAtPath:userDataPath]) {
NSData *userData = [NSData dataWithContentsOfFile:userDataPath];
BLUser *user = (BLUser *)[BLUtility unarchiverObject:userData withKey:@"UserData"];
_userNameTextField.text = user.userName;
_emailTextField.text = user.email;
_passwordTextField.text = user.password;
_ageTextField.text = [NSString stringWithFormat:@"%ld", user.age];
}
}

###延伸阅读:

31. SQLite3

通过 FMDB 封装,得以 OBjective-C 访问 SQLite,完成常用增删改查。从语法和数据体积实现轻量级存储方案。这个方案稍后用在习题“同学录”上实践一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//
// BLPoemDB.h


#import <Foundation/Foundation.h>
#import "FMDatabase.h"
#import "BLPoem.h"

@interface BLPoemDB : NSObject {
/**
* 创建一个 FMDatabase 实例
*/
FMDatabase *_db;
}

/**
* 创建一个表
*
* @return 是否创建成功
*/
- (BOOL) createPoemTable;

/**
* 增加一首诗到表
*
* @param poem 待添加的诗
*
* @return 是否添加成功
*/
- (BOOL) addPoem:(BLPoem *)poem;

/**
* 查询所有诗
*
* @return 查询到的包含所有诗的数组
*/
- (NSMutableArray *) getAllPoems;

/**
* 查询被收藏的诗
*
* @return 查询到的包含所有收藏的诗的数组
*/
- (NSMutableArray *) getFavoritesPoems;

/**
* 更改是非收藏这一首诗
*
* @param favorite 是否收藏
* @param poemId 待更改的诗的 ID
*
* @return 是否更改收藏成功
*/
- (BOOL) setFavorite:(BOOL)favorite favoriteId:(NSInteger)poemId;

/**
* 删除指定 ID 的一首诗
*
* @param poemId 待删除的诗的 ID
*
* @return 是否删除成功
*/
- (BOOL) deletePoemWithPoemId:(NSInteger)poemId;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//
// BLPoemDB.m
// BLDataSaveDemo


#import "BLPoemDB.h"
#import "BLUtility.h"

#define BLPOEMDBNAME @"BLPoemDB.sqlite"

@implementation BLPoemDB

/**
* 初始化数据库 FMDatabase 实例
*
* @return 初始化得到的 FMDatabase 实例
*/
-(id) init {
self = [super init];
if (self) {
NSString *dbPath = [BLUtility getPathWithinDocumentDir:BLPOEMDBNAME];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL existFile = [fileManager fileExistsAtPath:dbPath];
if (existFile == NO) {
NSString *poemDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:BLPOEMDBNAME];
[fileManager copyItemAtPath:poemDBPath toPath:dbPath error:nil];
}
_db = [[FMDatabase alloc] initWithPath:dbPath];
if ([_db open] == NO) {
return nil;
}
}
return self;
}


- (BOOL) createPoemTable {
[_db beginTransaction];
BOOL success = [_db executeUpdate:@"CREATE TABLE IF NOT EXISTS POEMTABLE ("
@"ID INTEGER PRIMARY KEY NOT NULL,"
@"POEMNAME TEXT NOT NULL,"
@"POETNAME TEXT NOT NULL,"
@"POEMCONTENT TEXT NOT NULL,"
@"WHETHERFAVORITE BOOL NOT NULL);"];
[_db commit];

if(!success || [_db hadError]) {
[_db rollback];
return NO;
}
return YES;
}

- (BOOL) addPoem:(BLPoem *)poem {
FMResultSet *rs = [_db executeQuery:@"SELECT ID FROM POEMTABLE WHERE ID=?", [NSNumber numberWithInteger:poem.poemId]];
if(rs && [rs next]) {
[rs close];
return YES;
}
[rs close];

[_db beginTransaction];
NSString *bookMark = nil;
BOOL success = [_db executeUpdate:@"INSERT OR IGNORE INTO POEMTABLE (ID,POEMNAME,POETNAME,POEMCONTENT,WHETHERFAVORITE) VALUES (?,?,?,?,?);",
[NSNumber numberWithInteger:poem.poemId],
poem.poemName,
poem.poetName,
poem.poemContent,
[NSNumber numberWithBool:poem.whetherFavorite],
bookMark];
[_db commit];

if(!success || [_db hadError]) {
[_db rollback];
return NO;
}
return YES;
}

- (NSMutableArray *) getAllPoems {
NSMutableArray *result = [[NSMutableArray alloc] init];
// FMResultSet *rs = [_db executeQuery:@"SELECT * FROM POEMTABLE WHERE ID=? AND WHETHERFAVORITE=?", [NSNumber numberWithInt:poem.poemId], [NSNumber numberWithBool:poem.whetherFavorite]];
FMResultSet *rs = [_db executeQuery:@"SELECT * FROM POEMTABLE"];
while([rs next]) {
BLPoem *poem = [[BLPoem alloc] init];
poem.poemId = [rs intForColumn:@"ID"];
poem.poemName = [rs stringForColumn:@"POEMNAME"];
poem.poetName = [rs stringForColumn:@"POETNAME"];
poem.poemContent = [rs stringForColumn:@"POEMCONTENT"];
poem.whetherFavorite = [rs boolForColumn:@"WHETHERFAVORITE"];
[result addObject:poem];
}
[rs close];
return result;
}

- (NSMutableArray *) getFavoritesPoems {
NSMutableArray *result = [[NSMutableArray alloc] init];
FMResultSet *rs = [_db executeQuery:@"SELECT * FROM POEMTABLE WHERE WHETHERFAVORITE=?", [NSNumber numberWithBool:YES]];
while([rs next]) {
BLPoem *poem = [[BLPoem alloc] init];
poem.poemId = [rs intForColumn:@"ID"];
poem.poemName = [rs stringForColumn:@"POEMNAME"];
poem.poetName = [rs stringForColumn:@"POETNAME"];
poem.poemContent = [rs stringForColumn:@"POEMCONTENT"];
poem.whetherFavorite = [rs boolForColumn:@"WHETHERFAVORITE"];
[result addObject:poem];
}
[rs close];
return result;
}

- (BOOL) setFavorite:(BOOL)favorite favoriteId:(NSInteger)poemId {
if (_db == nil) {
return NO;
}
[_db beginTransaction];
BOOL result = [_db executeUpdate:@"UPDATE POEMTABLE SET WHETHERFAVORITE=? WHERE ID=?", [NSNumber numberWithBool:favorite], [NSNumber numberWithInteger:poemId]];
[_db commit];
return result;
}

- (BOOL) deletePoemWithPoemId:(NSInteger)poemId {
[_db beginTransaction];
BOOL success = [_db executeUpdate:@"DELETE FROM POEMTABLE WHERE ID=?", [NSNumber numberWithInteger:poemId]];
[_db commit];
if(!success || [_db hadError]) {
[_db rollback];
return NO;
}
return YES;
}

- (void) dealloc {
[_db close];
_db = nil;
}

@end

延伸阅读:

32. 多线程和多媒体

简单介绍了常见多媒体的播放:

  • UIImagePickerViewController
  • UIActionSheet (👈名字谐音)
  • AVAudioPlayer
  • SystenSounID
  • MPMoviePlayerViewController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
#import <MediaPlayer/MediaPlayer.h>

@interface BLOneViewController ()<UIAlertViewDelegate, UIActionSheetDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate> {
AVAudioPlayer *audioPlayer;
SystemSoundID systemSoundId;
}


#pragma mark - Play Music button action

- (IBAction)palyMusic:(UIButton *)sender {



if (!audioPlayer) {
NSString *mp3Path = [[NSBundle mainBundle] pathForResource:@"yishengsuoai" ofType:@"mp3"];
NSURL *mp3Url = [[NSURL alloc] initFileURLWithPath:mp3Path];
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:mp3Url error:NULL];
audioPlayer.numberOfLoops = -1;
[audioPlayer prepareToPlay];
}
if ([audioPlayer isPlaying]) {
[audioPlayer stop];
sender.titleLabel.text = @"Play Music";

}else {
[audioPlayer play];
sender.titleLabel.text = @"Pasue Music";

}
}

33. 硬件访问、CALayer & Core Graphics

33.1. gravity and user acceleration

Useage: shaking device to feedback or 360° image

1
2
3
4
5
6
7
8
9
10
11
12
13
// 4 - Start update divice motion and print them
// The accelerometer measures the sum of two acceleration vectors: gravity and user acceleration.
NSOperationQueue *diviceMotionQueue = [[NSOperationQueue alloc] init];
[self.motionManager startDeviceMotionUpdatesToQueue:diviceMotionQueue
withHandler:^(CMDeviceMotion *motion, NSError *error) {

NSLog(@"\n \n-------------------------------------------------------------------------->\n \n 1 - attitude: %@ \n 2 - rotation rate x: %f, y: %f, z: %f,\n \n 3 - gravity x: %f, y: %f, z: %f \n \n 4 - user acceleration x: %f, y: %f, z: %f \n \n 5 - magnetic field accuracy: %d, x: %f, y: %f, z: %f \n \n <--------------------------------------------------------------------------\n \n" ,
motion.attitude,
motion.rotationRate.x, motion.rotationRate.y, motion.rotationRate.z,
motion.gravity.x, motion.gravity.y, motion.gravity.z,
motion.userAcceleration.x, motion.userAcceleration.y, motion.userAcceleration.z,
motion.magneticField.accuracy, motion.magneticField.field.x, motion.magneticField.field.y, motion.magneticField.field.z);
}];

Search the definiton of each new word, really 😄😄😄.

Do you believe that, At least I don’t think so.

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
-------------------------------------------------------------------------->

1 - attitude: CMAttitude Pitch: 0.751292, Roll: 0.555726, Yaw: 22.198534

2 - rotation rate x: 0.001511, y: -0.000795, z: 0.002454,

3 - gravity x: 0.009698, y: -0.013112, z: -0.999867

4 - user acceleration x: -0.002603, y: 0.000783, z: -0.014095

5 - magnetic field accuracy: -1, x: 0.000000, y: 0.000000, z: 0.000000

<--------------------------------------------------------------------------

33.2. CALayer

Modifying the Layer’s Appearance Properties.
Lots of properties really similar to Sketch.

33.3. Core Graphics

CGContextRef: An opaque type that represents a Quartz 2D drawing environment.

Path on CGContextRef vs Vector on Artboard in Sketch, by learn Sketch really make sense of those abstract concepts.

34. 自定义手势、Block & GCD

34.1. 手势

尽量设计使用常用的手势

设计一个小 Demo,实现:Shake Your iPhone To Send Feedback Email To Me😄

34.2. Block

比较简洁的函数,就在当前就可以调用。

34.3. GCD

Group queue:并行执行多任务比较牛。

35. APNS、Core Date、URL Scheme、单元测试、APP发布流

35.1. APNS

Remote Notification

1
2
3
4
// 1 - Register Remote Notification
// 2 - Register deviceToken
// 3 - Receive Remote Notification
// 4 - Update Icon Badge Number

稍后添加到 Classmates

35.2. Core Data

核心概念:

1
2
3
4
5
6
7
8
// 程序员和管理对象模式之间的桥梁,做数据库的增删改查等操作
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;

// 数据库里的不同管理对象类型
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;

// 数据库文件和程序之间的联系桥梁
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;

稍后把 Classmate 整个 Core Data 的版本

35.3. URL Scheme

就是一个通过个地址访问系统软件或第三方软件,如地图和 Launch Center Pro。iOS 9之后估计有更多可能性。

35.4. 单元测试或断点测试

35.5. App 发布流程

设计模式课程给我的感受是:理清各种平时固定用的 API 的背后的原理或思路,尤其是写代码时候反复去审视开发框架和思路都有一定的帮助。

课件下载:https://github.com/gewill/GeekBand-iOS-Demo/tree/master/Design%20Patterns

11. 层次结构

动机:

  • 对象之间关系
  • 允许一组相互协作的对象当成单⼀对象处理
  • 无需⼦类化,实现⾃定义
  • 降低 parents class 复杂度
  • 使用 tree 结构,⽅便数据的存储,操作和搜索

Layers associated with a window

View Hierarchy in Xcode

其实还有更强大的工具:Reveal,附使用方法

节选自UIView.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface UIView(UIViewHierarchy)

@property(nonatomic,readonly) UIView *superview;
@property(nonatomic,readonly,copy) NSArray *subviews;
@property(nonatomic,readonly) UIWindow *window;

- (void)removeFromSuperview;
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index;
- (void)exchangeSubviewAtIndex:(NSInteger)index1 withSubviewAtIndex:(NSInteger)index2;

- (void)addSubview:(UIView *)view;
- (void)insertSubview:(UIView *)view belowSubview:(UIView *)siblingSubview;
- (void)insertSubview:(UIView *)view aboveSubview:(UIView *)siblingSubview;

- (void)bringSubviewToFront:(UIView *)view;
- (void)sendSubviewToBack:(UIView *)view;

- (void)didAddSubview:(UIView *)subview;
- (void)willRemoveSubview:(UIView *)subview;

- (void)willMoveToSuperview:(UIView *)newSuperview;
- (void)didMoveToSuperview;
- (void)willMoveToWindow:(UIWindow *)newWindow;
- (void)didMoveToWindow;

- (BOOL)isDescendantOfView:(UIView *)view; // returns YES for self.
- (UIView *)viewWithTag:(NSInteger)tag; // recursive search. includes self

// Allows you to perform layout before the drawing cycle happens. -layoutIfNeeded forces layout early
- (void)setNeedsLayout;
- (void)layoutIfNeeded;

iOS rendering tree:

  • UIView 负责界⾯显示和事件处理
  • CALayer 负责屏幕渲染(Layer Tree)
  • View/Layer 的变化需要通过渲染器实时渲染到屏幕上
  • layer.presentationLayer

The layer trees for a window

延伸阅读:

12. 响应链(Responder Chain)

The responder chain on iOS

节选自 UIResponder.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject {
@private
}

- (UIResponder*)nextResponder;

- (BOOL)canBecomeFirstResponder; // default is NO
- (BOOL)becomeFirstResponder;

- (BOOL)canResignFirstResponder; // default is YES
- (BOOL)resignFirstResponder;

- (BOOL)isFirstResponder;

// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);

- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0);

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender NS_AVAILABLE_IOS(3_0);
// Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain.
- (id)targetForAction:(SEL)action withSender:(id)sender NS_AVAILABLE_IOS(7_0);

UIKit Inheritance

延伸阅读:

13. Prototype

如 UITableView 的 Prototype Cells:

  • 原型对象的基本特征是可以被复制 NSCopy 和 NSCoding 协议
  • 可以利用界面⼯具和支持复制的类确保互操作性
  • 原型对象使用深复制
  • 利用 NSKeyedArchiver 和 NSKeyedUnarchiver 实现深复制

总结

  • MVC,target-action
  • 两步创建,模版模式
  • 委托模式,观察者模式,消息通知,KVC/KVO
  • 归档和解档(Serialization),复制模式
  • 层次结构,响应链
  • prototype

一句话就是剥离或轻量化 Controller 中关于 View 和 Model 的部分。

MVVM 介绍

MVC 有人称之为 Massive View Controller。

MVC 并没有做太多事情来解决 iOS 应用中日益增长的重量级视图控制器的问题。在典型的 MVC 应用里,许多逻辑被放在 View Controller 里。它们中的一些确实属于 View Controller,但更多的是所谓的“表示逻辑(presentation logic)”,以 MVVM 属术语来说,就是那些将 Model 数据转换为 View 可以呈现的东西的事情,例如将一个 NSDate 转换为一个格式化过的 NSString。根据 Model 属性设置 UILabel、UIButton text。

Model-View-ViewModel

我们的图解里缺少某些东西,那些使我们可以把所有表示逻辑放进去的东西。我们打算将其称为 “View Model” —— 它位于 View/Controller 与 Model 之间:

Model-View-ViewModel 可以:

  • MVVM 可以兼容你当下使用的 MVC 架构。
  • MVVM 增加你的应用的可测试性。
  • MVVM 配合一个绑定机制效果最好。

并没有对我们的 MVC 架构做太多改变。还是同样的代码,只不过移动了位置。它与 MVC 兼容,带来更轻量的 View Controllers。

Model 是不可变的,所以我们可以只在初始化的时候指定我们 View Model 的属性。对于可变 Model,我们还需要使用一些绑定机制,这样 View Model 就能在背后的 Model 改变时更新自身的属性。此外,一旦 View Model 上的 Model 发生改变,那 View 的属性也需要更新。Model 的改变应该级联向下通过 View Model 进入 View。

更轻量的 View Controllers

  • 把 Data Source 和其他 Protocols 分离出来
  • 将 Domain Logic 移到 Model 中
  • 创建 Store 类
  • 把网络请求逻辑移到 Model 层
  • 把 View 代码移到 View 层

我们已经看到一些用来创建更小巧的 view controllers 的技术。我们并不是想把这些技术应用到每一个可能的角落,只是我们有一个目标:写可维护的代码。知道这些模式后,我们就更有可能把那些笨重的 view controllers 变得更整洁。

延伸阅读:

之前直接听英语单词不懂,基础概念也没有,很难真正明白。现在学习了许多中文课程,主要是概念有了,重新看一遍,容易理解的多。

所以最好是想看中文了解相关背景概念,熟悉一下其实的关键词对应的英文,然后再学习英文的课程。

项目驱动,习题驱动学习比较有劲头。

Table View

  • UITableView Segue: NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
  • UITableView Spinner: UITableViewController has an “activity indicator” built in
  • What if your Model changes? (void)reloadData;

Universal Applications

Generally recommned use different storyboards.

UISplitViewController

  • UISplitViewControllerDelegate: awakeFromNib
  • Hide Master in portrait orientation only (the default)

做习题涉及和想到的点

我的习题代码 Github

  • Lazy load image in table view: Apple example
    LazyTableImagesor or SDWebImage
  • Try NSURLSession
  • 望文生义的能力很重要
  • 拿到一个项目代码,start step into from initial view,了解了整个 app 的运行时的结构或循环,才能理解整个 app 的逻辑框架。
  • 框架>目录>类>方法>属性/实例变量,这样子下来整个一个 App 就跑起来了,入口就是 main.m,然后各种设置属性和发消息,基于事件驱动的这一层理解运行时是比较合适的颗粒。
  • Lazy load iamge use AFNetworking
1
[cell.appImage setImageWithURL:[NSURL URLWithString:[mediumImage valueForKeyPath:@"label"]] placeholderImage:[UIImage imageNamed:@"placeholder"]];

习题完成情况

第二题

完成 1.0:

  • 实现 table view 自带刷新
  • 添加 Cellular 开关,并能够检查网络连接方式,并提醒用户
  • NSURLSession 下载 JSON

2015-08-26

进行中 1.1:

  • Lazy load iamge use AFNetworking

2015-09-03 11:36:32

Further Reading

  • 深入理解 RunLoop

    通过 Runloop 讲解了 NSURLConnection 的工作过程
    iOS 中,关于网络请求的接口自下至上有如下几层:

1
2
3
4
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
  • CFSocket 是最底层的接口,只负责 socket 通信。

  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。

  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。

  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。

  • Network Reachability Manager

    AFNetworkReachabilityManager monitors the reachability of domains, and addresses for both WWAN and WiFi network interfaces.

    Do not use Reachability to determine if the original request should be sent.

You should try to send it.
You can use Reachability to determine when a request should be automatically retried.
Although it may still fail, a Reachability notification that the connectivity is available is a good time to retry something.
Network reachability is a useful tool for determining why a request might have failed.
After a network request has failed, telling the user they’re offline is better than giving them a more technical but accurate error, such as “request timed out.”

  • See also WWDC 2012 session 706, “Networking Best Practices.

  • SDWebImage

    AFNetworking takes advantage of Foundation URL Loading System caching using NSURLCache, as well as a configurable in-memory cache for UIImageView and UIButton, which uses NSCache by default. Caching behavior can be further specified in the caching policy of a corresponding NSURLRequest. Other SDWebImage features, like background decompression of image data is also provided by AFNetworking.

    If you’re already using AFNetworking and just want an easy async image loading category, the built-in UIKit extensions will probably fit your needs.

  • AFNetworking example

    iOS example 非常经典,包含了使用方法和一个 UITableView实用框架。

  • AFNetworking 2.0 Tutorial

测试题原文

1. 做一个类似微信的聊天的 tableview(tablewviewcell 自定义) 。只需要 显示文字内容和用户头像(不需要做输入部分,文字内容自己模拟)

1 必须实现文字内容背景
2 背景的长度和高度要合适
3 发生者和接受者左右布局区分

2. 使用 http api 请求解析 json or xml 数据,并且使用 tableview 展示出来 http://ax.itunes.apple.com/WebObjects/MZStoreServices.woa/ws/RSS/toppaidapplications/limit=10/json or xml

提示:

  1. 可以使用第三方库,也可以使用 SDK 基础库
  2. 显示网络下载数据时间
  3. 比较 xml or json 解析效率
  4. tableviewcell自定义

注:

  • 希望大家在一周内上传答案到极客班 github 个人的作业文件夹(以学生编 号命名)
  • IOS 专业 Github 目录: https://github.com/GeekBand/GeekBand-IOS-1501-Homework
  • 老师会在直播环节点评此题
  • 作业的最终批改和成绩公布会在结业时进行。

孔祥波

iOS 测试题解答:

  • Static cell or dynamic
  • Different identifiers for cells
  • 学习 iOS 开发的目标是提升工资,还是爱好,还是进入一个好的团队?
  • 应聘1-3年工作经验要求时,就报极客班孔老师的名字😎
  • 学习曲线摆在那里,一点点进步。
  • apple-ios-samples
  • iOS-Swift-Demos

Swift Tutorial - Core Data

From Swift Tutorial - Core Data by rm2kdev

Project on GitHub

Add some comments into the project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//
// ViewController.swift
// test Core Data
//
// Created by Will Ge on 8/21/15.
// Copyright (c) 2015 gewill.org. All rights reserved.
//

import UIKit
import CoreData

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}

override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}


@IBOutlet weak var textUserName: UITextField!
@IBOutlet weak var textPassword: UITextField!

@IBAction func saveButton(sender: AnyObject) {

// Cora Data already settled done in AppDelegate.swift
// get the ManagedObjectContext
var appDel = UIApplication .sharedApplication().delegate as! AppDelegate
var context: NSManagedObjectContext = appDel.managedObjectContext!

// get the ManagedObject
var newUser = NSEntityDescription.insertNewObjectForEntityForName("Users", inManagedObjectContext: context) as! NSManagedObject

// set value for key
newUser.setValue("" + textUserName.text, forKey: "username")
newUser.setValue("" + textPassword.text, forKey: "password")

// save
context.save(nil)
print("New username: \(textPassword.text) and password: \(textPassword.text) saved successfully.\n")


}

@IBAction func loadButton(sender: AnyObject) {

var appDel = UIApplication .sharedApplication().delegate as! AppDelegate
var context: NSManagedObjectContext = appDel.managedObjectContext!

// add FetchRequest and add Predicate/ SortDescriptor etc.
var request = NSFetchRequest(entityName: "Users")
request.returnsObjectsAsFaults = false
request.predicate = NSPredicate(format: "username = %@", "" + textUserName.text)

var requests: NSArray = context.executeFetchRequest(request, error: nil)!

if (requests.count > 0) {

var res = requests[0] as! NSManagedObject
textUserName.text = res.valueForKeyPath("username") as! String
textPassword.text = res.valueForKeyPath("password") as! String

} else {

print("0 Results Returned... Poterial Error.\n")
}

}
}

test_Core_Data.xcdatamodeld settings

Discovry - Core Data stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//  AppDelegate.swift


// MARK: - Core Data stack

lazy var applicationDocumentsDirectory: NSURL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named "org.gewill.test_Core_Data" in the application's documents Application Support directory.
let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
return urls[urls.count-1] as! NSURL
}()

lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = NSBundle.mainBundle().URLForResource("test_Core_Data", withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {
// The persistent store coordinator for the application. This implementation creates and return a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("test_Core_Data.sqlite")
var error: NSError? = nil
var failureReason = "There was an error creating or loading the application's saved data."
if coordinator!.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil, error: &error) == nil {
coordinator = nil
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data"
dict[NSLocalizedFailureReasonErrorKey] = failureReason
dict[NSUnderlyingErrorKey] = error
error = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}

return coordinator
}()

lazy var managedObjectContext: NSManagedObjectContext? = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
if coordinator == nil {
return nil
}
var managedObjectContext = NSManagedObjectContext()
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()

// MARK: - Core Data Saving support

func saveContext () {
if let moc = self.managedObjectContext {
var error: NSError? = nil
if moc.hasChanges && !moc.save(&error) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog("Unresolved error \(error), \(error!.userInfo)")
abort()
}
}
}

}

Debug:

1
2
p applicationDocumentsDirectory
(NSURL) $R0 = 0x00000001740a8b20 "file:///var/mobile/Containers/Data/Application/601088B9-EB64-4E8D-9817-96E23D0D5D7E/Documents/"
1
2
3
4
5
6
7
8
9
ssh root@192.168.0.101
root# cd /var/mobile/Containers/Data/Application/601088B9-EB64-4E8D-9817-96E23D0D5D7E/Documents/
root# ls -la
total 192
drwxr-xr-x 2 mobile mobile 170 Aug 21 22:16 ./
drwxr-xr-x 5 mobile mobile 204 Aug 21 22:16 ../
-rw-r--r-- 1 mobile mobile 20480 Aug 21 22:16 test_Core_Data.sqlite
-rw-r--r-- 1 mobile mobile 32768 Aug 21 22:24 test_Core_Data.sqlite-shm
-rw-r--r-- 1 mobile mobile 140112 Aug 21 22:24 test_Core_Data.sqlite-wal

Read More

0%