概述
上一篇文章《iOS开发系列--Swift语言》中对Swift的语法特点以及它和C、ObjC等其他语言的用法区别进行了介绍。当然,这只是Swift的入门基础,但是仅仅了解这些对于使用Swift进行iOS开发还是不够的。在这篇文章中将继续介绍一些Swift开发中一些不常关注但是又必备的知识点,以便对Swift有进一步的了解。
访问控制
和其他高级语言一样Swift中也增加了访问控制,在Swift中提供了private、internal、public三种访问级别,但是不同的是Swift中的访问级别是基于模块(module,或者target)和源文件(.swift文件)的,而不是基于类型、命名空间声明。
- private:只能访问当前源文件中的实体(注意Swift中的private和其他语言不太一样,它是基于源文件的,作用范围是整个源文件,如果一个源文件中有两个类,那么一个类可以访问另外一个类的私有成员)。
- internal:可以访问当前模块中的其他任何实体,但是在模块外无法访问,这是所有实体的默认访问级别(通常在一个单目标Application中不需要自行设置访问级别)。
- public:可以访问当前模块及其他模块中的任何实体(通常用于Framework)。
下面是关于Swift关于不同成员访问级别的约定规则:
- 如果一个类的访问级别是private那么该类的所有成员都是private(此时成员无法修改访问级别),如果一个类的访问级别是internal或者public那么它的所有成员都是internal(如果类的访问级别是public,成员默认internal,此时可以单独修改成员的访问级别),类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则);
- 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private);
- 在不违反1、2两条规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal,那么可以添加private(set)修饰将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的);
- 必要构造方法(required修饰)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员是private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循规则1;
- 子类的访问级别不高于父类的访问级别,但是在遵循三种访问级别作用范围的前提下子类可以将父类低访问级别的成员重写成更高的访问级别(例如父类A和子类B在同一个源文件,A的访问级别是public,B的访问级别是internal,其中A有一个private方法,那么A可以覆盖其private方法并重写为internal);
- 协议中所有必须实现的成员的访问级别和协议本身的访问级别相同,其子协议的访问级别不高于父协议;
- 如果一个类继承于另一个类的同时实现了某个协议那么这个类的访问级别为父类和协议的最低访问级别,并且此类中方法访问级别和所实现的协议中的方法相同;
- 扩展的成员访问级别遵循规则1,但是对于类、结构体、枚举的扩展可以明确声明访问级别并且可以更低(例如对于internal的类,你可以声明一个private的扩展),而协议的访问级别不可以明确声明;
- 元组的访问级别是元组中各个元素的最低访问级别,注意:元组的访问级别是自动推导的,无法直接使用以上三个关键字修饰其访问级别;
- 函数的访问级是函数的参数、返回值的最低级别,并且如果其访问级别和默认访问级别(internal)不符需要明确声明;
- 枚举成员的访问级别等同于枚举的访问级别(无法单独设置),同时枚举的原始值、关联值的访问级别不能低于枚举的访问级别;
- 泛型类型或泛型函数的访问级别是泛型类型、泛型函数、泛型类型参数三者中最低的一个;
- 类型别名的访问级别不能高于原类型的访问级别;
上面这些规则看上去比较繁琐,但其实很多内容理解起来也是顺理成章的(如果你是一个语言设计者相信大部分规则也会这么设计),下面通过一个例子对于规则3做一解释,这一点和其他语言有所不同但是却更加实用。在使用ObjC开发时大家通常会有这样的经验:在一个类中希望某个属性对外界是只读的,但是自己又需要在类中对属性进行写操作,此时只能直接访问属性对应的成员变量,而不能直接访问属性进行设置。但是Swift为了让语法尽可能精简,并没有成员变量的概念,此时就可以通过访问控制来实现。
Person.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import Foundation
public class Person {
//设置setter私有,但是getter为public
public private ( set ) var name : String
public init ( name : String ){
self . name = name
}
public func showMessage (){
println ( "name=\( name )" )
}
} |
main.swift
1
2
3
4
5
6
7
|
import Foundation
var p = Person ( name : "Kenshin" )
//此时不能设置name属性,但是可读 //p.name = "Kaoru" println ( "name=\( p . name )" )
p . showMessage () |
Xcode中的每个构建目标(Target)可以当做是一个模块(Module),这个构建目标可以是一个Application,也可以是一个通用的Framework(更多的时候是一个Application)。
Swift命名空间
熟悉ObjC的朋友都知道ObjC没有命名空间,为了避免类名重复苹果官方推荐使用类名前缀,这种做法从一定程度上避免了大部分问题,但是当你在项目中引入一个第三方库而这个第三方库引用了一个和你当前项目中用到的同一个库时就会出现问题。因为静态库最终会编译到同一个域,最终导致编译出错。当然作为一个现代化语言Swift一定会解决这个问题,可是如果查看Swift的官方文档,里面关于Swift的命名空间并没有太多详细的说明。但是Swift的作者Chris Lattner在Twitter中回答了这个问题:
Namespacing is implicit in swift, all classes (etc) are implicitly scoped by the module (Xcode target) they are in. no class prefixes needed
Swift中是实现了命名空间功能的,只是这个命名空间不像C#的namespace或者Java中的package那样需要显式在文件中指定,而是采用模块(Module)的概念:在同一个模块中所有的Swift类处于同一个命名空间,它们之间不需要导入就可以相互访问。很明显Swift的这种做法是为了最大限度的简化Swift编程。其实一个module就可以看成是一个project中的一个target,在创建项目的时候默认就会创建一个target,这个target的默认模块名称就是这个项目的名称(可以在target的Build Settings—Product Module Name配置)。
下面不妨看一个命名空间的例子,创建一个Single View Application应用“NameSpaceDemo”。默认情况下模块名称为“NameSpaceDemo”,这里修改为“Network”,并且添加”HttpRequest.swift"。然后添加一个Cocoa Touch Framework类型的target并命名为“IO”,添加“File.swift”。然后在ViewController.swift中调用HttpRequest发送请求,将请求结果利用File类来保存起来。
File.swift
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
|
import Foundation
public class File {
public var path : String !
public init ( path : String ) {
self . path = path
}
public func write ( content : String ){
var error : NSError ?
content . writeToFile ( path , atomically : true , encoding : NSUTF8StringEncoding , error : & error )
if error != nil {
println ( "write failure..." )
}
}
public func read () - > String ?{
var error : NSError ?
var content = String ( contentsOfFile : path , encoding : NSUTF8StringEncoding , error : & error )
if error != nil {
println ( "write failure..." )
}
return content
}
} |
HttpRequest.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import Foundation
class HttpRequest {
class func request ( urlStr : String , complete :( responseText : String ?)- > ()){
var url = NSURL ( string : urlStr )
let task = NSURLSession . sharedSession (). dataTaskWithURL ( url !) { ( data , response , error ) - > Void in
var str : String ?
if error == nil {
str = NSString ( data : data , encoding : NSUTF8StringEncoding ) as ? String
}
complete ( responseText : str )
}
task . resume ()
}
} |
ViewController.swift
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
|
import UIKit
//导入模块 import IO
class ViewController : UIViewController {
let filePath = "/Users/KenshinCui/Desktop/file.txt"
override func viewDidLoad () {
super . viewDidLoad ()
//加上命名空间Network调用,注意这里命名空间可以省略
Network . HttpRequest . request ( url , complete : { ( responseText ) - > () in
if let txt = responseText {
//调用模块中的类和方法
var file = File ( path : self . filePath )
file . write ( txt )
// println(file.read()!) } else {
println ( "error..." )
}
})
}
} |
可以看到首先同一个Module中的HttpRequest类可以加上命名空间调用(当然这里可以省略),另外对于不同Modle下的File类通过导入IO模块可以直接使用File类,但是这里需要注意访问控制,可以看到File类及其成员均声明为了public访问级别。 用模块进行命名空间划分的方式好处就是可以不用显式指定命名空间,然而这种方式无法在同一个模块中再进行划分,不过这个问题可以使用Swift中的嵌套类型来解决。在下面的例子中仍然使用前面的两个类HttpRequest和File类来演示,不同的是两个类分别嵌套在两个结构体Network和IO之中。
Network.HttpRequest.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import Foundation
struct Network {
class HttpRequest {
class func request ( urlStr : String , complete :( responseText : String ?)- > ()){
var url = NSURL ( string : urlStr )
let task = NSURLSession . sharedSession (). dataTaskWithURL ( url !) { ( data , response , error ) - > Void in
var str : String ?
if error == nil {
str = NSString ( data : data , encoding : NSUTF8StringEncoding ) as ? String
}
complete ( responseText : str )
}
task . resume ()
}
}
} |
IO.File.swift
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
|
import Foundation
struct IO {
class File {
var path : String !
init ( path : String ) {
self . path = path
}
func write ( content : String ){
var error : NSError ?
content . writeToFile ( path , atomically : true , encoding : NSUTF8StringEncoding , error : & error )
if error != nil {
println ( "write failure..." )
}
}
func read () - > String ?{
var error : NSError ?
var content = String ( contentsOfFile : path , encoding : NSUTF8StringEncoding , error : & error )
if error != nil {
println ( "write failure..." )
}
return content
}
}
} |
main.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import Foundation
let filePath = "/Users/KenshinCui/Desktop/file.txt"
Network . HttpRequest . request ( url , complete : { ( responseText ) - > () in
if let txt = responseText {
var file = IO . File ( path : filePath )
file . write ( txt )
//println(file.read()!)
} else {
println ( "error..." )
}
}) sleep ( 30 ) //延迟30s避免命令行程序运行完进程结束,等待网络请求
|
Swift和ObjC互相调用
Swift的设计的初衷就是摆脱ObjC沉重的历史包袱,毕竟ObjC的历史太过悠久,相比于很多现代化语言它缺少一些很酷的语法特性,而且ObjC的语法和其他语言相比差别很大。但是Apple同时也不能忽视ObjC的地位,毕竟ObjC经过二十多年的历史积累了大量的资源(开发者、框架、类库等),因此在Swift推出的初期必须考虑兼容ObjC。但同时Swift和ObjC是基于两种不同的方式来实现的(例如ObjC可以在运行时决定对象类型,但是Swift为了提高效率要求在编译时就必须确定对象类型),所以要无缝兼容需要做大量的工作。而作为开发人员我们有必要了解两种语言之间的转化关系才能对Swift有更深刻的理解。
Swift和ObjC映射关系
其实从前面的例子中大家不难发现Swift和ObjC必然存在着一定的映射关系,例如对于文件的操作使用了字符串的writeToFile方法,在网络请求时使用的NSURLSession,虽然调用方式不同但是其参数完全和做ObjC开发时调用方式一致。原因就是Swift编译器自动做了映射,下面列举了部分Swift和ObjC的映射关系帮助大家理解:
Swift | ObjC | 备注 |
---|---|---|
AnyObject | id(ObjC中的对象任意类型) | 由于ObjC中的对象可能为nil,所以Swift中如果用到ObjC中类型的参数会标记为对应的可选类型 |
Array、Dictionary、Set | NSArray、NSDictionary、NSSet | 注意:ObjC中的数组和字典不能存储基本数据类型,只能存储对象类型,这样一来对于Swift中的Int、UInt、Float、Double、Bool转化时会自动桥接成NSNumber |
Int | NSInteger、NSUInteger | 其他基本类型情况类似,不再一一列举 |
NSObjectProtocol | NSObject协议(注意不是NSObject类) | 由于Swift在继承或者实现时没有类的命名空间的概念,而ObjC中既有NSObject类又有NSObject协议,所以在Swift中将NSObject协议对应成了NSObjectProtocol |
CGContext | CGContextRef |
Core Foundation中其他情况均是如此,由于Swift本身就是引用类型,在Swift不需要再加上“Ref” |
ErrorType | NSError | |
“ab:" | @selector(ab:) |
Swift可以自动将字符串转化成成selector |
@NSCopying | copy属性 | |
init(x:X,y:Y) | initWithX:(X)x y:(Y)y | 构造方法映射,Swift会去掉“With”并且第一个字母小写作为其第一个参数,同时也不需要调用alloc方法,但是需要注意ObjC中的便利工厂方法(构建对象的静态方法)对应成了Swift的便利构造方法 |
func xY(a:A,b:B) | void xY:(A)a b:(B)b | |
extension(扩展) | category(分类) | 注意:不能为ObjC中存在的方法进行extension |
Closure(闭包) | block(块) | 注意:Swift中的闭包可以直接修改外部变量,但是block中要修改外部变量必须声明为__block |
Swift兼容大部分ObjC(通过类似上面的对应关系),多数ObjC的功能在Swift中都能使用。当然,还是有个别地方Swift并没有考虑兼容ObjC,例如:Swift中无法使用预处理指令(例如:宏定义,事实上在Swift中推举使用常量定义);Swift中也无法使用performSelector来执行一个方法,因为Swift认为这么做是不安全的。
相反,如果在ObjC中使用Swift也同样是可行的(除了个别Swift新增的高级功能)。Swift中如果一个类继承于NSObject,那么他会自动和ObjC兼容,这样ObjC就可以按照上面的对应关系调用Swift的方法、属性等。但是如果Swift中的类没有继承于NSObject呢?此时就需要使用一个关键字“@objc”进行标注,ObjC就可以像使用正常的ObjC编码一样调用Swift了(事实上继承于NSObject的类之所以在ObjC中能够直接调用也是因为编译器会自动给类和非private成员添加上@objc,类似的@IBoutlet、@IBAction、@NSManaged修饰的方法属性Swift编译器也会自动添加@objc标记)。
Swift调用ObjC
当前ObjC已经积累了大量的第三方库,相信在Swift发展的前期调用已经存在的ObjC是比较常见的。在Swift和ObjC的兼容性允许你在一个项目中使用两种语言混合编程(称为“mix and match”),而不管这个项目原本是基于Swift的还是ObjC的。无论是Swift中调用ObjC还是ObjC中调用Swift都是通过头文件暴漏对应接口的,下图说明了这种交互方式:
不难发现,要在Swift中调用ObjC必须借助于一个桥接头文件,在这个头文件中将ObjC接口暴漏给Swift。例如你可以创建一个“xx.h”头文件,然后使用“#import”导入需要在Swift中使用的ObjC类,同时在Build Settings的“Objective-C Bridging Header”中配置桥接文件“xx.h”。但是好在这个过程Xcode可以帮助你完成,你只需要在Swift项目中添加ObjC文件,Xcode就会询问你是否创建桥接文件,你只需要点击“Yes”就可以帮你完成上面的操作:
为了演示Swift中调用ObjC的简洁性, 下面创建一个基于Swift的Single View Application类型的项目,现在有一个基于ObjC的“KCLoadingView”类,它可以在网络忙时显示一个加载动画。整个类的实现很简单,就是通过一个基础动画实现一个图片的旋转。
KCLoadingView.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
# import < UIKit / UIKit . h >
/** * 加载视图,显示加载效果
*/
@ interface KCLoadingView : UIImageView
/** * 启动,开始旋转
*/
- ( void ) start ;
/** * 停止
*/
- ( void ) stop ;
@ end
|
KCLoadingView.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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
#import "KCLoadingView.h" static NSString * const kAnimationKey = @ "rotationAnimation" ;
@interface KCLoadingView ()
@property (strong, nonatomic ) CABasicAnimation *rotationAnimation;
@end @implementation KCLoadingView
#pragma mark - 生命周期及其基类方法 - (instancetype)initWithFrame:(CGRect)frame { if ( self = [ super initWithFrame:frame]) {
[ self setup];
}
return self ;
} #pragma mark - 公共方法 - ( void )start {
[ self .layer addAnimation: self .rotationAnimation forKey:kAnimationKey];
} - ( void )stop {
[ self .layer removeAnimationForKey:kAnimationKey];
} #pragma mark - 私有方法 - ( void )setup {
self .image = [UIImage imageNamed:@ "loading" ];
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@ "transform.rotation.z" ];
rotationAnimation.toValue = [ NSNumber numberWithFloat:M_PI * 2.0];
rotationAnimation.duration = 0.7;
rotationAnimation.cumulative = YES ;
rotationAnimation.repeatCount = HUGE_VALF;
self .rotationAnimation = rotationAnimation;
[ self .layer addAnimation:rotationAnimation forKey:kAnimationKey];
} @end |
当将这个类加入到项目时就会提示你是否创建一个桥接文件,在这个文件中导入上面的“KCLoadingView”类。现在这个文件只有一行代码
ObjCBridge-Bridging-Header.h
1
|
# import "KCLoadingView.h" |
接下来就可以调用这个类完成一个加载动画,调用关系完全顺其自然,开发者根本感觉不到这是在调用一个ObjC类。
ViewController.swfit
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
|
import UIKit
class ViewController : UIViewController {
lazy var loadingView : KCLoadingView = {
var size = UIScreen . mainScreen (). bounds . size
var lv = KCLoadingView ()
lv . frame . size = CGSizeMake ( 37.0 , 37.0 )
lv . center = CGPointMake ( size . width * 0.5 , size . height * 0.5 )
return lv
}()
lazy private var converView : UIView = {
var cv = UIView ( frame : UIScreen . mainScreen (). bounds )
cv . backgroundColor = UIColor ( red : 0.0 , green : 0.0 , blue : 0.0 , alpha : 0.5 )
return cv
}()
override func loadView () {
//设置背景
var image = UIImage ( named : "iOS9" )
var background = UIImageView ( frame : UIScreen . mainScreen (). bounds )
background . userInteractionEnabled = true
background . image = image
self . view = background
}
override func viewDidLoad () {
super . viewDidLoad ()
//设置蒙层
self . view . addSubview ( self . converView )
//添加加载控件
self . view . addSubview ( self . loadingView )
loadingView . start ()
}
override func touchesBegan ( touches : Set , withEvent event : UIEvent ) {
loadingView . stop ()
}
} |
运行效果
ObjC调用Swift
从前面的Swift和ObjC之间的交互图示可以看到ObjC调用Swift是通过Swift生成的一个头文件实现的,好在这个头文件是由编译器自动完成的,开发者不需要关注,只需要记得他的格式即可“项目名称-Swift.h”。如果在ObjC项目中使用了Swift,只要在ObjC的“.m”文件中导入这个头文件就可以直接调用Swift,注意这个生成的文件并不在项目中,它在项目构建的一个文件夹中(可以按住Command点击头文件查看)。同样通过前面的例子演示如何在ObjC中调用Swift,新建
请发表评论