UI-1
---恢复内容开始---
1 [TOC] # 一-开发之前1.学习大纲

技术目标:
- 强化编程思想:抽取封装能力,思考解决能力,框架设计能力
- 强化自我能力:解决问题能力,自学能力,知识存储记录能力
2.项目启动过程
运行就是打包的过程,把包放到模拟器中,此时设置一系列的内容,找到程序的入口(MainInterface),依次按照步骤来.
3.拖拽的控件
-
为什么用weak?
拖拽的控件所在的控制器都是被强引用所存在的,这个控制器被强引用,控制器的view被控制器强引用,view的控件被view强引用。所以外部拖线这件事其实就是我拿到这个控件,而不需要strong再引用了。 -
什么可以单击事件?
一般继承UIControl的都有单击事件 -
常见错误
- 属性连线出问题了(找不到属性或者属性多了):this class is not key value coding-compliant for the key xxxxxx
- 方法出问题了(找不到方法了): unrecoginzed selector sent to instance
二-UI
UIView
-
概念
屏幕上所有的UI元素
都叫做控件,所有控件都继承自UIView
,哪怕是UIControl
也是继承UIView
.这些控件的基本属性都放到了公共的UIView
中. -
常见属性/方法
superView
: 父控件subViews
: 子控件tag
: 标识frame
: 控件在父控件的位置尺寸(以父控件左上角为00)bounds
: 控件的位置尺寸(以自己左上角为00)- 所以一般只改尺寸,xy都是0 (一般layer层用)
center
: 控件中心(以父控件左上角为00)addSubView
: 添加removeFromsuperView
: 从父控件移除viewWithTag
:根据tag找到View- 尽量少使用tag,因为效率差,很乱
-
ViewDidLoad:
- 系统调用
- 控制器的View加载完毕调用
-
常用控件:
UILabel
用于显示文字
- 属性
numberofLines
: 文字行数,等于0的时候自动换行lineBreakMode
: 换行模式,是头部省略还是尾部省略还是中间省略font
: 字体
UIImageView
用于显示图片
-
属性
contentMode
: 填充模式带有scale的会缩放,不带的不会缩放,但是需要裁剪
typedef NS_ENUM(NSInteger, UIViewContentMode) {
UIViewContentModeScaleToFill,// 完全压缩或拉伸
UIViewContentModeScaleAspectFit, // 宽高比不变,不会变形
UIViewContentModeScaleAspectFill,// 宽高比不变,填充
UIViewContentModeRedraw,// 重新绘制
UIViewContentModeCenter,
UIViewContentModeTop,
UIViewContentModeBottom,
UIViewContentModeLeft,
UIViewContentModeRight,
UIViewContentModeTopLeft,
UIViewContentModeTopRight,
UIViewContentModeBottomLeft,
UIViewContentModeBottomRight,
};- `clipsToBounds`: 裁剪多余部分 - `animationImages`: 设置动画图片(放一个数组) - `animationRepeatCount`: 播放次数 - `animationDuration`: 播放时间
-
设置毛玻璃效果
由于UIToolBar
自带毛玻璃效果,所以可以给UIImageView增加UIToolBar的方法设置毛玻璃的简单效果。UIToolBar有一个barStyle
枚举可以设置毛玻璃样式,配合alpha透明度可以做设置。 -
序列帧动画
- 加载图片的两种方式:
UIImageNamed
- 就算指向他的指针被销毁,该资源也不会从内存销毁
- 放到Assets里的蹄片,只能imageNmaed加载,默认就有缓存的
- 所以是图片经常使用的时候,就把图片放Assets里,用imageNamed方法
imageWithContentsOfFile
-
指向他的指针被销毁,资源就从内存中小时
-
无法缓存,用于不经常使用的大批量的图片
-
从资源路径中找(SandBox)
-
手机里的缓存数据都是在沙盒中
-
图片放到Assets里就拿不到路径了,放到项目中可以。具体可以在查看沙盒的时候找Bundle包,里面有的图片资源就是项目中的,没有的就是放到Assets里面的
-
拿到图片路径:
self.imgView.image = [UIImage imageWithContentsOfFile:path];
```-
- 关于颜色:
- 白色的RGB都是255
- 黑色的RGB都是0
- 灰色的RGB都一样,靠近0就是深灰,靠近255就是浅灰
- 还有什么hex格式,argb格式等
UIButton
按钮既能显示文字又能显示图片,还能随时调整内部图片文字的位置
-
状态
normal
:一般hightlighted
:高亮disabled
:不可点击
-
内容图片和背景图片
- 内容图片不会随按钮变大而变化
- 背景图片会随着按钮变化而拉伸
-
属性
- 文字,图片等设置都需要用set方法,因为要区分状态
- 设置按钮字体:
button.titleLabel.font
- 获得按钮的文字,文字颜色,图片,背景图片 :
xxxForState
-
button不同于imageView和label,它是用addTarget监听的,但是其实都可以通过手势添加监听。
-
调整按钮内部子控件位置:默认的是图片左边,文字右边,但是开发是各种需求的
当自己算完按钮每部距离什么时候,可能出现文字显示不全问题,这时候只需要
sizeToFit
,但是有时会出现文字错位问题!因为这个方法可能会修改center,那么可以先sizetofit算,然后再修改center即可。方法1: UIButton有两个方法返回CGRect,可以自定义Button重写这两个方法
- (CGRect)titleRectForContentRect:(CGRect)contentRect;
- (CGRect)imageRectForContentRect:(CGRect)contentRect;
方法2: 其实既然都重写了,不如直接在`layoutSubViews`方法里面修改 方法3: 不需要重写一个新的Button,直接修改内边距 ```objc{ CGFloat labelWidth = modelButton.titleLabel.frame.size.width;
CGFloat imageWith = modelButton.imageView.frame.size.width;
modelButton.imageEdgeInsets = UIEdgeInsetsMake(0, labelWidth, 0, -labelWidth);
modelButton.titleEdgeInsets = UIEdgeInsetsMake(0, -imageWith, 0, imageWith);
```
```objc
/*
一、按钮的状态
1.UIControlStateNormal
1> 除开UIControlStateHighlighted、UIControlStateDisabled、UIControlStateSelected以外的其他情况,都是normal状态
2> 这种状态下的按钮【可以】接收点击事件
2.UIControlStateHighlighted
1> 【当按住按钮不松开】或者【highlighted = YES】时就能达到这种状态
2> 这种状态下的按钮【可以】接收点击事件
3.UIControlStateDisabled
1> 【button.enabled = NO】时就能达到这种状态
2> 这种状态下的按钮【无法】接收点击事件
4.UIControlStateSelected
1> 【button.selected = YES】时就能达到这种状态
2> 这种状态下的按钮【可以】接收点击事件
二、让按钮无法点击的2种方法
1> button.enabled = NO;
*【会】进入UIControlStateDisabled状态
2> button.userInteractionEnabled = NO;
*【不会】进入UIControlStateDisabled状态,继续保持当前状态
*/
```
-
图片拉伸
在一些聊天软件中,那些聊天气泡,美工都是准备一张图的,这时候这个Button的背景就是图片,但是Button很大的时候拉伸很难看。此时需要代码进行拉伸。这种图片一般都是要保护一部分不被拉伸
,例如四个角什么的- 代码拉伸
// 拉伸 UIImage *img = [UIImage imageNamed:@"1"]; // 方法一:返回一张受保护并且拉伸平铺的图片(edge是受保护区域),此时就留下了图片最中心1,1的像素点无线平铺 UIImage *resizableImage = [img resizableImageWithCapInsets:UIEdgeInsetsMake(img.size.height * 0.5, img.size.width * 0.5, img.size.height * 0.5 - 1, img.size.width * 0.5 - 1)]; // 方法二:左边和右边需要保护的区域 UIImage *strechImage = [img stretchableImageWithLeftCapWidth:img.size.width * 0.5 topCapHeight:img.size.height * 0.5] [self.btn setBackgroundImage:resizableImage forState:UIControlStateNormal];
其实这些可以写在分类里,返回一张受保护的图片(写一个UIImage分类)
-
直接拉伸
选择Slicing,会自动计算最合适的保护区域,要不自己拽。 -

懒加载
访问数据就是重写get方法,一般的控件,数组等都应该用懒加载
plist
开发中有一些小数据可以存储在plist中。例如很多页面用的数据,NSArray或者NSDictionary之类的数据。
- 创建
- 生成plist只有可能添加的是字典或者数组。一般是直接在plist中添加,或者代码写好字典数组,然后
writeToFile
到本地,然后拽到项目中
- 生成plist只有可能添加的是字典或者数组。一般是直接在plist中添加,或者代码写好字典数组,然后
- 解析
// 写入 NSArray *array = @[@"1",@"2",@{@"aaa":@"aa"}]; [array writeToFile:@"/Users/aixiaoxin/Desktop/study.plist" atomically:YES]; // 读取 NSArray *arr = [NSArray arrayWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"study" ofType:@"plist"]]; NSLog(@"%@",arr);
字典转模型
所谓模型
,就是专门用来存放数据的对象。
方法:
- 纯手工:自己创造模型,模型新增构造方法initWithDict等,然后外界赋值,内部实现.
-(instancetype)initWithDict:(NSDictionary *)dict { if (self == [super init]) { self.name = dict[@"name"]; self.icon = dict[@"icon"]; } return self;
}
```
-
kvc字典转模型:
-
YYModel:
自定义控件
- 代码创建
自定义UIView,一般在init初始化方法中添加自己的子控件(initWithFrame中写就可以,这个方法init也会调用),在layoutsubView中布局frame(这里能拿到self.frame),再引入模型接口,在模型的set方法中赋值模型,设置数据(或者提供接口方法直接设置)

- xib创建
- xib:是轻量级的,用来描述局部的UI界面 .xib打包后就是.nib。所以加载是通过nib加载的。
- 加载xibView:
// 读取xib
// last是最后一个数组,因为一个xib可能拽了很多view,其实一般情况下就一个,便于开发
UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"Play" owner:nil options:nil] lastObject];
xibView.frame = CGRectMake(100, 100, 300, 100);
[self.view addSubview:xibView];
```
- xib的绑定和连线
和代码类似,关联类,然后传入模型等。 - 注意
-
如果xib各处都用,那应该提供快速的创建方法
// 快速加载xib
-
+(instancetype)play {
return [[[NSBundle mainBundle] loadNibNamed:@"Play" owner:nil options:nil] lastObject];
}
- 如果xib的.m中,还想用代码创建子控件,此时和纯代码是不一样的,纯代码是init和initWithFrame方法。xib是
initWithCoder:
,在这里创建子控件就可以了。 - 如果子控件是从xib中创建的
initWithCoder
中创建的,此时控件是处于为唤醒状态,此时要用到awakeFromNib
方法,在这里可以添加xib中创建的子控件的子控件。其实如果想在xib中再加控件,统一这个方法就行。 - xib创建的是不可以用
allocinit
直接加载的。
- 加载原理
xib转化成xml代码,里面有控件所有的设置。
UIView渐变动画
方法

KVC
-
KVC 可以取值/赋值/字典转模型等。
-
赋值:
setValueForKey
:可以进行自动类型转换,如果用KVC和网络搭建,非常方便。一般一个对象KVC赋值属性值是可行的。而setValueForKeyPath
: keyPath是可以嵌套对象的,假如赋值一个人的狗属性的名字(点语法点出来),就可以person setValue: @"旺财 forKeyPath:@"dog.name"
。其实后者包含前者全部功能,所以直接用setValueForKeyPath
就行。 -
改变类的私有变量: 一般私有类都是在
.m的{}中
,此时是点不出来的,这意味着我定义了一个_age
,而我直接可以setValueForKeyPath:@"_age"
来修改这个值(写@"age"都行,他会去找下划线和不带下划线的)。 -
字典转模型:
-(instancetype)initWithDict:(NSDictionary *)dict { if (self == [super init]) { // 废弃
-
// self.name = dict[@"name"];
// self.icon = dict[@"icon"];
// KVC字典转模型
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
-(void)setValue:(id)value forUndefinedKey:(NSString *)key {
}```
KVC字典转模型的问题:如果有模型嵌套,就会出问题,因为是暴力的键值对赋值。当然,此时就会用到框架(YYModel,MJExtension)
- 取值:
valueForKeyPath
方法,开发一般不怎么用,因为点语法就行了 - 运行时的时候KVC非常强大
KVO
Key Value Observing(键值监听)
当某个对象的属性发生改变时进行监听。假如监听一个对象的名字改变,scrollView的contentOffSet改变等。
如果一个类用KVO监听,苹果会自动为这个类生成一个子类(查看isa指针的时候就是NSKVONotifying_XXX类型),所以用的多了性能很差
监听属性,就要给属性的对象增加观察者
MYModel *model = [MYModel new];
// 增加监听观察后,那么应该有个方法来进行 监听完成后的作为
[model addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
model.name = @"改变前";
model.name = @"改变后";
[model removeObserver:self forKeyPath:@"name"];
/**
监听属性值改变后
@param keyPath 要改变的属性
@param object 要改变的属性所属的对象
@param change 改变的内容
@param context 上下文
*/
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@==%@===%@",keyPath,object,change);
}
结果:
2017-03-20 18:10:22.341 button[6755:1544348] name==<MYModel: 0x60800022d7e0>==={
kind = 1;
new = "\U6539\U53d8\U524d";
}
2017-03-20 18:13:10.575 button[6785:1563494] name==<MYModel: 0x618000030500>==={
kind = 1;
new = "\U6539\U53d8\U540e";
}
UIScrollView
设备的屏幕大小是有限的,普通的View不具备滚动功能,UIScrollView可以通过滚动展示大量内容。
-
基本使用
- 将需要展示的内容添加到UIScrollViez中
- 如果想要滚动,设置
contentSize
属性,也就是滚动范围(可以远大于本体scrollView的size),所以当contentSize尺寸减去本体size是负数的话就无法滚动了,怎么也要大于本体尺寸 - 默认scroll就设置了
clipsToBounds = YES
,也就是内容超出边框后就隐藏 - 默认scroll设置了弹簧效果
- 无法滚动?
- 没有设置contentSize
scrollView.scrollEnabled
设置为NO(只是不能滚动)scrollView.userInteraction
设置为NO(失去任何交互能力)- scrollView.subViews自带两个滚动条
- 滚动就是修改contentOffset,此时可以用set方法加一个动画
-
常见属性
bounces
默认YES,弹簧效果alwaysBouncesHorizontal
默认NO,和下一个方法一样,设置YES的时候可以让没有contentsize的scrollView滚动(只是一个弹簧效果)alwaysBouncesVertical
默认NO,这两个值就算设置YES也没区别,因为这时候已经设置了contentSize。当没有设置contentSize的时候,如果设置这两个是YES就可以滚动了,就像网络图片还没下载完成,只有下拉才能下载,然后加载contentsize,这时候就需要用到这两个属性了showHorizontalScrollIndicator
展示水平滚动条showVerticalScrollIndicator
展示垂直滚动条
public var contentOffset: CGPoint // 滚动视图偏移量 public var contentSize: CGSize // 滚动视图的内容大小 public var contentInset: UIEdgeInsets // scrollview的contentview的顶点相对于scrollview的位置 public var directionalLockEnabled: Bool // default NO. if YES, try to lock vertical or horizontal scrolling while dragging public var bounces: Bool // 是否开启回弹效果 public var alwaysBounceVertical: Bool // 是否始终水平回弹 public var alwaysBounceHorizontal: Bool // 是否始终垂直回弹 public var pagingEnabled: Bool // 是否分页(开启后滑动有自动定位功能) public var scrollEnabled: Bool // 是否可以滚动 public var showsHorizontalScrollIndicator: Bool // 是否显示水平滚动条 public var showsVerticalScrollIndicator: Bool // 是否显示垂直滚动条 public var indicatorStyle: UIScrollViewIndicatorStyle // 滚动条的样式(黑色/白色/默认) public var scrollsToTop: Bool // 默认YES,单击上方状态栏(服务商和电池那个地方),会自动回到顶端
-
重要属性
contentOffSet
,是一个CGPoint属性,是一个偏移量- 可以控制内容滚动的位置
- 可以得知内容滚动的位置(get)
contentInset
,是UIEdgeInset属性,是内边距,分别是上左下右,滚动的时候会多出来一块儿不显示的部分就是内边距效果,会变相的增加额外的滚动距离
-
三者区别
 -
