一、Interface Builder
1. Main.storyboard
1) 设计界面顶端
视图控制器代表一个控制器对象,会从文件中加载控制器及相关的视图;
第一响应者是用户当前正在进行交互的对象。
2) 启动文件:LauchScreen
包含启动界面UI的 storyboard。
2. MVC
Model-View-Controller(模型-视图-控制器),用于拆分以下三类代码,帮助确保代码的最大可重用性。
模型:保存程序数据的类。
视图:用户可以看到并能与之交互的元素。
控制器:绑定模型与视图的代码。
输出接口与操作方法:
在控制器类中使用 outlet 来引用 storyboard 中的对象,相当于指向用户界面中的对象的指针。用 IBOutlet 关键字声明。
通常使用类扩展来放置视图控制器的输出接口,因为此视图控制器之外的代码不需要使用它们。
在 storyboard 中对对象进行设置,以触发控制器类中的某些特殊方法(action method / action)。操作方法可接受 0 个或 1 个参数,该参数通常被命名为 sender,指向触发该方法的对象。
3.frame && bounds
下图中,视图A是视图B的父视图。向右为X轴正向,向下为Y轴正向。
frame 用于确定子视图在父视图中的位置,及子视图本身的大小,参考坐标系是父视图自身的 local 坐标系。
bounds 的参考坐标系是视图自身的 local 坐标系。local 坐标系的原点默认是(0,0),坐标原点永远在当前视图左上角。可以通过 setBounds 修改 origin 的值,修改之后坐标原点的坐标值被更新为指定的值,但坐标原点仍在当前视图左上角。
视图B在视图A中的位置是由 View B frame.origin 与 View A bounds 共同决定的。
View B frame.origin 是相对于点 (0,0) 的偏移量,并不一定是相对于父视图A的local坐标系的坐标原点的偏移。因为父视图A的local坐标系的坐标原点的坐标不一定是 (0,0)。
frame的size直接决定了当前view的大小,而bounds的size修改后,当前view的中心点不变,长宽以当前视图的中心点为中心进行放缩。
frame的size不一定等于bounds的size,旋转视图后当前视图的frame的origin与size均发生了改变,而bounds均未改变。
将一个正方形视图顺时针旋转九十度前后:
二、UI组件
UI开发代理设计模式:
当系统 UI 控件接收到用户的一些动作行为时,它并不知道该如何处理,于是通过代理方法将用户的行为告知开发者,开发者在代理方法中处理相关的程序逻辑。
1.文本控件(UILabel)
text => 文本
frame => 位置与大小
backgroundColor => 背景颜色
font => 字体、字号
textColor => 字体颜色
textAlignment => 文本对齐模式,默认居中对齐
shadowColor =>阴影颜色
shadowOffset => 阴影与文字本体之间的偏移量
numberOfLines => 文本的显示行数,默认为1,设置为0代表无限行
lineBreakMode => 换行与截断模式
2.按钮控件(UIButton)
通过 buttonWithType 方法进行控件的初始化,有四种类型可供选择: Custom、System、DetailDisclosure、ContactAdd. 点击 System 类型的 button 时,会出现点击效果。而点击 Custom 类型的 button 时,则无点击效果。
tag 属性可用于识别当前控件,也可通过 tag 属性的值对控件进行操作。
通过 setTitle 方法设置按钮文本。
通过 addTarget: action: forControlEvents 方法添加触发方法。
若 target 不为 nil,则 target 对应的类必须包含 action 对应的方法。否则,系统会沿着响应链向上查找,直至找到能够响应 action 方法的类。
- (void)didTapButton:(SDBButton*)button {
//每当按钮被按下,要对 selected 取反。否则,selected 将永远保持相同的值(YES or NO)
self.selectCountryCodeButton.selected = !button.isSelected;
if (self.selectCountryCodeButton.selected) {
self.countryCodePickerView.hidden = NO;
} else {
self.countryCodePickerView.hidden = YES;
}
}
3.文本输入框控件(UITextField)
placeholder 属性用于设置提示文字,这些文字在有输入时会自动隐藏,当无输入内容时才会显示出来。
borderStyle 属性用于设置输入框的界面风格。
textColor 属性用于设置输入框中文字的颜色。
UITextField 支持自定义左视图、右视图。leftView 属性需要传入一个 UIView 或其子类的对象,leftViewMode 属性设置了左视图的显示模式。
4.开关控件(UISwitch)
UISwitch 的大小是固定的,不能更改,但是可以通过 transform 进行放缩。
isOn 属性用于判断控件的开关状态。
onTintColor 属性用于设置控件开启状态的填充色。
tintColor 属性用于设置控件关闭状态的边界色。
thumbTintColor 属性用于设置开关按钮的颜色。
可通过 addTarget 方法添加触发方法。
5.分页控制器(UIPageControl)
用于页码管理,表现形式是一行圆点,其中高亮的圆点代表当前所处的页码。
currentPageIndicatorTintColor 属性用于设置高亮页码点的颜色。
numberOfPages 属性用于设置页码数量。
通过 addTarget 方法添加触发方法。单击控件时,会触发交互方法。
6.分段控制器(UISegmentedControl)
用于管理和实现一组内容的切换逻辑。
UISegmentedControl* seg = [[UISegmentedControl alloc] initWithItems:@[@"one",@"two"]];
传入的数组中字符串的个数和内容决定了控件中按钮的个数与标题。
setImage 方法可用于设置某个按钮的图案,按钮的编号从 0 开始。
setContentOffset 方法用于设置按钮的标题内容的位置偏移。
momentary 属性若为 NO,则控件为切换按钮模式。否则,为触发按钮模式。
insertSegmentWithTitle 方法用于在当前按钮组中插入一个新的标题按钮。
removeSegment 方法用于移除某个按钮,removeAllSegments 方法用于移除所有按钮。
setTitle 方法可用于重新设置按钮的标题内容。
setWidth 方法用于设置按钮的宽度,但是不一定与标题文字的宽度相匹配。
apportionsSegmentWidthsByContent 属性若为 YES,则按钮的宽度将自适应标题文字的宽度。
通过 addTarget 方法添加触发方法。
7.滑块控件(UISlider)
前述控件的状态变化量是离散的,而滑块控件的状态变化量是连续的。
minimumTrackTintColor 属性用于设置滑块左侧的轨道颜色。
maximumTrackTintColor 属性用于设置滑块右侧的轨道颜色。
thumbTintColor 属性用于设置滑块的颜色。
continuous 属性若为 NO,则当滑动操作结束时,才会触发响应方法。否则,响应方法会被连续触发。
通过 addTarget 方法添加触发方法。
可通过 minimumValueImage、maximumValueImage、setThumbImage 对控件进行图片修饰。
8.活动指示器控件(UIActivityIndicatorView)
用于在加载等待时,做出一些界面活动,以防用户觉得界面卡死。
将控件添加到视图之后,需要调用 startAnimating 方法使指示器开始转动。调用 stopAnimating 方法使指示器停止转动。
9.进度条控件(UIProgressView)
progressTintColor 属性用于设置已走过的进度条的颜色。
trackTintColor 属性用于设置未走过的进度条的颜色。
progress 属性用于设置进度条当前的进度,取值为0~1的浮点数。
10.步进控制器(UIStepper)
用于离散式数据调节。
autorepeat 属性的值若为YES,则当按住控件中的按钮不放时,控制器的值会一直连续改变。否则,仅当完成单击按钮的动作之后,控制器的值才会改变,触发方法才会被执行。
warps 属性的值若为YES,则当控制器的值增加到最大值之后,若仍单击增加按钮,则值会从最小值重新开始增大。否则,当控制器的值达到最大值后,增加按钮将被禁用。
tintColor 属性用于设置控件自身的颜色。
可以通过 setIncrementImage、setDecrementImage 方法自定义增加按钮、减小按钮的图片。
11.选择器控件(UIPickerView)
一个简易的列表控件,用于提供有限个数的选项供用户选择。
/// 用于设置component的数目,此处指代分区数
- (NSInteger) numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 2;
}
/// 用于设置每个component的行数
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return 10;
}
/// 用于设置列表中每一行所显示的内容
- (NSString*) pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component {
return [NSString stringWithFormat:@"%lu分区%lu数据",component,row];
}
/// 用于设置行高
- (CGFloat)pickerView:(UIPickerView *)pickerView rowHeightForComponent:(NSInteger)component {
return 44;
}
/// 用于设置component的宽度
- (CGFloat)pickerView:(UIPickerView *)pickerView widthForComponent:(NSInteger)component {
return 140;
}
当用户选择数据完毕后,系统会调用 didSelectRow 方法通知开发者。
12.CALayer
任何一个UIView的子类都包含CALayer属性。Layer是视图中专门用于渲染UI的层级,并且View层的UI展现也是通过Layer渲染的。
创建圆角按钮:
btn.layer.masksToBounds = YES;
btn.layer.cornerRadius = 12;//用于设置圆角的半径
当 masksToBounds 属性设置为YES时,对视图边界进行修饰的效果才会显现。
创建带边框的按钮:
btn.layer.borderColor = UIColor.blueColor.CGColor;
btn.layer.borderWidth = 12;
添加阴影效果:
btn.layer.shadowColor = UIColor.blueColor.CGColor;
btn.layer.shadowOffset = CGSizeMake(-15, -15);
btn.layer.shadowOpacity = 0.4;//设置阴影的透明度([0,1]),默认为0,即全透明
13.警告控制器(UIAlertController)
用于弹出警告框。
/// 用户单击屏幕时,系统自动调用。在此方法中实现对警告视图的创建和设置。
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"title"
message:@"message of alert"
preferredStyle:UIAlertControllerStyleAlert];
//封装了触发方法的选项按钮
UIAlertAction *action = [UIAlertAction actionWithTitle:@"button"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
NSLog(@"click");
}];
UIAlertAction *action2 = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *_Nonnull action) {
NSLog(@"cancel");
}];
UIAlertAction *action3 = [UIAlertAction actionWithTitle:@"attention" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *_Nonnull action) {
NSLog(@"attention");
}];
//向警告视图内添加选项按钮
[alertView addAction: action];
[alertView addAction: action2];
[alertView addAction: action3];
//向警告视图内添加一个输入框。仅当preferredStyle为UIAlertControllerStyleAlert时,才可用
[alertView addTextFieldWithConfigurationHandler:^(UITextField *_Nonnull textField) {
textField.placeholder = @"place";
}];
[self presentViewController:alertView animated:YES completion:nil];
}
弹出模态ViewController(弹出控制器): 当在view controller A中模态显示view controller B的时候,A就充当presenting view controller(弹出VC),而B就是presented view controller(被弹出VC)。
对于 UIAlertControllerStyleAlert 风格而言,当选项按钮不超过两个时,按钮会横向排列。否则按钮会纵向排列。
14.搜索栏控件(UISearchBar)
是文本框、分段控制器的组合与扩展。
UISearchBar *searchBar = [[UISearchBar alloc] initWithFrame:CGRectMake(0, 100, 300, 100)];
searchBar.tintColor = [UIColor redColor];//设置光标颜色
searchBar.placeholder = @"please enter the information";
searchBar.showsScopeBar = YES;//是否显示扩展栏
searchBar.showsCancelButton = YES;
searchBar.showsBookmarkButton = YES;
searchBar.showsSearchResultsButton = YES;
[searchBar setScopeButtonTitles:@[@"one",@"two",@"three"]];//设置扩展栏的内容
[self.view addSubview:searchBar];
showsBookmarkButton 、showsSearchResultsButton,二者不能同时显示,后设置的会覆盖先设置的。
UISearchBar中单击按钮的触发逻辑和用户输入要搜索的字符时的相关监听都是通过 UISearchBarDelegate 协议中的方法回调的。因此要遵守协议,并将delegate属性设置为当前视图自身。
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
//单击切换扩展栏上的按钮时触发的方法
}
- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
//搜索框中字符将要改变时触发的方法
return YES;
}
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
//搜索框中字符已经改变后触发的方法
}
- (void)searchBarBookmarkButtonClicked:(UISearchBar *)searchBar {
//单击图书按钮所触发的方法
}
- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
//单击取消按钮所触发的方法
}
- (void)searchBarResultsListButtonClicked:(UISearchBar *)searchBar {
//单击搜索结果按钮所触发的方法
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
//单击键盘上的搜索键所触发的方法
}
- (BOOL)searchBarShouldBeginEditing:(UISearchBar *)searchBar {
//搜索栏将要开始编辑时所触发的方法
return YES;
}
- (BOOL)searchBarShouldEndEditing:(UISearchBar *)searchBar {
//搜索栏将要结束编辑时所触发的方法
return YES;
}
- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
//搜索栏已经开始编辑时所触发的方法
}
- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar {
//搜索栏已经结束编辑时所触发的方法
}
15.日期时间选择器(UIDataPicker)
继承于 UIControl,不采用代理回调的模式。
UIDatePicker* datePicker = [[UIDatePicker alloc] initWithFrame:CGRectMake(0, 200, 200, 100)];
datePicker.datePickerMode = UIDatePickerModeTime;
[datePicker addTarget:self action:@selector(change:) forControlEvents:UIControlEventValueChanged];
[self.view addSubview:datePicker];
16.事件的传递、响应过程
UIApplication–>UIWindow–>递归找到最合适处理的控件–>控件调用 touches 方法–>判断是否实现 touches 方法–>没有实现默认会将事件传递给上一个响应者–>找到实现了相关处理方法的上一个响应者–>若找不到,则方法作废。
程序创建的第一个对象是 UIApplication,创建的第一个视图控件是 UIWindow。
UIApplication 的代理是 AppDelegate,AppDelegate 中有一个 window 属性,对应于 UIWindow。
UIWindow 中有一个 rootViewController 属性,对应于 UIViewController。
创建的视图 view 被添加到 UIViewController 上,随后显示出来。
传递过程:
-
当触摸事件发生时,压力转为电信号,iOS系统将产生UIEvent对象,记录事件产生的时间和类型。然后系统将事件加入到一个由UIApplication管理的事件队列中。
-
UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)。
-
主窗口会在视图层次结构中依照传递链找到一个最合适的视图来处理触摸事件,并使其成为第一响应者。在传递过程中,视图会首先看自己是否能处理事件,触摸点是否在自己身上。如果能,则继续寻找是否存在更合适的子视图。若不能,则传递过程中止,从而该事件不被响应。
-
找到合适的视图控件后,就会调用视图控件的 touches 方法来作事件的具体处理, 如 touchesBegin 等。如果找到了最合适的响应者,但是其没有实现touches方法, 就会调用其上一个响应者对象的touches方法。
第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder属性决定,可以通过重写该属性来决定传递规则。当一个 view 被 add 到 superView 上的时候,它的 nextResponder 属性就会指向它的 superView。
Hit-Test:
用户的触摸事件首先会由系统截获,进行包装处理等。然后递归遍历 view 层级,直到找到合适的响应者来处理事件,这个过程被称为 Hit-Test。
// 返回视图层级中能响应触控点的最深视图
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 返回视图是否包含指定的某个点
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
通过在视图层级中依次对视图调用这两个方法来确认该视图是否能响应被点击的点。首先会调用hitTest,然后hitTest会调用pointInside,最终hitTest返回的那个view就是最终的响应者Responder。
传递链中是没有 controller 的,因为 controller 本身不具有大小的概念。但是响应链中是有 controller 的,因为 controller 继承自 UIResponder。
事件不响应的原因:
- 触摸点不在当前范围内。
- alpha < 0.01,透明度小于 0.01。
- hidden = true,隐藏不可见。
- userInteractionEnabled = false,不允许交互。
17.UITouch
// 一根或者多根手指开始触摸view时自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指在view上移动时自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 一根或者多根手指离开view时自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程时自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
// 触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
// 触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
// 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
// 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
// 当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
UITouch保存着跟手指相关的信息,比如触摸的位置、时间、阶段等。 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指所处的触摸位置。 当手指离开屏幕时,系统会销毁相应的UITouch对象。
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象。 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。
所以根据touches中UITouch对象的个数可以判断出是单点触摸还是多点触摸。
18.UIGestureRecognizer
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action;
- (void)addTarget:(id)target action:(SEL)action;
- (void)removeTarget:(nullable id)target action:(nullable SEL)action;
// 枚举了手势的当前状态
@property(nonatomic,readonly) UIGestureRecognizerState state;
// 触摸开始时,便会开始识别手势
// 默认为YES,即当识别到手势的时候,发送touchesCancelled:withEvent:或pressesCancelled:withEvent:以终止触摸事件在事件传递链上的传递。
@property(nonatomic) BOOL cancelsTouchesInView;
// 默认为NO ,在触摸开始的时候,就会发消息给事件传递链,如果设置为YES,在触摸没有被识别失败前,不会给事件传递链发送消息。
@property(nonatomic) BOOL delaysTouchesBegan;
// 默认为YES ,即手势识别结束后,等待一个很短的时间,如果没有接收到新的手势识别任务,发送touchesEnded或pressesEnded消息到事件传递链。
@property(nonatomic) BOOL delaysTouchesEnded;
// 举例:设置为 YES,轻拍三下屏幕会被识别为轻拍三下手势。设置为 NO,则会被识别为三次轻拍一下手势。
// [A requireGestureRecognizerToFail:B] 它可以指定当A手势发生时,即便A已经滿足条件了,也不会立刻触发,会等到指定的手势B确定失败之后才触发。
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
因为addTarget方式的存在,iOS允许一个手势对象对应多个selector触发方法,并且触发的时候,所有添加的selector都会被执行。
子类:
- 点击手势——UITapGestureRecognizer,支持单击和多次点击,在手指触摸屏幕并抬起时被触发。
- 捏合手势——UIPinchGestureRecognizer,即当双指捏合和扩张时会触发动作的手势。
- 拖拽手势——UIPanGestureRecognzer,即当点中视图并进行慢速拖拽时会触发的手势。
- 滑动手势——UISwipeGestureRecognizer,滑动手势更快,拖拽手势速度较慢。
- 旋转手势——UIRotationGestureRecognizer,即进行旋转动作时会触发的手势。
- 长按手势——UILongPressGestureRecognizer,即长按屏幕时触发的手势。
19.UINavigationController(导航控制器)
导航控制器是用于管理 ViewController 的容器类。采用栈的设计模式,当一个 VC 被 push 进导航控制器中时,它会覆盖替换先前展示的视图,从而展现在当前的屏幕界面上。对于 pop 操作,当前栈顶的 VC 会被弹出,pop 操作完成之后,栈顶的 VC 会被呈现出来。
导航控制器内部提供一个 UINavigationBar 类型的导航栏,用于显示当前栈顶的视图控制器标题、放置一些交互按钮。导航栏上的按钮需要被设置为 UIBarButtonItem 对象。该对象可以使用系统风格,也可进行自定义。
导航控制器还带有工具栏 UIToolBar 控件,默认情况下该控件被隐藏。工具栏中放置的工具按钮也是 UIBarButtonItem 类型的对象。
通过更改导航控制器的一些属性,可以设置在某些特定情况下(例如横屏使用、用户单击屏幕等)自动隐藏导航栏与工具栏。
20.UITabBarController(标签控制器)
标签控制器也是用于管理 VC 的容器类。但是不同于导航控制器,标签控制器所管理的视图之间不存在层级关系,而是并列的。因此,只要标签控制器存在,那么视图控制器便不会被释放。标签控制器多用于分类并列结构的应用中。
标签控制器自带一个标签栏,显示在界面的底部。用于展示所管理的 VC 对应的标签。当向标签控制器中添加多于5个 VC 时,标签控制器会自动创建一个包含表格视图的导航控制器,在表格视图中将多出的 VC 排列出来。此外,系统还自动在导航栏上创建了一个按钮,用于改变所管理的 VC 的排序。
上述标签栏上的具体标签为 UITabBarItem 对象,开发者可自定义该对象。系统风格的 UITabBarItem 会提供特定的图片和标题。
21.UIScrollView(滚动视图)
滚动视图用于展示某些内容超过一屏的视图。例如,用户需要通过滑动屏幕来浏览当前网页的所有内容。
UIScrollView 的 contentSize 属性用于设置滚动视图的内容大小,其尺寸决定了可滚动的范围。
contentInsetAdjustmentBehavior 属性用于设置如何自动调整当前视图的 ContentInset,该属性与”顶部留白“问题相关。
若将 scrollsToTop 属性设置为 YES,则当用户单击状态栏(屏幕上显示服务商与系统时间的一栏)时,滚动视图便会自动滚动到顶端。
self.view.backgroundColor = UIColor.whiteColor;
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:**self**.view.frame];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(50, 10, 200, 800)];
label.backgroundColor = UIColor.redColor;
// 将要呈现的视图添加到 UIScrollView
[scrollView addSubview:label];
scrollView.contentSize = CGSizeMake(**self**.view.frame.size.width*2, self.view.frame.size.height*2);
// 将滚动视图添加到 self.view
[self.view addSubview:scrollView];
在用户对滚动视图进行操作时,UIScrollViewDelegate 协议中定义了许多方法可以帮助开发者对用户的行为进行监听。首先要让相应类遵守 UIScrollViewDelegate 协议,设置 UIScrollView 对象的 delegate 属性为遵守协议的类对象。
22.WKWebView(网络视图)
WKWebView 可以响应用户的操作进行网页界面之间的跳转,此时不需要当前开发者的参与,因为这是网页端的逻辑。
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame];
// 通过网页地址字符串创建了一个 URL 链接
// 该函数内部封装了对标准格式的字符串类型的 URL 的解析操作
NSURL *url = [NSURL URLWithString:@"https://juejin.cn/user/3334184167571352"];
// 通过 URL 链接创建网络请求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 使用网络请求加载网络视图
[webView loadRequest:request];
[self.view addSubview:webView];
通过 HTML 字符串加载网络视图,用于加载本地网页文件等。
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame];
NSString *htmlString = @"<html>"
"<head>"
"<meta charset=\"UTF-8\">" //字符串中的引号,要使用转义符进行转义
"<title>主标题|副标题</title>"
"</head>"
"<body>"
"<p>hello world</p>"
"</body>"
"</html>";
[webView loadHTMLString:htmlString baseURL:nil];
[self.view addSubview:webView];
通过 NSData 数据加载网络视图。使用此方式不需再限定数据的格式。
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame];
NSURL *imageURL = [[NSURL alloc] initFileURLWithPath:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];
NSData *data = [NSData dataWithContentsOfURL:imageURL];
[webView loadData:data MIMEType:@"image/png" characterEncodingName:@"UTF-8" baseURL:imageURL];
[self.view addSubview:webView];
WKNavigationDelegate:
23.UITableView(表格视图)
UITableView 通过数据源进行视图与数据的绑定,通过代理方法对表格视图的属性进行设置。
在 UITableView 中,每条数据通过 UITableViewCell 类对象进行展示,UITableView 控件对其中的 cell 进行复用管理。
UITableViewDataSource 协议中有两个方法必须实现,UITableViewDelegate 协议中的所有方法都是可选实现的。
对于 cell 的复用,采用复用池的设计模式。例如若当前表格视图有100行数据,但是屏幕只能显示10行数据。那么 UITableView 创建 cell 视图时,只需创建11个 cell 视图即可。当某个 cell 被滑出屏幕之外时,则被回收进复用池。新的 cell 将要被滑入屏幕时,从复用池中取用。
一个 UITableView 控件可以包含多种不同类型的 cell,可通过 Identifier 参数进行区分。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 从复用池中取 cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellID"];
// 若复用池中没有 cell 可用,则创建新的 cell。设置的 Identifier 参数必须与复用池中的 cell 的参数一致
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cellID"];
}
return cell;
}
在将 UITableView 控件设置为可编辑模式的前提下,可对 cell 进行增、删、移动操作。
UITableView 编辑模式的相关方法都是 UI 层面的方法,不会改变数据的结构。因此需要手动对数据进行操作,如删除、添加、改变顺序等。
为 UITableView 添加分区索引栏。
- (NSArray <NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
// 返回的数组的元素个数应与控件的分区数一致,二者一一对应
return @[@"one",@"two"];
}
24.UICollectionView
1)特点
- 支持水平方向和竖直方向两个方向的布局。
- 数据载体为 item。
- 通过 UICollectionViewLayout 类配置的方式进行界面布局。类中实际存放的是每个数据载体 item 的布局信息,包括大小、位置、3D变换等。
- 通过 UICollectionViewLayoutDelegate 协议可以动态地对布局进行重设。
三、多线程编程
为了提高资源利用率来提升系统整体效率,实际往往是将耗时操作放在后台执行,避免阻塞主线程,在iOS中UI绘制和用户响应都是放在主线程进行。
1.NSThread
- (void)viewDidLoad {
[super viewDidLoad];
//打印当前线程
//通过qualityOfService属性来设置线程优先级
NSLog(@"开始:%@ 优先级:%ld", [NSThread currentThread], [NSThread currentThread].qualityOfService);
//1.创建NSTread对象,必须调用start方法开始,并且只能传一个参数object
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
NSThread *anotherThread = [[NSThread alloc] initWithBlock:^{}];
thread.name = @"testThread";
thread.qualityOfService = NSQualityOfServiceUserInteractive;
[thread start];
//2.直接创建并启动线程
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"test"];
[NSThread detachNewThreadWithBlock:^{}];
//3.隐式直接创建
[NSThread performSelectorInBackground:@selector(run:) withObject:nil];
NSLog(@"结束:%@", [NSThread currentThread]);
}
- (void)run:(NSObject *)object {
//阻塞休眠
// [NSThread sleepForTimeInterval:5];
//中止当前线程
// [NSThread exit];
NSLog(@"子线程运行:%@ %@ 优先级:%ld", [NSThread currentThread], object, [NSThread currentThread].qualityOfService);
}
2.NSOperation
是NSOperationQueue中一个操作任务。NSOperation本身是一个抽象类,不能直接使用。可以使用系统提供的子类NSInvocationOperation 和NSBlockOperation,或者自己实现NSOperation子类的方式来执行操作任务。NSOperation对象都是通过调用start方法来开始执行任务的。可以通过依赖关系设置操作执行顺序,可以控制任务在特定的任务执行完后才执行。
NSOperationQueue:负责管理系统提交的多个NSOperation,底层维护了一个线程池。可以设置同一个队列中任务的优先级,能够使同一个并行队列中的任务区分先后地执行。
NSOperation、NSOPerationQueue的优势:
1)可以添加完成的代码块,在操作完成后执行。
2)添加操作之间的依赖关系,方便的控制执行顺序。
3)设定操作执行的优先级。
4)可以很方便的取消一个操作的执行。可以取消未执行的任务。
5)可以使用KVO观察对操作执行状态的更改:isExecuteing、isFinished、isCancelled。
NSOperationQueue *queue;
//获取执行当前NSOperation的NSOperationQueue队列
queue = [NSOperationQueue currentQueue];
//获取主线程的NSOperationQueue队列
queue = [NSOperationQueue mainQueue];
//自定义队列
queue = [[NSOperationQueue alloc] init];
//队列名
queue.name = @"testOperationQueue";
//最大并发操作数(系统有限制,即使设置很大,也会自动调整)
queue.maxConcurrentOperationCount = 10;
//设置优先级
queue.qualityOfService = NSQualityOfServiceDefault;
//自定义NSOperation,如果SEL和Block为空,系统不会加入到指定队列
NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"blockOperation");
}];
//添加依赖关系,invocationOperation执行完后才执行blockOperation
[blockOperation addDependency:invocationOperation];
//添加到队列中
[queue addOperation:invocationOperation];
[queue addOperations:@[invocationOperation, blockOperation] waitUntilFinished:NO];
//直接添加代码块任务
[queue addOperationWithBlock:^{
//do something
}];
//打印所有的NSOperation
for(int i=0; i<queue.operationCount; i++) {
NSLog(@"队列%@的第%d个NSOperation:%@", queue.name, i, queue.operations[i]);
}
//终止所有NSOperation
[queue cancelAllOperations];
//执行完所有NSOperation才能解除阻塞当前线程
[queue waitUntilAllOperationsAreFinished];
3.GCD(Grand Central Dispatch)
1)基本概念
队列:队列负责开发者提交的任务,不过不同任务的执行时间不一样,先处理的任务不一定先完成。队列既可是串行的,也可是并行的,队列底层会维护一个线程池来处理任务,串行队列只需要维护一个线程即可,并行队列则需要维护多个线程。
任务:用户提交给队列的工作单元,这些任务将会提交给队列底层维护的线程池。
异步:可以在新的线程中执行任务,但不一定会开辟新的线程。dispatch函数会立即返回,然后Block在后台异步执行。
同步:在当前线程执行任务,不会开辟新的线程。必须等到Block函数执行完毕后,dispatch函数才会返回。
2)特点
GCD可用于多核的并行运算;
GCD会自动利用更多的CPU内核(比如双核、四核);
GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。
3)常用 API
获取队列:
//获取指定优先级的全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//创建自定义并行队列
dispatch_queue_t queue1 = dispatch_queue_create("testQueue1", DISPATCH_QUEUE_CONCURRENT);
//获取系统主线程关联的串行队列
dispatch_queue_t queue2 = dispatch_get_main_queue();
//创建自定义串行队列
dispatch_queue_t queue3 = dispatch_queue_create("testQueue3", DISPATCH_UEUE_SERIAL);
提交任务:
//异步提交代码块到并发队列
dispatch_async(queue, ^{
});
//同步提交代码块到自定义并发队列
dispatch_sync(queue1, ^{
});
//异步提交代码块到串行队列,线程池将在指定时间执行代码块(实际是5秒后加入到队列中,实际并不一定会立刻执行,一般精度要求下是没问题的)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5*NSEC_PER_SEC)), queue2, ^{
});
//异步提交代码到自定义串行队列,同步函数,无论是在串行还是并行队列中执行,都要执行完才返回,所以要防止线程阻塞和死锁,time表示当前是第几次(如果提交给并发队列,则会启动五个线程来执行)
dispatch_apply(5, queue3, ^(size_t time) {
});
使得代码仅被执行一次:
//用于判断该代码块是否被执行过
static dispatch_once_t onceToken;
//主线程仅执行一次代码块
dispatch_once(&onceToken, ^{
});
dispatch_group:
//等group执行完后,才能执行下一步
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
/** 组(用于需要等待多个任务全部执行完再进行下一步) */
dispatch_group_t group = dispatch_group_create();
//并发执行的代码块1
dispatch_group_async(group, queue, ^{
});
//并发执行的代码块2
dispatch_group_async(group, queue, ^{
});
//等待两个代码块执行完汇总
dispatch_group_notify(group, queue, ^{
});
并发队列、栅栏:
//并发队列异步执行代码块1,2
dispatch_async(queue, ^{
//代码块1
});
dispatch_async(queue, ^{
//代码块2
});
/** 栅栏(用于需要依次执行完多个线程组) */
//1,2执行完后才会执行3,4
dispatch_barrier_async(queue, ^{
});
//并发队列异步执行代码块3,4
dispatch_async(queue, ^{
//代码块3
});
dispatch_async(queue, ^{
//代码块4
});
信号量:
/** 信号量(用于控制线程的等待和执行) */
//创建信号量,value 表示初始信号总量,必须大于或等于0
dispatch_semaphore_t t = dispatch_semaphore_create(1);
/*发送一个信号,这个函数会使传入的信号量的值加1。
返回值为 long 类型,当返回值为0时表示当前并没有其他线程等待传入的信号量。
当返回值不为0时,表示当前有一个或多个线程在等待传入的信号量,并且该函数会唤醒一个等待的线程(当线程有优先级时,唤醒优先级最高的线程;否则随机唤醒一个线程)*/
dispatch_semaphore_signal(t);
/*使信号量-1,如果信号量的值为0,则会一直等待(阻塞所在线程),否则继续执行后续语句
如果信号量的值大于0,那么该函数所处线程会继续执行后面的语句,并且将信号量的值减1;
如果信号量的值为0,那么该函数就阻塞当前线程,并等待timeout(即第二个参数设定的时间)被触发。
如果等待期间,信号量的值被 dispatch_semaphore_signal 函数加1从而使得信号量的值大于0,且该函数所处线程获得了信号量,那么就继续向后执行并将信号量的值减1。 否则该函数会一直阻塞当前线程,直至等到触发timeout,之后所处线程自动执行后续语句。*/
dispatch_semaphore_wait(t, DISPATCH_TIME_FOREVER);
/*1.可以将异步执行变为同步执行,如需要等待下载完后再直接返回数据,即等待 block 执行结束再继续执行后续语句*/
//总信号量设置为0
dispatch_semaphore_t t1 = dispatch_semaphore_create(0);
//执行耗时代码
void (^downloadTask)(void) = ^ {
//下载图片
//完成后发送信号量
dispatch_semaphore_signal(t1);
};
downloadTask();
//一直等到信号量计数为1才执行下一步,也就是等到图片下载完成
dispatch_semaphore_wait(t1, DISPATCH_TIME_FOREVER);
/*2.保证线程安全*/
//设置信号量初始计数为1,保证同一时刻只能有一个线程执行当前代码
dispatch_semaphore_t t2 = dispatch_semaphore_create(1);
//相当于加锁,消耗使用计数,如果已经被一个线程使用,后续只能挂起等待信号量回复
dispatch_semaphore_wait(t2, DISPATCH_TIME_FOREVER);
//执行业务代码
...
...
//解锁
dispatch_semaphore_signal(t2);
/*3.模拟NSOperationQueue的最大并发操作数*/
//最大并发操作支持10
dispatch_semaphore_t t3 = dispatch_semaphore_create(10);
//剩余操作同上,其实就是类似于将NSOperationQueue的maxConcurrentOperationCount设置为10
通过信号量使得并发 for 循环变成串行操作:
signal 和 wait 操作不能在同一个线程中执行,否则会产生死锁。
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
for (int i = 0; i < 5; i++) {
//下次操作必须等到信号量大于 0 才会继续,否则下次操作将永久阻塞
printf("信号量等待中\n");
//模拟异步任务
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"https://github.com"]];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[tampArray addObject:@(i)];
NSLog(@"本次耗时操作完成,信号量+1 %@\n",[NSThread currentThread]);
//本次for循环的异步任务执行完毕,此时要发送信号使得信号量值加 1,否则下次操作将永远不会被触发
dispatch_semaphore_signal(sema);
}];
[dataTask resume];
//必须等到信号量大于 0 才会继续下一次循环
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
}
参考文章:
四、UI 知识
1.视图控制器的生命周期
视图控制器的生命周期是指一个或多个视图控制器从创建到释放的整个过程。当需要在生命周期的不同时期执行不同类型的方法时,通常会通过重写这些方法来执行相应的操作。
1) alloc:创建对象,分配空间
2) initWithCoder:(NSCoder *)aDecoder(如果使用storyboard或者nib)
3) init (initWithNibName):如果通过nib初始化对象
4) awakeFromNib:这个方法用的时候,outlet还没有连接起来,是view Controller刚从storyboard建的时候,没有完全建好,不过可能有一些事情要在这个方法里面完成,比如splitViewDelegate,需要在非常早期完成。
5) loadView:加载view
6) viewDidLoad:view加载完毕
7) viewWillAppear:控制器的view将要显示
8) viewWillLayoutSubviews:控制器的view将要布局子控件
9) viewDidLayoutSubviews:控制器的view布局子控件完成这期间系统可能会多次调用viewWillLayoutSubviews 、viewDidLayoutSubviews 俩个方法
10) viewDidAppear:控制器的view完全显示
11) viewWillDisappear:控制器的view即将消失的时候这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法
12) viewDidDisappear:控制器的view完全消失的时候
13) dealloc:控制器销毁
当点击某个按钮,出现一个新视图时:
首先会加载下一个界面然后才会调用当前界面的消失方法。
1)alloc:ViewController2
2)initWithCoder:(NSCoder *)aDecoder:ViewController2
3)init (initWithNibName):ViewController2
4)awakeFromNib:ViewController2
5)loadView:ViewController2加载view
6)viewDidLoad:ViewController2view加载完毕
7)viewWillDisappear:ViewController1 将要消失
8)viewWillAppear:ViewController2将要出现
9)viewWillLayoutSubviews:ViewController2
10)viewDidLayoutSubviews:ViewController2
11)viewDidDisappear:ViewController1 完全消失
12)viewDidAppear:ViewController2完全出现
视图消失的过程:
1)viewWillDisappear:ViewController2视图将被从屏幕上移除之前执行
2)viewWillAppear:ViewController1控制器的view将要显示
3)viewDidDisappear:ViewController2视图已经被从屏幕上移除,用户看不到这个视图了
4)viewDidAppear:ViewController1
5)dealloc:ViewController2视图被销毁,此处需要对在init和viewDidLoad中创建的对象进行释放
2.绝对布局与相对布局
iOS坐标系以左上角为坐标原点,往右为X轴正方向,往下是Y轴正方向。
frame:每个view必备的属性,表示view在父视图坐标系中的位置和大小,参考坐标系是父视图的bounds所对应的坐标系。每个视图的父视图不一定相同。
2.1 autoresizing
autoResizing 只能设置父视图与子视图之间的关系。
仅当父视图的 autoresizesSubviews 属性被设置为 YES 时,通过 autoResizing 做的调整才会生效。
通过设置子视图的 autoresizingMask 属性,可设置当父视图改变尺寸时,子视图的四边边距及宽、高的拉伸情况。枚举值如下:
// 可通过按位或运算组合使用枚举值
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0, // 不自动调整
UIViewAutoresizingFlexibleLeftMargin = 1 << 0, // 弹性调整与父视图左边界的距离,保证与父视图右边界的距离不变
UIViewAutoresizingFlexibleWidth = 1 << 1, // 弹性调整子视图的宽度,保证与父视图左、右边界的距离都不变
UIViewAutoresizingFlexibleRightMargin = 1 << 2, // 弹性调整与父视图右边界的距离,保证与父视图左边界的距离不变
UIViewAutoresizingFlexibleTopMargin = 1 << 3, // 弹性调整与父视图上边界的距离,保证与父视图下边界的距离不变
UIViewAutoresizingFlexibleHeight = 1 << 4, // 弹性调整子视图的高度,保证与父视图上、下边界的距离都不变
UIViewAutoresizingFlexibleBottomMargin = 1 << 5 // 弹性调整与父视图下边界的距离,保证与父视图上边界的距离不变
};
self.view.backgroundColor = UIColor.whiteColor;
UILabel *label1 = [[UILabel alloc] init];
label1.frame = CGRectMake(100, 150, 200, 220);
label1.backgroundColor = UIColor.blueColor;
[self.view addSubview:label1];
UILabel *label2 = [[UILabel alloc] init];
label2.frame = CGRectMake(0, 50, 100, 120);
label2.backgroundColor = UIColor.blackColor;
[label1 addSubview:label2];
label1.autoresizesSubviews = YES;
//label1.autoresizesSubviews = NO;
label2.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
label1.frame = CGRectMake(100, 150, 260, 220);
//通过查看 label1 尺寸改变前后,label2 的位置变化来理解 autoResizing
2.2 autolayout 自动布局框架
其设计目的是为了让开发者将精力集中在控件之间的相对位置关系而非控件的具体坐标上。
若先设置了手动布局(如手动设置视图的 frame),而后使用了 autolayout。则 autolayout 布局会覆盖之前的手动布局。
使用 autolayout 进行布局的核心是约束,即控制并调节控件的尺寸和位置。主要有两种类型的约束关系:子视图与父视图控件之间的约束关系、平级视图之间的约束关系。
UIView *view = [[UIView alloc] init];
view.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:view];
//将 view 约束在屏幕竖直方向的中央位置
NSLayoutConstraint *constraintX = [NSLayoutConstraint constraintWithItem:view //要约束的第一个视图对象
attribute:NSLayoutAttributeCenterX //第一个视图的约束属性
relatedBy:NSLayoutRelationEqual //约束属性间的关系
toItem:self.view //要约束的第二个视图对象
attribute:NSLayoutAttributeCenterX //第二个视图对象的约束属性
multiplier:1 //约束的比例
constant:0]; //约束的值
[self.view addConstraint:constraintX];
//将视图高度约束为 100
NSLayoutConstraint *constraintHeight = [NSLayoutConstraint constraintWithItem:view //要约束的第一个视图对象
attribute:NSLayoutAttributeHeight //第一个视图的约束属性
relatedBy:NSLayoutRelationEqual //约束属性间的关系
toItem:nil //要约束的第二个视图对象
attribute:NSLayoutAttributeNotAnAttribute //第二个视图对象的约束属性
multiplier:1 //约束的比例
constant:100]; //约束的值
[view addConstraint:constraintHeight]; //添加一个约束
第一个视图对象与第二个视图对象之间的约束属性的值: view1 = view2 * multiplier + constant.
注意事项:
- 在通过 addConstraint 方法添加约束之前,必须先将子视图添加到父视图上。
- 与父视图相关的约束,约束对象必须添加到父视图上。
- 若要通过代码添加控件的约束,必须将控件的 translatesAutoresizingMaskIntoConstraints 属性设置为 NO。
2.3 VFL(Visual Format Language)
VFL 中不能出现空格,并且左右两端都要保留 ‘|’ 。
///@param constraintsWithVisualFormat 约束对应的 VFL 字符串
///@param options 设置所约束的控件的对齐模式
///@param metrics 若 VFL 字符串中使用了变量,则需要使用此参数将变量映射到 VFL 字符串中。若变量不是 OC 对象,需要将其转换为 OC 对象
///@param views 将 VFL 中用到的控件名称映射为视图控件对象。NSDictionaryOfVariableBindings 是可以实现此目的的宏。下述两个方法调用中的写法等价。
NSArray *constraintArray = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-20-[view(100@10)]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)];
NSNumber *width = @200;
NSArray *constrainintArray2 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-width-[view(200)]|" options:0 metrics:@{@"width":width} views:@{@"view":view}];
[self.view addConstraints:constraintArray];
[self.view addConstraints:constrainintArray2]; //添加一组约束
//removeConstraint、removeConstraints 用于移除约束
H 代表在水平方向添加约束,'|' 出现在字符串左端,代表父视图左边界,出现在右端则代表右边界。
V 代表在竖直方向添加约束,'|' 出现在字符串左端,代表父视图上边界,出现在右端则代表下边界。
-X- 代表距离父视图边界的距离,X可为常量或变量。
[控件名称(约束控件的宽度或高度@此约束的优先级)]
2.4 Masonry
需要引入头文件 Masonry.h
若先设置了手动布局(如手动设置视图的 frame),而后使用了 autolayout。则 autolayout 布局会覆盖之前的手动布局。
自动布局只对 UIView 及其子类有效。不能用于其它类型的 UI 控件。
// 新增约束
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
// 更新约束,不需要移除旧约束
// 同一个布局:相对的元素必须一致
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
// 清除之前的所有约束,使用最新的约束
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *label = [[UILabel alloc] init];
[self.view addSubview:label]; //需要先将视图添加到父视图上
[label mas_makeConstraints:^(MASConstraintMaker *make) {
// 将视图控件的中心约束等于其父视图的中心
make.center.equalTo(self.view);
// 将控件的高度约束为 50 个单位
make.height.equalTo(@50);
// 将控件的宽度约束为 50 个单位
make.width.equalTo(@50);
// 将控件约束在距离父视图上边界 20 个单位处
make.top.equalTo(@20);
}];
label.backgroundColor = UIColor.redColor;
return;
}
// 将控件约束在距离父视图左边界10个单位处
make.left.equalTo(self.view.mas_left).offset(10);
// 大于等于、设置偏移量、手动设置优先级
make.right.greaterThanOrEqualTo(self.view.mas_right).offset(20).priority(100);
// 小于等于、指定优先级高低(Height、Medium、Low)
make.top.lessThanOrEqualTo(self.view.mas_top).offset(15).priorityHigh();
// 将控件的左右边界约束在父视图的左右边界处
make.left.right.equalTo(self.view);
// 当布局是从左到右时
leading(首部) 等价于 left, trailing(尾部) 等价于 right.
// 当布局是从右到左时
leading(首部) 等价于 right, trailing(尾部) 等价于 left.
参考文章
1.使用Masonry的mas_updateConstraints错误理解
3.presentViewController 与 pushViewController
当输入 Wi-Fi 密码时,会有一个由下至上的动画,而后出现一个用于输入密码的界面。
在 iPhone 系统中,当从 Settings 切换到 General 时,会有一个从右至左的动画,而后展现 General 的界面,并且在界面左上角有用于“返回上一层”的按钮。
一般情况下,前者通过 presentViewController 实现,后者通过 pushViewController 实现。
presentViewController: 暗示使用者需要完成或取消某件事情(例如输入密码等)之后才能做其他事。
pushViewController: 用于浏览不同的界面。使用者可以决定前进或后退到某个页面。
对于移除当前页面操作,前者对应于 dismissViewController,后者对应于 popViewController。二者不要混用。
本节参考文章:
1. why "present modal view controller"?
2. difference between presentViewController and UINavigationController?
4.Storyboard 与纯代码方式的区别
Storyboard:
-
优点:
- 所见即所得,界面更加直观;
- 对 UI 设计十分友好;
- 方便自动布局。
-
缺点:
- 多人协同开发时,若有冲突,难以解决冲突。
- 加载速度、运行速度稍低。
纯代码方式:
-
优点:
- 代码可复用性高;
- 易于 Debug;
- 便于多人协同开发;
- 便于版本控制,易于追踪相关代码。
-
缺点:
- 界面不够直观;
- 代码量较大,书写较为复杂。
5.CAShapeLayer
继承自 CALayer。
5.1 FillRule 属性
位于图形内部的区域,将被填充为 fillColor 指定的颜色。
图形的边界线的颜色,由 strokeColor 所决定。
kCAFillRuleNonZero:
为该属性默认值。按该规则,若要判断一个点是否在图形内,从该点作任意方向的一条射线,然后检测射线与图形路径的交点情况。从0开始计数,若射线穿过的路径的方向是从左向右,则计数加1。若路径方向是从右向左,则计数减1。最终如果计数结果是0,则认为点在图形外部,否则认为点在图形内部。如下图所示。
kCAFillRuleEvenOdd:
按该规则,要判断一个点是否在图形内,从该点作任意方向的一条射线,然后检测射线与图形路径的交点的数量。如果交点个数是奇数则认为点在内部,交点个数是偶数则认为点在外部。因此该规则与路径方向无关。如下图所示。
参考文章
2.iOS CAShapeLayer的FillRule属性总结
3.iOS 利用CAShapeLayer的FillRule属性生成一个空心遮罩的layer
6.UIView
6.1 contentMode 属性
typedef NS_ENUM(NSInteger, UIViewContentMode) {
UIViewContentModeScaleToFill,
UIViewContentModeScaleAspectFit, // contents scaled to fit with fixed aspect. remainder is transparent
UIViewContentModeScaleAspectFill, // contents scaled to fill with fixed aspect. some portion of content may be clipped.
UIViewContentModeRedraw, // redraw on bounds change (calls -setNeedsDisplay)
UIViewContentModeCenter, // contents remain same size. positioned adjusted.
UIViewContentModeTop,
UIViewContentModeBottom,
UIViewContentModeLeft,
UIViewContentModeRight,
UIViewContentModeTopLeft,
UIViewContentModeTopRight,
UIViewContentModeBottomLeft,
UIViewContentModeBottomRight,
};
UIViewContentModeScaleToFill 是缩放填充,可能会导致图片变形。此为默认值。
UIViewContentModeScaleAspectFit 是等比例缩放图片以使得图片能够整体显示在 UIView 中,因此若图片原始尺寸大于 UIView 的尺寸,则 UIView 中可能存在空余部分。
UIViewContentModeScaleAspectFill 是等比例缩放图片以使得图片能够将整个 UIView 填充,因此若图片原始尺寸大于 UIView 的尺寸,则图片可能无法完全显示出来。
对于余下的 value,当图片尺寸超过 UIView 的尺寸时,只会有部分图片被显示在 UIView 中;当图片尺寸小于 UIView 的尺寸时,图片会在 value 指定的位置被显示出来。
参考文章
1.iOS UIImageView的contentMode属性详解
2.UIImageView 的contentMode属性,以及图片拉伸技巧
7. iOS Device Display
设计师给出的设计稿里的尺寸,实际上指的是 UIKit Size (Points),此属性与屏幕的分辨率不是同一属性。屏幕的分辨率一般是指屏幕的横向、纵向的像素个数。
UIImage 的属性包括 size 与 scale,其中 size 属性存储的是图片的逻辑尺寸,而 scale 属性存储的是图片的放缩因子。Value(size) * Value(scale) 的结果便是图片实际的像素数目。
一般来说,图片名称包含 '@2x' 的图片,其放缩因子会被设置是 2。图片名称包含 '@3x' 的图片,其放缩因子会被设置是 3。若不包含相应的指示符,则放缩因子默认为 1.
参考文章
1.iOS Device Compatibility Reference
2.scale
3.size