编写iOS应用UI的方式大概有两种,一种是Storyboard/Xib,另一种是手写代码。采用Storyboard/Xib方式组织UI,由于提供可视化的特性,只要从UI库中拖动UI控件,便可以显示结果,极大地提高开发速度。但面临一个问题就是多人协作开发,由于所有的UI都放在同一个Storyboard文件中,使用Git/SVN合并代码就会出现冲突。多人协作开发还不是主要问题,有人提出可以创建多个Storyboard来分开UI编写,而Storyboard/Xib最主要问题是代码复用性比较差。所以有些人就选择手写UI代码,这样不仅可以解决多人协作开发问题,而且通过自定义控件在多个View使用。但每次手写UI代码后都要编译、构建和运行,最后在模拟器显示,这样会拖慢开发速度。如果每次修改UI控件后,保存修改便实时在模拟器显示修改后结果,就可以极大的提高编写UI的速度。
Live Change.gif
Auto Layout
Auto Layout是什么
Auto Layout是一个基于constraint(约束)的布局系统,它根据UI元素之间约束关系来调整UI元素的位置和大小。
Auto Layout解决什么问题
- 更容易适配不同分辨率设备的屏幕(iPhone 6 Plus, iPhone 6, iPhone 5s/5, iPhone 4s/4)
- 当设备旋转时不需要做额外处理
- 使用constraint来描述布局逻辑,更利于理解和清晰
如何使用Auto Layout
Auto Layout中约束的类对应是NSLayoutConstraint, 而创建NSLayoutConstraint对象主要有两种方式,第一种是
1
2
3
4
5
6
7
|
+
(
id
)
constraintWithItem
:
(
id
)
view1
attribute
:
(
NSLayoutAttribute
)
attribute1
relatedBy
:
(
NSLayoutRelation
)
relation
toItem
:
(
id
)
view2
attribute
:
(
NSLayoutAttribute
)
attribute2
multiplier
:
(
CGFloat
)
multiplier
constant
:
(
CGFloat
)
constant
;
|
上面方法主要意思是,某个view1的attribute1等于(小于或等于/大于或等于)某个view2的attribute2的multiplier倍加上constant。而attribute主要由表示位置(上/下/左/右)和大小(宽/高)的以下几个值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
typedef
enum
: NSInteger
{
NSLayoutAttributeLeft
=
1
,
NSLayoutAttributeRight
,
NSLayoutAttributeTop
,
NSLayoutAttributeBottom
,
NSLayoutAttributeLeading
,
NSLayoutAttributeTrailing
,
NSLayoutAttributeWidth
,
NSLayoutAttributeHeight
,
NSLayoutAttributeCenterX
,
NSLayoutAttributeCenterY
,
NSLayoutAttributeBaseline
,
NSLayoutAttributeNotAnAttribute
=
0
}
NSLayoutAttribute
;
|
简化一下,使用公式可以表达为:
1
|
view1
.
attribute1
=
view2
.
attribute2
*
multiplier
+
constant
|
第二种方式是:
1
2
3
4
|
+
(
NSArray
*
)
constraintsWithVisualFormat
:
(
NSString
*
)
format
options
:
(
NSLayoutFormatOptions
)
opts
metrics
:
(
NSDictionary
*
)
metrics
views
:
(
NSDictionary
*
)
views
;
|
这种方式主要是采用Visual Format Language(可视化格式语言)来描述约束布局,虽然语法比较简洁,但是可读性比较差和容易出错。
Auto Layout存在问题
虽然Auto Layout在布局view方面是非常强大和灵活,但是创建constraint的语法过于繁杂,引用Masonry一个例子:
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
|
UIView
*superview
=
self
;
UIView
*view1
=
[
[
UIView
alloc
]
init
]
;
view1
.
translatesAutoresizingMaskIntoConstraints
=
NO
;
view1
.
backgroundColor
=
[
UIColor
greenColor
]
;
[
superview
addSubview
:view1
]
;
UIEdgeInsets
padding
=
UIEdgeInsetsMake
(
10
,
10
,
10
,
10
)
;
[
superview
addConstraints
:
@
[
//view1 constraints
[
NSLayoutConstraint
constraintWithItem
:view1
attribute
:NSLayoutAttributeTop
relatedBy
:NSLayoutRelationEqual
toItem
:superview
attribute
:NSLayoutAttributeTop
multiplier
:
1.0
constant
:padding
.
top
]
,
[
NSLayoutConstraint
constraintWithItem
:view1
attribute
:NSLayoutAttributeLeft
relatedBy
:NSLayoutRelationEqual
toItem
:superview
attribute
:NSLayoutAttributeLeft
multiplier
:
1.0
constant
:padding
.
left
]
,
[
NSLayoutConstraint
constraintWithItem
:view1
attribute
:NSLayoutAttributeBottom
relatedBy
:NSLayoutRelationEqual
toItem
:superview
attribute
:NSLayoutAttributeBottom
multiplier
:
1.0
constant
:
-
padding
.
bottom
]
,
[
NSLayoutConstraint
constraintWithItem
:view1
attribute
:NSLayoutAttributeRight
relatedBy
:NSLayoutRelationEqual
toItem
:superview
attribute
:NSLayoutAttributeRight
multiplier
:
1
constant
:
-
padding
.
right
]
,
]
]
;
|
如此简单的一个例子都要编写这么多行代码,想象一下如果创建多个view的constraint时会多么痛苦啊。另一个方式是采用Visual Format Language (VFL),虽然语法比较简洁,但是可读性比较差和容易出错。
Masonry
为什么使用Masonry
Masonry是采用链式DSL(Domain-specific language)来封装NSLayoutConstraint,通过这种方式编写Auto Layout布局代码更加易读和简洁。
使用Masonry的MASConstraintMaker
来表达相同constraint
1
2
3
4
5
6
7
8
|
UIEdgeInsets
padding
=
UIEdgeInsetsMake
(
10
,
10
,
10
,
10
)
;
[
view1
mas_makeConstraints
:
^
(
MASConstraintMaker
*make
)
{
make
.
top
.
equalTo
(
superview
.
mas_top
)
.
with
.
offset
(
padding
.
top
)
;
//with is an optional semantic filler
make
.
left
.
equalTo
(
superview
.
mas_left
)
.
with
.
offset
(
padding
.
left
)
;
make
.
bottom
.
equalTo
(
superview
.
mas_bottom
)
.
with
.
offset
(
-
padding
.
bottom
)
;
make
.
right
.
equalTo
(
superview
.
mas_right
)
.
with
.
offset
(
-
padding
.
right
)
;
}
]
;
|
甚至可以更短
1
2
3
|
[
view1
mas_makeConstraints
:
^
(
MASConstraintMaker
*make
)
{
make
.
edges
.
equalTo
(
superview
)
.
with
.
insets
(
padding
)
;
}
]
;
|
如何使用
使用Masonry创建constraint来定义布局的方式有三种:mas_makeConstraints
,mas_updateConstraints
,mas_remakeConstraints
。
1. mas_makeConstraints
使用mas_makeConstraints
创建constraint后,你可以使用局部变量或属性来保存以便下次引用它;如果创建多个constraints,你可以采用数组来保存它们。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// in public/private interface
@property
(
nonatomic
,
strong
)
MASConstraint
*topConstraint
;
.
.
.
// when making constraints
[
view1
mas_makeConstraints
:
^
(
MASConstraintMaker
*make
)
{
self
.
topConstraint
=
make
.
top
.
equalTo
(
superview
.
mas_top
)
.
with
.
offset
(
padding
.
top
)
;
make
.
left
.
equalTo
(
superview
.
mas_left
)
.
with
.
offset
(
padding
.
left
)
;
}
]
;
.
.
.
// then later you can call
[
self
.
topConstraint
uninstall
]
;
|
2. mas_updateConstraints
有时你需要更新constraint(例如,动画和调试)而不是创建固定constraint,可以使用mas_updateConstraints
方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// this is Apple's recommended place for adding/updating constraints
// this method can get called multiple times in response to setNeedsUpdateConstraints
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
-
(
void
)
updateConstraints
{
[
self
.
growingButton
mas_updateConstraints
:
^
(
MASConstraintMaker
*make
)
{
make
.
center
.
equalTo
(
self
)
;
make
.
width
.
equalTo
(
@
(
self
.
buttonSize
.
width
)
)
.
priorityLow
(
)
;
make
.
height
.
equalTo
(
@
(
self
.
buttonSize
.
height
)
)
.
priorityLow
(
)
;
make
.
width
.
lessThanOrEqualTo
(
self
)
;
make
.
height
.
lessThanOrEqualTo
(
self
)
;
}
]
;
//according to apple super should be called at end of method
[
super
updateConstraints
]
;
}
|
3. mas_remakeConstraints
mas_remakeConstraints
与mas_updateConstraints
比较相似,都是更新constraint。不过,mas_remakeConstraints
是删除之前constraint,然后再添加新的constraint(适用于移动动画);而mas_updateConstraints
只是更新constraint的值。
1
2
3
4
5
6
7
8
9
10
11
|
-
(
void
)
changeButtonPosition
{
[
self
.
button
mas_remakeConstraints
:
^
(
MASConstraintMaker
*make
)
{
make
.
size
.
equalTo
(
self
.
buttonSize
)
;
if
(
topLeft
)
{
make
.
top
.
and
.
left
.
offset
(
10
)
;
}
else
{
make
.
bottom
.
and
.
right
.
offset
(
-
10
)
;
}
}
]
;
}
|
想了解以上三个代码片段的更多细节,可以下载Masonry iOS Examples工程查阅。
Classy
Classy简介和特性
Classy是一个能与UIKit无缝结合stylesheet(样式)系统。它借鉴CSS的思想,但引入新的语法和命名规则。
灵活内嵌的语法
{
}
:
;
这些语法符号是可选的,你可以选择适合自己的风格来表达stylesheet。
你可以使用{
}
:
;
来限定stylesheet
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
$
main
-
color
=
#e1e1e1;
MYCustomView
{
background
-
color
:
$
main
-
color
;
title
-
insets
:
5
,
10
,
5
,
10
;
&
amp
;
gt
;
UIProgressView
.
tinted
{
progress
-
tint
-
color
: black
;
track
-
tint
-
color
: yellow
;
}
}
^
UIButton
.
warning
,
UIView
.
warning
^
UIButton
{
title
-
color
[
state
:highlighted
]
:
#e3e3e3;
}
|
或者你使用空格来限定stylesheet
1
2
3
4
5
6
7
8
9
10
11
|
$
main
-
color
=
#e1e1e1
MYCustomView
background
-
color
$
main
-
color
title
-
insets
5
,
10
,
5
,
10
&
amp
;
gt
;
UIProgressView
.
tinted
progress
-
tint
-
color
black
track
-
tint
-
color
yellow
^
UIButton
.
warning
,
UIView
.
warning
^
UIButton
title
-
color
[
state
:highlighted
]
#e3e3e3
|
默认样式
Classy在应用程序Bundle默认查找文件名为stylesheet.cas的样式文件。如果你采用这个文件名,你可以不用做任何东西就能加载样式文件。
但如果你想指定其他file path(样式文件名),你可以创建[CASStyler defaultStyler]
1
|
[
CASStyler
defaultStyler
]
.
filePath
=
[
[
NSBundle
mainBundle
]
pathForResource
:
@
&
quot
;
myStyles
.
cas
&
quot
;
ofType
:nil
]
;
|
如果你还想当发生错误时,获取错误信息以便于调试,可以使用-(void)setFilePath:error:
1
2
3
|
NSError
*error
=
nil
;
NSString
filePath
=
[
[
NSBundle
mainBundle
]
pathForResource
:
@
&
quot
;
myStyles
.
cas
&
quot
;
ofType
:nil
]
;
[
[
CASStyler
defaultStyler
]
setFilePath
:filePath
error
:
&
amp
;
amp
;
error
]
;
|
如果你是使用Storyboard/Xib组织UI界面,那就需要在main.m的int main(int argc, char * argv[])
方法设置 filePath,这样可以确保在创建UIWindow之前加载stylesheet。否则(采用手写UI代码),你在 AppDelegate.m的- (BOOL)application:didFinishLaunchingWithOptions:
方法设置filePath
Live Reload
Live Reload是实时显示编写UI代码效果的关键特性,它能够实时检查stylesheet文件变化,无需重新编译、构建和运行模拟器,从而极大提高开发速度。
为了启用Live Reload,你需要指定stylesheet路径,并且只运行在模拟器上。
1
2
3
4
|
#if TARGET_IPHONE_SIMULATOR
NSString
*absoluteFilePath
=
CASAbsoluteFilePath
(
@
&
quot
;
.
.
/
Styles
/
stylesheet
.
cas
&
quot
;
)
;
[
CASStyler
defaultStyler
]
.
watchFilePath
=
absoluteFilePath
;
#endif
|
Selectors
Style Selectors是指定哪个view使用哪种样式的方式。主要有三种方法来指定目标view:
- Object Class
- View Hierarchy
- Style Class
你可以混合使用三种方法,例子如下:
1
2
3
4
5
6
7
|
/* match views
* where class is UIButton or UIButton subclass
* and styleClass is "large"
* and superview class is UITabBar
*/
UITabBar
&
amp
;
gt
;
^
UIButton
.
large
{
}
|
想了解具体如何使用,请查阅官网Selectors章节
为了避免与Objective-C的message selectors混淆,术语style selectors表示Classy stylesheets的selectors
Properties
Classy支持所有UIAppearance的属性和方法,也支持与UIAppearance无关的很多属性。Classy使用与UIKit相同属性命名,所以你不必考虑如何将style property映射到Objective-C的property。
UIPageControl
类的属性如下:
1
2
|
@property
(
nonatomic
,
retain
)
UIColor
*pageIndicatorTintColor
;
@property
(
nonatomic
,
retain
)
UIColor
*currentPageIndicatorTintColor
;
|
style property的名字采用与objective-c一样的名字
1
2
3
4
|
UIPageControl
{
pageIndicatorTintColor
black
currentPageIndicatorTintColor
purple
}
|
style property的命名规则采用kebab case
1
2
3
4
|
UIPageControl
{
page
-
indicator
-
tint
-
color
black
current
-
page
-
indicator
-
tint
-
color
purple
}
|
想了解具体如何使用,请查阅官网Properties章节
Keep it DRY(Don’t Repeat Yourself)
在编程中一个很重要的原则就是避免重复,这不仅可以大量减少重复代码,并且使得代码更加容易复用和维护。Classy提供三种方式避免代码重复:grouping,nesting,variables
Grouping
如果有两个以上的style selectors共用相同的属性时
1
2
3
4
5
6
7
8
9
10
|
UISlider
.
info
{
minimum
-
track
-
tint
-
color
black
maximum
-
track
-
tint
-
color
purple
}
UISlider
.
error
{
minimum
-
track
-
tint
-
color
black
maximum
-
track
-
tint
-
color
purple
thumb
-
tint
-
color
red
}
|
我们可以提取相同的属性到分组style selector中
1
2
3
4
5
6
7
8
|
UISlider
.
info
,
UISlider
.
error
{
minimum
-
track
-
tint
-
color
black
maximum
-
track
-
tint
-
color
purple
}
UISlider
.
error
{
thumb
-
tint
-
color
red
}
|
Nesting
如果两个以上style selectors共用相同的view hierarchy时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
UICollectionView
{
background
-
color
#a2a2a2
}
UICollectionView
&
amp
;
gt
;
UICollectionViewCell
{
clips
-
to
-
bounds
NO
}
UICollectionView
&
amp
;
gt
;
UICollectionViewCell
UILabel
{
text
-
color
purple
}
UICollectionView
&
amp
;
gt
;
UICollectionViewCell
UILabel
.
title
{
font
20
}
|
我们通过nesting方式将view hierarchies表达成这样方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
UICollectionView
{
background
-
color
#a2a2a2
&
amp
;
gt
;
UICollectionViewCell
{
clips
-
to
-
bounds
NO
UILabel
{
text
-
color
purple
&
amp
;
amp
;
.
title
{
font
20
}
}
}
}
|
Variables
Classy让你通过定义variables来将多个相同的style property值存储以便共享。Variable命名规则如下:
- 必须以大小写字母或
$
符号开头 - 可以包含
_
,-
或任何字母数字
1234567891011// prefix with ' $ ' to help distinguish variables$ brand - color = #e1e1e1// OR notinsets = 5 , 10 , 5 , 10UIButton {background - color $ brand - colorcontentEdgeInsets insetsbackground - image [ state :selected ] bg_button insets}
最后官方还提供一个实例来解释具体如何使用:Custom Views ExampleClassyLiveLayout
ClassyLiveLayout通过结合Classy stylesheets与Masonry一起使用,能够在运行的模拟器中微调Auto Layout约束实时显示效果的工具。
ClassyLiveLayout一个核心category:
UIView+ClassyLayoutProperties
,在UIView
定义以下属性:1234567891011@property ( nonatomic , assign ) UIEdgeInsets cas_margin ;@property ( nonatomic , assign ) CGSize cas_size ;// shorthand properties for setting only a single constant value@property ( nonatomic , assign ) CGFloat cas_sizeWidth ;@property ( nonatomic , assign ) CGFloat cas_sizeHeight ;@property ( nonatomic , assign ) CGFloat cas_marginTop ;@property ( nonatomic , assign ) CGFloat cas_marginLeft ;@property ( nonatomic , assign ) CGFloat cas_marginBottom ;@property ( nonatomic , assign ) CGFloat cas_marginRight ;cas_margin
和cas_size
分别表示UI元素的位置和大小,而其余的属性都是对两个属性进一步细分。我们可以从stylesheets中访问style properties来定义constraints布局,做到将数据与代码分离,有利于修改和复用代码。12345678910UIView . blue - box {cas_size : 80 100cas_margin - top : 60cas_margin - left : 50}UIView . red - box {cas_size - width : 120cas_margin - left : 20}我们可以在
updateConstraints
或updateViewConstrains
定义布局时引用style properties1234567891011121314151617- ( void ) updateViewConstraints {[ super updateViewConstraints ] ;[ _blueBoxView mas_updateConstraints : ^ ( MASConstraintMaker *make ) {make . width . equalTo ( @ ( _blueBoxView . cas_size . width ) ) ;make . height . equalTo ( @ ( _blueBoxView . cas_size . height ) ) ;make . top . equalTo ( @ ( _blueBoxView . cas_margin . top ) ) ;make . left . equalTo ( @ ( _blueBoxView . cas_margin . left ) ) ;} ] ;[ _redBoxView mas_updateConstraints : ^ ( MASConstraintMaker *make ) {make . width . equalTo ( @ ( _redBoxView . cas_size . width ) ) ;make . height . equalTo ( _blueBoxView ) ;make . top . equalTo ( _blueBoxView ) ;make . left . equalTo ( _blueBoxView . mas_right ) . with . offset ( _redBoxView . cas_margin . left ) ;} ] ;}当定义view layouts时,将Auto Layout的constraints都放在stylesheets中实时加载(Live reload)。如果你修改constraints,无需重新编译、构建和运行模拟器便能实时看到修改后的效果。
示例工程
配置工程
由于需要引用Masonry,Classy和ClassyLiveLayout,Podfile配置如下:
123pod & #039;Masonry', '~> 0.6.1'pod & #039;Classy', '~> 0.2.4'pod & #039;ClassyLiveLayout', '~> 0.6.0'编写代码
1. 添加stylesheet.cas文件到工程
当安装好Masonry,Classy和ClassyLiveLayout后,第一次运行项目会出现没有stylesheet.cas文件错误:
No stylesheet.cas file error.png只要向工程添加空的stylesheet.cas文件即可。
Create stylesheet.cas file.png2. 创建
LiveView
类,该类继承SHPAbstractView
。
Create LiveView inherit SHPAbstractView.png在
ViewController
创建LiveView
对象,然后被self.view
引用。
Setup root view in ViewController.png当编译运行时,在
SHPAbstractView.h
由于找不到UIView
出现编译错误。
SHPAbstractView Compile error.png只需引入UIKit便可以解决,但运行一下应用程序,出现一下错误:
Must override methods.png主要原因是任何自定义
UIView
继承SHPAbstractView
都需要override两个方法:- (void)addSubviews
和- (void)defineLayout
,我们可以查看SHPAbstractView
的源码可知:
SHPAbstractView Source Code .png所以只要在
LiveView.m
文件覆盖两个方法即可12345678#pragma mark - Add subviews and define layout- ( void ) addSubviews{}- ( void ) defineLayout{}3. LiveView类设计
LiveView
主要由包含redBoxView
和blueBoxView
两个属性,redBoxView
表示红色方块,blueBoxView
表示蓝色方块。12345678#import "SHPAbstractView.h"@interface LiveView : SHPAbstractView@property ( strong , nonatomic ) UIView *redBoxView ;@property ( strong , nonatomic ) UIView *blueBoxView ;@end4. LiveView类实现
由于
SHPAbstractView
类如何初始化View已经做了处理,暴露两个接口- (void)addSubviews
和-(void)defineLayout
分别处理构建view hierarchy和定义布局,子类只要覆盖SHPAbstractView
这两个方法就可以创建LiveView了。
但是我们将Auto Layout的constraints都放在stylesheets中实时加载(Live reload),即放在本工程的stylesheet.cas文件,将布局数据和布局代码分离。12345678910111213UIView . redBox {cas _ marginTop 50cas _ marginLeft 20cas _ size 100 100}UIView . blueBox {cas _ marginTop 50cas_marginRight - 20cas _ size 100 100}有了constraints数据后,便可以在代码布局:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849@implementation LiveView#pragma mark - Add subviews and define layout- ( void ) addSubviews{self . backgroundColor = [ UIColor whiteColor ] ;[ self addSubview :self . redBoxView ] ;[ self addSubview :self . blueBoxView ] ;}- ( void ) defineLayout{[ self . redBoxView mas_updateConstraints : ^ ( MASConstraintMaker * make ) {make . top . equalTo ( @ ( self . redBoxView . cas_marginTop ) ) ;make . left . equalTo ( @ ( self . redBoxView . cas_marginLeft ) ) ;make . width . equalTo ( @ ( self . redBoxView . cas_sizeWidth ) ) ;make . height . equalTo ( @ ( self . redBoxView . cas_sizeHeight ) ) ;} ] ;[ self . blueBoxView mas_updateConstraints : ^ ( MASConstraintMaker *make ) {make . top . equalTo ( @ ( self . blueBoxView . cas_marginTop ) ) ;make . right . equalTo ( @ ( self . blueBoxView . cas_marginRight ) ) ;make . width . equalTo ( @ ( self . blueBoxView . cas_sizeWidth ) ) ;make . height . equalTo ( @ ( self . blueBoxView . cas_sizeHeight ) ) ;} ] ;}#pragma mark - Lazy initialization- ( UIView * ) redBoxView{if ( ! _redBoxView ) {_redBoxView = [ UIView new ] ;_redBoxView . cas_styleClass = @ & quot ; redBox & quot ; ;_redBoxView . backgroundColor = [ UIColor redColor ] ;}return _redBoxView ;}- ( UIView * ) blueBoxView{if ( ! _blueBoxView ) {_blueBoxView = [ UIView new ] ;_blueBoxView . cas_styleClass = @ & quot ; blueBox & quot ; ;_blueBoxView . backgroundColor = [ UIColor blueColor ] ;}return _blueBoxView ;}5. 模拟器支持Live Reload
为了启用Live Reload,你需要指定stylesheet路径,并且只运行在模拟器上。
Support Live Reload.png此时效果:
Live Change.gif6. 分离样式文件
由于有网友提出这样一个问题:如果所有view的样式都放在同一个
stylesheet.cas
文件,会让stylesheet.cas
文件繁杂,并且当多个人协同开发时,不易于合并代码,所以有必要将样式文件分离到多个文件中。1.创建
variable.cas
文件,并将redBox
对应UIView的样式放在variable.cas
文件中。
variable.cas file.png2.在
stylesheet.cas
样式文件使用@import
指令引用variable.cas
文件
stylesheet.cas file.png最后效果
Live Change 1.gif
Live Change 2.gif示例代码存放地址:LiveAutoLayout
总结
之前手写UI代码每次更改一般都要重新编译、构建和运行模拟器才能看到效果,但结合使用Masonry,Classy和ClassLiveLayout之后,告别这个费时过程,极大地提高开发速度;不仅如此,我们将Auto Layout的constraints都放在stylesheets中实时加载(Live reload),将布局数据和布局代码分离,使得代码更加复用和维护。Classy还提供三种避免重复方法:Grouping, Nestting和Variable,尽可能复用样式数据。
范例二
使用UIImageView、UILabel、UIButton实现一个综合小案例
功能分析
(1)点击箭头切换序号、图片、描述 (2)如果是首张图片,左边箭头不能点击 (3)如果是尾张图片,右边箭头不能点击步骤分析
(1)搭建UI界面 (2)监听按钮点击切换序号、图片、描述
1. 界面分析
1> 需要读取或修改的属性的控件
// 序号标签
// 图片
// 图片描述
// 左边按钮
// 右边按钮
2> 需要监听响应事件的对象,需要添加监听方法
// 左边按钮
// 右边按钮
uiimage 是图片,不是控件,他的父类为NSObject,UIImageView是加载图片的控件,父类为UIView
完全的代码编写界面(复习回忆)
#import "ViewController.h" @interface ViewController () //序号标签 @property (nonatomic, strong) UILabel *noLabel; //图片 @property (nonatomic, strong) UIImageView *icon; //图片描述 @property (nonatomic, strong) UILabel *descLabel; //左边按钮 @property (nonatomic, strong) UIButton *leftButton; //右边按钮 @property (nonatomic, strong) UIButton *rightButton; @end @implementation ViewController //初始化工作 //viewDidLoad是视图加载完成后调用的方法,通常在此方法中执行视图控制器的初始化工作 - (void)viewDidLoad { [super viewDidLoad]; //实例化控件 //1、序号标签的编写 UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 320, 40)]; label.text = @"1/5"; //居中对齐 label.textAlignment = NSTextAlignmentCenter; [self.view addSubview:label]; //记录改变 self.noLabel = label; //2、图片控件 CGFloat imageW = 200; CGFloat imageH = 200; CGFloat imageX = (320 - imageW) / 2; CGFloat imageY = 80; //实例化一个图像视图 UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(imageX, imageY, imageW, imageH)]; //实例化一个图像 UIImage *image = [UIImage imageNamed:@"biaoqingdi"]; //把图片显示到imageView imageView.image = image; [self.view addSubview:imageView]; //记录下改变 self.icon = imageView; //3、图片描述 label 控件 UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(0, 300, 300, 80)]; label1.text = @"发发发"; //居中对齐 label1.textAlignment = NSTextAlignmentCenter; [self.view addSubview:label1]; //记录改变 self.descLabel = label1; //4、左边的按钮 UIButton *leftBtn = [[UIButton alloc] init]; //设置按钮的背景图 [leftBtn setBackgroundImage:[UIImage imageNamed:@"left_normal"] forState:UIControlStateNormal]; [leftBtn setBackgroundImage:[UIImage imageNamed:@"left_highlighted"] forState:UIControlStateHighlighted]; //设置按钮的大小 leftBtn.frame = CGRectMake(0, 0, 40, 40); //设置按钮的位置 leftBtn.center = CGPointMake(self.icon.frame.origin.x / 2, self.icon.center.y); [self.view addSubview:leftBtn]; self.leftButton = leftBtn; //5、右边的按钮 UIButton *rightBtn = [[UIButton alloc] init]; //设置按钮的背景图 [rightBtn setBackgroundImage:[UIImage imageNamed:@"right_normal"] forState:UIControlStateNormal]; [rightBtn setBackgroundImage:[UIImage imageNamed:@"right_highlighted"] forState:UIControlStateHighlighted]; //设置按钮的大小 rightBtn.frame = CGRectMake(0, 0, 40, 40); //设置按钮的位置 rightBtn.center = CGPointMake(self.view.frame.size.width - self.icon.frame.origin.x / 2, self.icon.center.y); [self.view addSubview:rightBtn]; self.leftButton = rightBtn; } @end
完整的代码如下:
#import "ViewController.h" @interface ViewController () //序号标签 @property (nonatomic, strong) UILabel *noLabel; //图片 @property (nonatomic, strong) UIImageView *icon; //图片描述 @property (nonatomic, strong) UILabel *descLabel; //左边按钮 @property (nonatomic, strong) UIButton *leftButton; //右边按钮 @property (nonatomic, strong) UIButton *rightButton; //图片索引,index默认是0 @property (nonatomic, assign) int index; /**设置一个图像的数组*/ //新的注释,可以显式中文 @property (nonatomic, strong) NSArray *imageList; /* @property 自动为我们生成 set,get 方法的声明和实现 带下划线的成员变量 */ @end @implementation ViewController //控件懒加载 //不需要每次都在 viewdidload 里实例化数组,只要在需要的时候实例化即可 //重写 get 方法 - (NSArray *)imageList { //只有第一次调用imageList 的 getter 方法的时候,如果为空,那么再实例化并建立数组,其他时候,直接返回成员变量 if (_imageList == nil) { //使用字典 NSDictionary *dict1 = @{@"name" : @"biaoqingdi", @"desc" : @"表情"}; NSDictionary *dict2 = @{@"name" : @"bingli", @"desc" : @"病历"}; NSDictionary *dict3 = @{@"name" : @"chiniupa", @"desc" : @"吃牛扒"}; NSDictionary *dict4 = @{@"name" : @"danteng", @"desc" : @"蛋疼"}; NSDictionary *dict5 = @{@"name" : @"wangba", @"desc" : @"王八"}; self.imageList = @[dict1, dict2, dict3, dict4, dict5]; } return _imageList; } //初始化工作 //viewDidLoad是视图加载完成后调用的方法,通常在此方法中执行视图控制器的初始化工作 - (void)viewDidLoad { [super viewDidLoad]; //实例化控件 //1、序号标签的编写 UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 20, 320, 40)]; // label.text = @"1/5"; //居中对齐 label.textAlignment = NSTextAlignmentCenter; [self.view addSubview:label]; //记录改变 self.noLabel = label; //2、图片控件 CGFloat imageW = 200; CGFloat imageH = 200; CGFloat imageX = (320 - imageW) / 2; CGFloat imageY = 80; //实例化一个图像视图 UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(imageX, imageY, imageW, imageH)]; //实例化一个图像 // UIImage *image = [UIImage imageNamed:@"biaoqingdi"]; //把图片显示到imageView // imageView.image = image; //把图像增加到 view [self.view addSubview:imageView]; //记录下改变 self.icon = imageView; //3、图片描述 label 控件 UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectMake(0, 300, 300, 80)]; // label1.text = @"发发发"; //居中对齐 label1.textAlignment = NSTextAlignmentCenter; [self.view addSubview:label1]; //记录改变 self.descLabel = label1; //4、左边的按钮 UIButton *leftBtn = [[UIButton alloc] init]; //设置按钮的背景图 [leftBtn setBackgroundImage:[UIImage imageNamed:@"left_normal"] forState:UIControlStateNormal]; [leftBtn setBackgroundImage:[UIImage imageNamed:@"left_highlighted"] forState:UIControlStateHighlighted]; //设置按钮的大小 leftBtn.frame = CGRectMake(0, 0, 40, 40); //设置按钮的位置 leftBtn.center = CGPointMake(self.icon.frame.origin.x / 2, self.icon.center.y); [self.view addSubview:leftBtn]; //设置监听 [leftBtn addTarget:self action:@selector(leftClick) forControlEvents:UIControlEventTouchUpInside]; self.leftButton = leftBtn; //5、右边的按钮 UIButton *rightBtn = [[UIButton alloc] init]; //设置按钮的背景图 [rightBtn setBackgroundImage:[UIImage imageNamed:@"right_normal"] forState:UIControlStateNormal]; [rightBtn setBackgroundImage:[UIImage imageNamed:@"right_highlighted"] forState:UIControlStateHighlighted]; //设置按钮的大小 rightBtn.frame = CGRectMake(0, 0, 40, 40); //设置按钮的位置 rightBtn.center = CGPointMake(self.view.frame.size.width - self.icon.frame.origin.x / 2, self.icon.center.y); [self.view addSubview:rightBtn]; //设置监听 [rightBtn addTarget:self action:@selector(rightClick) forControlEvents:UIControlEventTouchUpInside]; self.rightButton = rightBtn; [self change]; } - (void)change { //更具 self.index 来显示序号标签,图形,,描述 self.noLabel.text = [NSString stringWithFormat:@"%d / %d", self.index + 1, 5]; self.icon.image = [UIImage imageNamed:self.imageList[self.index][@"name"]]; self.descLabel.text = self.imageList[self.index][@"desc"]; self.leftButton.enabled = (self.index != 0); self.rightButton.enabled = (self.index != 4); } //left - (void)leftClick { self.index--; [self change]; } //right - (void)rightClick { self.index++; [self change]; } @end
小结:
/**设置一个图像的数组*/
这是 xcode 的新的注释,鼠标浮动时,可以显式出中文注释。
手码懒加载创建控件的步骤
1> 定义控件属性,注意:属性必须是strong的,如下:
@property (nonatomic, strong) UIImageView *icon;
2> 在属性的getter方法中实现懒加载。
使用懒加载的好处:
1> 不必将创建对象的代码全部写在viewDidLoad方法中,代码的可读性更强
2> 每个控件的getter方法中分别负责各自的实例化处理,代码彼此之间的独立性强,松耦合
按钮的状态
normal(普通状态) 默认情况 对应的枚举常量:UIControlStateNormal highlighted(高亮状态) 按钮被按下去的时候(手指还未松开) 对应的枚举常量:UIControlStateHighlighted disabled(失效状态,不可用状态) 如果enabled属性为NO,就是处于disable状态,代表按钮不可以被点击 对应的枚举常量:UIControlStateDisabled使用Plist文件重构本项目代码:
目的:将数据与代码分离(类似 java 里的 xml 文件写数据,数据和代码分离)
之前的代码,尤其是字典那部分,还是处理的不好,显得太耦合。需要把数据和代码分离,这里学习属性列表文件,property list
新建file
本地文件,也可以网络上解析 xml 文件
这样,只需要修改对应的 xml 文件即可,不用再打开代码,修改代码
//控件懒加载 //不需要每次都在 viewdidload 里实例化数组,只要在需要的时候实例化即可 - (NSArray *)imageList { //只有第一次调用imageList 的 getter 方法的时候,如果为空,那么再实例化并建立数组,其他时候,直接返回成员变量 if (_imageList == nil) { //bundle 包的概念 只读 NSString *path = [[NSBundle mainBundle] pathForResource:@"imageDate" ofType:@".plist"]; NSLog(@"%@", path); //File 表示从完整路径查找文件 _imageList = [NSArray arrayWithContentsOfFile:path]; } return _imageList; }
小结:
1、将数据与代码分离,Plist 文件的加载方法:
直接将数据直接写在代码里面,不是一种合理的做法。如果数据经常改,就要经常翻开对应的代码进行修改,造成代码扩展性低,因此,可以考虑将经常变的数据放在文件中进行存储,程序启动后从文件中读取最新的数据。如果要变动数据,直接修改数据文件即可,不用修改代码。 一般可以使用属性列表文件存储NSArray或者NSDictionary之类的数据,这种属性列表文件的扩展名是plist,因此也成为“Plist文件”NSString *path = [[NSBundle mainBundle] pathForResource:@"ImageData" ofType:@"plist"];
_imageList = [NSArray arrayWithContentsOfFile:path];
提示:通常在方法中出现File字眼,通常需要传递文件的全路径作为参数,如下全路径:
/Users/dashuai/Library/Developer/CoreSimulator/Devices/83C611C9-DE98-4D02-BC64-D31C0403766E/data/Containers/Bundle/Application/E04713CF-A9D4-49D1-A934-B4093BCE5B3C/图片浏览.app/imageDate.plist
2、要想让UILabel自动换行,设置Lines为0即可。
3、UIButton和UIImageView
相同点 都能显示图片 不同点 UIButton默认情况就能监听点击事件,而UIImageView默认情况下不能 UIButton可以在不同状态下显示不同的图片 UIButton既能显示文字,又能显示图片 如何选择 UIButton:需要显示图片,点击图片后需要做一些特定的操作 UIImageView:仅仅需要显示图片,点击图片后不需要做任何事情 NSArray和NSDictionary的使用 当图片内容非常多时,“根据index来设置内容”的代码就不具备扩展性,要经常改动,为了改变现状,可以考虑将图片数据保存到一个数组中,数组中有序地放着很多字典,一个字典代表一张图片数据,包含了图片名、图片描述@property (strong, nonatomic) NSArray *images;
由于只需要初始化一次图片数据,因此放在get方法中初始化,将属性放在get方法中初始化的方式,称为“懒加载”\”延迟加载”
/**设置一个图像的数组*/
这是 xcode 的新的注释,鼠标浮动时,可以显式出中文注释。
手码懒加载创建控件的步骤
1> 定义控件属性,注意:属性必须是strong的,如下:
@property (nonatomic, strong) UIImageView *icon;
2> 在属性的getter方法中实现懒加载。
使用懒加载的好处:
1> 不必将创建对象的代码全部写在viewDidLoad方法中,代码的可读性更强
2> 每个控件的getter方法中分别负责各自的实例化处理,代码彼此之间的独立性强,松耦合
按钮的状态
normal(普通状态) 默认情况 对应的枚举常量:UIControlStateNormal highlighted(高亮状态) 按钮被按下去的时候(手指还未松开) 对应的枚举常量:UIControlStateHighlighted disabled(失效状态,不可用状态) 如果enabled属性为NO,就是处于disable状态,代表按钮不可以被点击 对应的枚举常量:UIControlStateDisabled