代理(监听行为)
在UIScrollView滚动时候就会告诉代理,代理发送一系列消息。
public protocol UIScrollViewDelegate : NSObjectProtocol {
@available(iOS 2.0, *)
optional public func scrollViewDidScroll(scrollView: UIScrollView) // 滚动的时候
@available(iOS 3.2, *)
optional public func scrollViewDidZoom(scrollView: UIScrollView) // 滚动视图已经缩放时候
@available(iOS 2.0, *)
optional public func scrollViewWillBeginDragging(scrollView: UIScrollView) // 将要开始拖动视图调用
@available(iOS 5.0, *)
optional public func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) // 将要结束拖拽视图调用
@available(iOS 2.0, *)
optional public func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) // 已经结束拖拽视图
@available(iOS 2.0, *)
optional public func scrollViewWillBeginDecelerating(scrollView: UIScrollView) // 即将减速的时候
@available(iOS 2.0, *)
optional public func scrollViewDidEndDecelerating(scrollView: UIScrollView) // 减速停止的时候
@available(iOS 2.0, *)
optional public func scrollViewDidEndScrollingAnimation(scrollView: UIScrollView) // 滚动动画结束时候
@available(iOS 2.0, *)
optional public func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? // 设置进行缩放的子视图(谁缩放)
@available(iOS 3.2, *)
optional public func scrollViewWillBeginZooming(scrollView: UIScrollView, withView view: UIView?) // 将要进行缩放时候
@available(iOS 2.0, *)
optional public func scrollViewDidEndZooming(scrollView: UIScrollView, withView view: UIView?, atScale scale: CGFloat) // 完成缩放的时候
@available(iOS 2.0, *)
optional public func scrollViewShouldScrollToTop(scrollView: UIScrollView) -> Bool // return a yes if you want to scroll to the top. if not defined, assumes YES
@available(iOS 2.0, *)
optional public func scrollViewDidScrollToTop(scrollView: UIScrollView) // 点击状态栏回到顶部,仅针对可上下滚动的scrollview 有效}
注意: 任何OC对象都可以成为代理,哪怕你SCrollView滚动,让一条狗遵守ScrollViewDelegate并且实现方法成为代理,那再狗的.m中就可以监听到scrollView滚动
6.代理(监听行为)
scrollView可以通过手势完成内容缩放放大
1. 用代理找到要缩放的scrollViewviewForZoomingInScrollView
2. 设置缩放比例:
```
scrollView.maximumZoomScale = 2.0
scrollView.minimumZoomScale = 0.5
scrollView.bouncesZoom = true // 缩放属性是否回弹
```
7.分页功能
- 就像苹果手机开机的分页页面,当你滚动的时候,左边比较多右边比较少,那么就自动滚动到左边,这就是分页功能。很多App的分页滚动功能,也是依据分页来实现的。
其实就是一个属性pageingEnable
开启后就可以了。系统分页的判断是按照scrollView的大小来分的。如果要添加分页控件再自行添加UIpageControl
。
- 很多分页的点都用得图片,这样看起来很好看,但是其实观察UIpageControl
头文件是没有图片选择的,它隐藏在.m的私有拓展中有一个属性_currentPageImage
,_otherPageImage
,此时可以通过KVC来转化成自己喜欢的图片
- 分页定时器:每隔一定的时间,希望scrollView做一些事情,去滚动,要用到定时器NSTimer
。在用户即将拖拽scrollView的时候停止定时器,不在拖拽的时候开启定时器。
1.一个定时器要不永久开启,只要一停止就自动销毁了,只能创建一个新的
2.程序一启动会默认开启一条线程,是主线程,主线程显示刷新UI界面,处理用户交互界面,所以主页面在进行别的操作的时候,定时器可能会卡住。所以要把`NSTimer`添加到运行循环(修改定时器在runloop的模式,目的是不管主线程在做什么,都会分配一定时间处理事情)。
// 添加到运行循环
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
常见控件监听
随便一个控件,点开头文件,如果继承自UIControl就都可以通过addTarget
来监听,有些特殊的提供delegate
就可以通过代理来监听。像TextField
这类比较特殊,它既有代理也继承UIControl,此时两者都可以监听开始编辑,文字改变,结束编辑
等状态的。
屏幕适配
-
程序员只需关注的是点,而不是像素,一个点所容纳的像素越多,越清晰。在iphone4S时代,屏幕尺寸固定,那么固定值算就可以适配。在5和5s时代尺寸变化了,此时还无法用autolayout(因为还要适配ios5),此时用的是
AutoresizingMask
,在iphone6时代开始用AutoLayOut
 -
Autoresizing
- Autoresizing和AutoLayout是不兼容的,想要使用现在需要去掉AutoLayout。我只在xib中用过,就是六根线。外面四根线是和父亲的关系,里面两根线是子类的关系(选择了会跟随父控件伸缩)
- 代码实现就是有个
AutoResizingMask
枚举,可以选择是上左下右怎么适配 - 只能解决子控件和父控件之间的关系
- AutoLayout比Autoresizing强得多
-
AutoLayout
-
ios6 开始引用,不过真正从xcode5开始大量使用,也是现在的主流
-
可以解决任何控件之间的关系
-
用自动布局,就不要在设置
frame
-
利用
约束
和参照
完成布局 -
左对齐/右对齐/顶部对齐/底部对齐
 -
中心点x和父控件一样/中心点y和父控件一样
 -
两个控件中心点x一样/中心点y一样
 -
在做pad留言板的时候,因为用的是autoResizing,对于UILabel总是文字居中显示,这时候上下端会出现很多空缺,解决办法只能计算文字到底多高,在固定高度,而用了AutoLayout就可以完美解决这个问题。UILabel只需要给一个位置就不报错,尺寸会自动根据文字内容而计算宽高,此时应该设置下最大宽度(不是真实宽度)
Relation->LessThan
,告诉UILabel什么时候开始换行。那么固定位置和宽度,UILabel就会自动剔除上下两端的空格了。 -
让父控件的高度跟随子控件伸缩:例如很多表格类型的app,每个cell高度都不同,因为里面的子控件大相径庭。 其实这时候cell 就不能直接设置高度了,而是要设置自己底部和会影响到你的子类的关系,例如你的子类最后一个控件的底永远和父类的底差10个点,那么不管子类怎么变高,父容器也会跟着变。
-
-
约束优先级:两个互相冲突的约束如果修改优先级,那么优先级较高的生效,当优先级较高的失效了或者删除了,优先级较低的才会生效。例如三个方块并排放各自距离20,当中间的删除后,让第三个自动到中间位置,这时候就用到优先级了。

代码实现AutoLayout
-
禁止autoresizing
self.myView.translatesAutoresizingMaskIntoConstraints = NO;
-
创建
NSLayoutconstraint
类创建约束关系- 宽高什么的添加到自己上
- 和父控件之间控制位置的约束添加到父控件上
- 两个平等的控件(都是儿子)之间的约束添加到父控件上
-
VFL可视化语言
-
修改约束:
// 修改约束 self.myWidth.constant = 25; [UIView animateWithDuration:2.0 animations:^{ // 需要强制更新 [self.view layoutIfNeeded]; }];
Masonry
- 目前最流行的第三方框架
- [Masonry/SnipKit](https://github.com/SnapKit/Masonry)
- 两个pch上面的宏定义可以让`mas_`全部省略
- `makeConstraints`是添加新的约束
- `updateConstraints`是更新约束
- `remakeConstraints`是删除之前所有约束,然后添加新的约束
UITableView
基本
TableView
分为Plain
和Group
两种style类型,这个是只读的,意味着只有在创建tableView的时候可以设置。indexPath
分为section
和row
两个属性,分别是组和行,根据这个可以确定唯一的一行cell.accessoryView
可以设置系统cell右侧的那个按钮状态样式- UITableView内部自动封装了一套复用机制。会让空闲的cell进入可重用线程池,当有新的cell出现会先去线程池中找有没有可复用的,没有才会创建。假如有100组数据,需要100个cell,但是手机上每屏只能放下10个,其实这时候只需创建11个cell就够用了。每一个数据模型就是一个cell。通过数据源方法来对每个cell进行数据设置。通过代理方法设置关于tableView的头,尾等视图设置
UITableView常见属性
rowHeight
:每一行cell高度sectionHeader(Footer)Height
:每一组头尾高度separateStyle
:分割线样式(删除分割线separateNone)tableHeader(footer)View
:整个tableView的头尾部,上面的是每一组的头尾,这个是整个的(插播个广告什么的)
Cell常见属性
accessoryView
--accessoryType
:都是设置右边控件的(View优先级更大)selectedStyle
:选中样式(有Blur,Gray,None等,ios6之前有效果,7之后全部都是灰色)backgroundView
:cell背景可以是好看的图片,selectedBackgroundView
:选中的背景图片contentView
:cell上添加的东西都是添加在contentView上的。因为有时候cell删除,滑动,整个cell都向左移动了,此时如果移动cell非常麻烦,但是移动contentView就方便多了- 自带cell中的imageView,textLabel等属性都是延迟加载的,是懒加载的,用到才会加载
数据源方法
- 提供几组,几行,每一个cell什么样子,数据是什么
代理方法
- 提供cell单机事件
- 提供cell的头/尾视图设置,头/尾高度设置,设置了这些,前面属性设置的高度就失效了。
- 提供了每一行的高度,rowHeight是有局限性的,这个可以让每个cell高度不同
UITableViewController
- 每一个控制器都有View,在UITableViewController中:
self.view
和self.tableView
是一个对象。 - 当数据比较少,还想去掉没有数据的分割线时:可以设置
self.tableView.tableFooterView = [UIView new]
性能优化
- cell标准写法.注册方法
自定义等高的cell
- 无论是系统自带cell还是自定义创建注册的cell,这个
[cell class]
都是通过cell alloc initWithStyle
方法实现的
-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
_name = [UILabel new];
[self.contentView addSubview:_name];
}
return self;
}
- 至于cell内部自然要用约束来控制。当需要用frame计算的时候一般会写在
layoutifNeed
里面,因为这里才能拿到self的frame.但是用约束计算的话直接写在cell alloc initWithStyle
方法里就行(写在addSubView以后),因为这是根据父控件的约束,不管父控件如何 - xib创建cell时候注册nib
[self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([MyTableViewCell class]) bundle:nil] forCellReuseIdentifier:@"cell"];
- 一个tableView显示不同的cell:由于
cellforRowAtIndexpath
方法返回一个cell,所以可以根据indexpath改变cell。说白了就是注册N个cell和reusedId,根据indexPath来dequeue不同的cell。广告cell就这么来的 - 自定义分割线:添加高度为1的UIView
静态cell和动态cell
- 动态cell是数据.数量.高度等等都是动态获取的
- 静态cell
Static Cells
是写死的,在sb中/xib中直接修改cell类型然后绘制出来的。可以做一些设置界面,然后通过代理监听 - 静态动态只是创建区别,功能都是实现cell
- 静态cell 也是可以自定义的,也可以画画然后关联类,然后从waakefromNib读取关联的cell
字典转模型
-
字典转模型无非就是遍历数组,然后拿到数组里的每一个字典,然后给模型用KVC赋值,这是一个重复的过程,所以要省事,用插件。而且有的模型很复杂,嵌套模型很多,这么写起来更麻烦。
-
MJExtension
GitHubMJExtensionmodel-objetArrayWith...
-
YYModel
GitHubYYModelmodel-yy_modelWithJson..
自定义不等高的cell
- (计算不等高UILabel)如果frame计算,需要注意UILabel内容不一样的时候,要想计算它的高度,需要
UILabel *lab = [[UILabel alloc] init];
// 100是固定宽度 后者是最大宽度,不限制
CGSize size = CGSizeMake(100, MAXFLOAT);
// 计算出高度
CGFloat height = [lab.text sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:size].height;
- 一些app都是frame计算的,假如需求是frame来计算不等高度的cell。控制器在
heightForRowAtIndexpath
方法中可以拿到模型方便计算,但是在这里计算要调用无数次,性能很差,这时候还必须先返回一个高度给cell,所以一开始觉得应该给数据模型增加所有的cell子控件frame 和总高度frame
属性,在总高度属性中,利用懒加载来计算 高度/各个frame,然后外界heightForRowAtIndexpath
直接设置高度。但是这么做,重用刷新就会出现问题,因为刷新的时候高度大于0,但是不是你想要的高度。真正做法是刷新的时候,拿到json的时候,字典转模型的时候,创建一个新的frame模型,里面存放着高度和各个cell子控件的frame,每次刷新数据就绑定好,然后heightForRowAtIndexpath
直接拿frame模型。 - 打印各种方法执行顺序,假如10条数据在页面,其实是先执行10次
heightForRowAtIndexpath
才会执行cellForRowAtIndexPath
,说白了就是cell展示前,所有高度早就计算好了。 - masonry每次都会重新计算,在一些很复杂很复杂的cell控件中,其实frame算性能比较好。(一哥们亲身经历)
- cell内部计算的时候也不一定写在
layoutsubVies
,写在模型的set方法也可以,因为这时候是肯定有frame的 self.tableView.estimatedRowHeight
:预估高度,会优化性能,让heightForRowAtIndexpath
执行次数少很多。而且没有估算的时候,是先执行heightForRowAtIndexpath
后执行cellForRowAtIndexPath
。当写了估算行高后,反过来了,因为已经有一个默认高度了。
数据刷新
- 全局刷新:
reloadData
:就是重新调用数据源方法 - 局部刷新:
reloadRowsAtIndexPath
:适合模型数量不变 - 添加刷新:
insertRowsAtIndexPath
- 删除刷新:
deleteRowsAtIndexPath
左滑删除
- 实现代理方法就可以出现滑动删除
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
// 删除模型,动画单行刷新
[self.dataSource removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
}
-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath{
return @"删除";
}
- 左滑出现多个按钮(例如微信,滑动出现删除/关注),此时只需要一个新方法,如果新增了这个方法,上面的return 删除监听就实效了。
-(NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewRowAction *action1 = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"关注" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
// 关注
}];
action1.backgroundColor = [UIColor orangeColor];
UITableViewRowAction *action2 = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"删除" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
// 删除模型,动画单行刷新
[self.dataSource removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationTop];
}];
action2.backgroundColor = [UIColor redColor];
return @[action1,action2];
}
如果想点击后退出编辑模式,tableView.editing = no
- 编辑模式
tableView.editing = yes
时,左侧就会出现删除按钮,其实就是上方的那个编辑模式。而且编辑模式有三种
// 设置cell的编辑模式,有NONE,插入,删除三种
override func tableView(tableView: UITableView, editingStyleForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCellEditingStyle {
return UITableViewCellEditingStyle.Insert
}
- 批量删除
有俩属性,编辑模式下就可以批量勾选等等
UICollectionView
-
流水布局
UICollectionViewFlowLayout
:说白了就是当里面内容变宽了变窄了这个那个了,但是整个布局会改变。就像手机界面,删除一个app,后面的会滑上去。 -
self.collrctionView 不等于 self.view
,不像tableView,collectionView是添加到view上的。 -
flowLayout
布局的时候可以设置item的大小,行间距,item间距,滚动方向,每一组内边距(sectionInset), -
self.collectionView
可以设置分页(pagingEnable),弹簧效果(bounces),隐藏滚动条(showH..)等操作,其实很多都是UIScrollView的属性 -
相册照片浏览器
// 必须要有布局,自带循环机制,每一个item就是每一个cell
// 必须要注册cell,并且必须要自定义cell,因为collectionView的默认cell什么都没有!
// 调整尺寸什么的都是布局的事,cell自定义后通过代理来完成注册,数量,模型内容
// 当要做的内容例如item大小不一的时候,系统无法满足,此时要自定义流水布局
#import "FlowLayout.h"
/*
自定义布局:只要了解5个方法
- (void)prepareLayout;
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect;
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds;
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity; // return a point at which to rest after scrolling - for layouts that want snap-to-point scrolling behavior
- (CGSize)collectionViewContentSize;
*/
@implementation FlowLayout
/*
UICollectionViewLayoutAttributes:确定cell的尺寸
一个UICollectionViewLayoutAttributes对象就对应一个cell
拿到UICollectionViewLayoutAttributes相当于拿到cell
*/
// 重写它方法,扩展功能
// 什么时候调用:collectionView第一次布局,collectionView刷新的时候也会调用
// 作用:计算cell的布局,条件:cell的位置是固定不变
// - (void)prepareLayout
//{
// [super prepareLayout];
//
// NSLog(@"%s",__func__);
//
//}
// 作用:指定一段区域给你这段区域内cell的尺寸(就是这个rect,一开始系统固定的,之后超过这个值就新增)
// 可以一次性返回所有cell尺寸,也可以每隔一个距离返回cell
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
// NSLog(@"%s",__func__);
// 这时候就返回所有了
NSArray *attrs = [super layoutAttributesForElementsInRect:CGRectMake(0, 0, MAXFLOAT, MAXFLOAT)];
return attrs;
}
// 什么时候调用:用户手指一松开就会调用
// 作用:确定最终偏移量
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity{
// 拖动比较快 最终偏移量 不等于 手指离开时偏移量
// 最终偏移量
CGPoint targetP = [super targetContentOffsetForProposedContentOffset:proposedContentOffset withScrollingVelocity:velocity];
// 获取collectionView偏移量
NSLog(@"%@ %@",NSStringFromCGPoint(targetP),NSStringFromCGPoint(self.collectionView.contentOffset));
return CGPointZero;
}
// Invalidate:刷新
// 在滚动的时候是否允许刷新布局
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
return YES;
}
// 计算collectionView滚动范围
//- (CGSize)collectionViewContentSize{
// return [super collectionViewContentSize];
//}
MVC
- M是模型,V是视图,C是控制器。View显示什么取决于M,控制器负责把数据模型给View。MVC因为模型的存在解决cell循环利用表格出bug了。只需要记住,不要随意直接修改cell上的值,要改,就改模型。
通知
- 每一个应用程序都有一个
NSNotificationCenter
实例,负责不同对象之间消息传递。 - 我做一件事要让外界知道,我就发布通知(post),谁来监听谁就(post),这个过程是一对多的,我广播发布,谁注册了监听就把通知给谁。
NSNotification
一个通知有三个属性- name:通知名字
- object:通知发布者(id)
- userInfo:传递内容信息(字典)
// 发布通知(object是谁发送)
[[NSNotificationCenter defaultCenter] postNotificationName:@"RemoveNotification" object:self userInfo:@{@"money":self.wine.money}];
// 监听通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(remove:) name:@"RemoveNotification" object:nil];
- (void)remove:(NSNotification*)noti {
NSLog(@"%@---%@---%@",noti.name,noti.object,noti.userInfo);
int money = [[noti.userInfo objectForKey:@"money"] intValue];
self.totalPriceLabel.text = [NSString stringWithFormat:@"%d",[self.totalPriceLabel.text intValue]-money];
}
object谁发布的name什么通知。可以不写name,那就是监听这个object的所有通知。
- 移除通知
remove
,当对象被销毁,就删除 - 通知是有顺序的,要先监听add,再发出post,否则兼听不到
- add通知的时候还有一个方法会让填写一个队列和block,队列不需要的时候写nil,这个也是可以用的。当队列需要,填写的队列就决定block在哪里执行。而且这个通知的移除不是一般的removeself!因为是系统观察的,方法你都没写!
@property (nonatomic, weak) id observe;
@end
@implementation ViewController
- (void)test
{
// 方式一:
// Observer:观察者
// selector:只要一监听到通知,就会调用观察者这个方法
// Name:通知名称
// object:谁发出的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reciveNote) name:@"note" object:nil];
}
- (void)test2
{
// Name:通知名称
// object:谁发出的通知
// queue:决定block在哪个线程执行,nil:在发布通知的线程中执行
// [NSOperationQueue mainQueue]:一般都是使用主队列
// usingBlock:只要监听到通知,就会执行这个block
// 注意:一定要记得移除
_observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"note" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
// 只要监听到通知 就会调用
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"%@",self);
}];
}
// 一个对象即将销毁就会调用
- (void)dealloc
{
// 移除通知
[[NSNotificationCenter defaultCenter] removeObserver:_observe];
}
// 异步:监听通知 主线程:发出通知 接收通知代码在主线程
// 主线程:监听通知 异步:发出通知 接收通知代码在异步
// 注意:在接收通知代码中 可以加上主队列任务
UIDevice通知
UIDevice
提供一个单例UIDevice-currentDevice
,代表设备,可以获取一些设备信息,例如电池电量,电池状态,设备类型,设备系统等等。- 以前的代码会判断系统版本
sytemVersion
来进行代码不同的写法,可以用于系统适配 UIDevice
会不间断的发布一些通知,例如- 设备旋转
- 电池状态改变
- 电池电量
- 近距离传感器(例如接电话后屏幕变黑)
键盘通知
-
键盘也有一些系统的通知,来判断键盘状态

UI进阶(多控制器就是进阶)
基础
启动页LaunchScreen
- 可以用自带的sb设置,也可以用
Assset
里面的启动页设置。 - sb设置的底层实现:把LauchScreen里的内容生成了一张图片储存在沙盒中
info.plist
- 作用:设置应用程序配置信息。最外层节点是一个字典
Bundle name
:应用程序名称(项目名称一般不是中文的,因为要打包,但是app很多中文名字,这时候在这里改这个选项是中文,例如财务审批
)Bundle identifier
:应用程序唯一标识(上传appStore的时候必须有这个,做推送的时候也必须有这个(推送是把消息发送给苹果服务器,服务器根据你手机的Bundle identifier来找到你这个app))Bundle version string , short
:版本号,软件版本号Bundle version
:应用程序打包版本号
PCH
- 作用:
- 自定义Log
// 定义debugFKLog
ifdef DEBUG
define FKLog(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);
else
define FKLLog(...)
endif
```
- 存储公共宏
- 存储公共头文件(自定义的分类等)
- 从xcode6开始苹果不推荐PCH,就是不编译了。现在需要创建pch后,在
Build Settings - 搜索prefix - 寻找 LLVM 7.0 Lauguage - pch改YES - 填写pch路径fullPath
- 原理:app编译时候,把pch所有内容拷贝到所有文件中(如果是混合开发,有c文件的话就会报错了,因为c不识别pch里面你的UI宏,这时候还给在pch做设置)
UIApplication
UIApplication
是应用程序的象征。每一个app只有一个,是单例。sharedApplication
- 一个ios程序启动后创建的第一个对象就是
UIApplication
,利用它而已做一些应用层设置。例如applicationIconBadgeNumber
:设置app右上角红色提醒数字
-
设置图标
-
注册通知
-
设置图标属性
/**
取得用户授权
*/
- (void)badgeNotification:(UIApplication *)application {
// 取得用户授权显示通知[BadgeNumberUIApplication层]
id typeBadge = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeBadge categories:nil];
id typeSound = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeSound categories:nil];
id typeAlert = [UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert categories:nil];
[application registerUserNotificationSettings:typeBadge];
[application registerUserNotificationSettings:typeSound];
[application registerUserNotificationSettings:typeAlert];
}
// ios 8后需要授权,目前只会写一种授权方式,iOS10有新方法
NSInteger times = arc4random() % 100;
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:times];
- `networkActivituIndicatorVisible`:设置联网小菊花的可见性(Bool)一般写在网络方法中
- `statusBarHidden`:设置状态栏隐藏
```
/**
想要使用 默认要在info.plist新增最后一个键值对,选no,这时候控制器设置无效
全局设置状态栏白色
全局设置导航栏背景mainColor
全局设置导航栏标题文字颜色白色,微软雅黑
全局设置导航栏Item颜色 白色
*/
-
(void)systemSetting {
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
[[UINavigationBar appearance] setBarTintColor:mainColor];
[[UINavigationBar appearance] setTintColor:[UIColor whiteColor]];
[[UINavigationBar appearance] setTitleTextAttributes: [NSDictionary dictionaryWithObjectsAndKeys: [UIColor whiteColor], NSForegroundColorAttributeName, [UIFont fontWithName:@"MicrosoftYaHei" size:18.0], NSFontAttributeName, nil]];
}
// 不是全局 交给每个控制器独立控制,默认是交给控制器的
-(UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
-(BOOL)prefersStatusBarHidden {
return YES;
}
```
- `keyWindow`:应用程序主窗口
- 强大的`openURL`方法 `[NSURL URLWithString:]`-->`根据协议头tel: sms:http:等来区分`
- 可以打电话
- 发短信
- 发邮件
- 打开网络资源
- 打开其他app
UIApplication
代理:
app容易受到干扰,打电话这个那个会切出去。当这些系统干扰出现的时候,app需要做一些事。例如保存数据等。此时UIApplication
会有代理监听(也就是AppDelegate里面那一堆方法)- 系统来电等
- 程序启动关闭等
- 收到内存警告
应用程序启动原理
- 执行main函数->执行UIApplicationMain->创建UIApplication对象->执行代理
- 开启运行循环(主运行循环),保证app不退出(死循环)
- 加载info.plist(一些基本属性,然后看看有没有main.sb,有就加载,没有就创建)
- 启动完毕,进入appDelegate代理
UIWindow
UIWindow
是一个特殊的UIView,ios启动完毕后,启动的第一个视图控件就是UIWindow
,然后创建控制器View,把控制器view加到UIWindow
上。所以说没有UIWindow
就看不到任何UI界面的。
- 纯代码写app的时候window是要手动创建的
self.window makeKeyAndVisible
的作用是显示,把跟控制器的view加载到我的window上。- 其实键盘,状态栏也是一个
UIWindow
- 从ios9后,如果添加了多个窗口,例如又创建了UIWindow,makeKey了,状态栏就没了,消失了,解决办法就是让状态栏给应用程序管理。
windowLevel
:窗口层级(不同窗口谁前谁后可以分层级)Alert > StatusBar > Normal
控制器如何加载View?
-
控制器利用
loadView
方法,专门创建控制器View。当控制器的View第一次使用的时候调用此方法。那么loadView
做了什么?1. 判断如果控制器是sb加载的,就找sb的View设置为自己当前View 2. 如果是xib加载,就把xibView设置自己View 3. 如果都不是,就会创建一个空白的View
一旦重写了这个方法,那么这个View就需要自己创建了,这样可以自定义控制器最一开始的View。
-
控制器View都是懒加载的
-
如果一个控件是透明的,那么它不能接收事件。一开始创建的控制器不是透明的,只是颜色是透明的,是可以接收事件的。
UITexiField
self.inputView
:修改文本框弹出键盘类型
UIPickerView/UIDatePicker
PickerView
代理选择设置内容。一般配合UITextField
的键盘类型设置为是自定义的PickerView
DatePicker
datepickerMode
: 日期模式可以选择想要的年月日
locale
:设置区域为localeWithLocaleId:@"zh"
中国
监听日期改变用addTarget-Valuechange
项目中这些选项都有,例如左边国家右边国旗的界面, 年月日时间的界面, 选择城市(市区)的界面。国家国旗就是一个pickerView,自定义View弄上imageView和label,用代理完成。年月日就如上,选择城市~~
String
字符串 用copy
目的是外界修改了,不会影响到自己。其实,一个字符串属性被赋值的时候,self.name = @"xxx"
这个赋值的值,要不就是写死的,要不就是网络获取的,但是一定都是NSString
类型。而copy属性呢,会在set方法的时候做一次copy,copy会做一次判断,判断外界给的值得类型是可变的还是不可变的,如果是不可变的,直接赋值,如果是可变的,就copy一份赋值。所以其实app的字符串都是不可变的,所以写strong
也是可以的,甚至会少一次判断,性能上会高一些。财务审批NSString非常多
,这一点用strong
是可行的。
---恢复内容结束---
![](http://images2015.cnblogs.com/blog/1014815/201707/1014815-20170705105216878-1240811650.jpg)