Result
在Swift5之前,我们一般是采用上面的方式来处理异常,在Swift5之后,苹果推出了一个Result枚举,Result枚举可以更加优雅地去处理异常。
比如说,在iOS开发中有这样一个网络请求:
代码语言:javascript复制let request = URLRequest(url: URL(string: "https://xxx")!)
URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
//处理错误error
} else {
//处理数据data
}
}
这里有三个参数:(Data?, URLResponse?, Error?) ,他们都是可选型。当请求成功时,data参数包含response中的数据,error是nil;当发生错误时,error指明具体的错误,data为nil。显然,data和error是互斥的,不存在data和error同时为nil,或者同时非nil的情况,但是编译器是无法确认此事实的。所以在Swift5中,新增了一个枚举Result,使我们能够更简单、更清晰地处理复杂代码中的错误。
定义
代码语言:javascript复制public enum Result<Success, Failure> where Failure : Error {
//A success, storing a 'Success' value
case success(Success)
//A failure, storing a 'Failure' value
case failure(Failure)
}
这里的Result枚举接受了两个泛型参数,一个是Success,一个是Failure,但是Failure必须遵循Error协议。
这里的Success代表正确执行的值,Failure代表出现问题时的错误值。
一个简单的案例
前面我们举过这样一个例子:
代码语言:javascript复制
// 定义异常
enum FileReadError : Error {
case FileIsNull // 路径为空
case FileNotFound // 路径下对应的文件不存在
}
// 改进方法,让方法抛出异常
func readFileContent(filePath : String) throws -> String {
//1,路径为空字符串
if filePath == "" {
throw FileReadError.FileIsNull
}
//2,路径有值,但是该路径下没有对应的文件
if filePath != "/user/desktop/123.plist" {
throw FileReadError.FileNotFound
}
//3,正确获取到文件内容
return "123"
}
现在我们将上例改为使用Result来处理异常:
代码语言:javascript复制// 定义异常
enum FileReadError : Error {
case FileIsNull // 路径为空
case FileNotFound // 路径下对应的文件不存在
}
// 改进方法,让方法抛出异常
func readFileContent(filePath : String) -> Result<String, FileReadError> {
//1,路径为空字符串
if filePath == "" {
return .failure(.FileIsNull)
}
//2,路径有值,但是该路径下没有对应的文件
if filePath != "/user/desktop/123.plist" {
return .failure(.FileNotFound)
}
//3,正确获取到文件内容
return .success("123")
}
let result = readFileContent(filePath: "/user/desktop/123.plist")
// 处理result
switch result {
case .failure(let error):
switch error {
case .FileIsNull:
print("路径为空")
case .FileNotFound:
print("路径下对应的文件不存在")
}
case .success(let content):
print(content)
}
异步失败处理案例
使用闭包来处理Result:
代码语言:javascript复制// 定义 Error
enum NetworkError : Error {
case UrlInvalid
}
// 定义一个函数,包含一个逃逸闭包进行异步回调
func getInfo (from urlString : String, completionHandler : @escaping (Result<String, NetworkError>) -> Void){
if urlString.hasPrefix("https://") {
//经过一系列网络处理以后得到一个服务器返回的数据
let data = "response result"
//获取数据
completionHandler(.success(data))
} else {
//URL有问题
completionHandler(.failure(.UrlInvalid))
}
}
//调用函数
getInfo(from: "777") { (result) in
//处理Result
switch result {
case .failure(let error):
switch error {
case .UrlInvalid:
print("Url有问题")
}
case .success(let content):
print(content)
}
}
元类型、.self和Self
元类型、.self
获取一个对象的类型:
代码语言:javascript复制var str = "Hello, playground"
type(of: str) // String.Type
元类型,可以理解成是类型的类型,通过类型.Type来定义,既然是类型,就可以修饰变量或者常量,如何得到这种类型呢?需要通过类型.self:
代码语言:javascript复制var str = "Hello, playground"
type(of: str) // String.Type
type(of: str.self) // String.Type
var a = String.self
type(of: a) // String.Type.Type
协议的元类型:
代码语言:javascript复制protocol TestProtocol {
}
TestProtocol.self // TestProtocol.Protocol
Self与协议
在定义协议的时候,Self使用的频率很高,用于协议(protocol)中限制相关的类型:
代码语言:javascript复制//限定遵循该协议的代理者必须是UIView或者其子类类型
protocol TestProtocol {
func eat() -> Self
}
extension TestProtocol where Self : UIView{
}
@objc关键字
在Swift中,很多地方都用到了@objc关键字,尤其是在一些混编项目中。出于安全的考虑,我们需要在暴露给OC使用的类、属性和方法前面加上@objc。那么在Swift中,哪些地方用到了这个关键字呢?
1,#selector中调用的方法需要在方法声明前面加上@objc
代码语言:javascript复制class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type: .contactAdd)
button.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
}
@objc func buttonClicked() {
print("buttonClicked")
}
}
2,协议的方法可选时,协议和可选方法前要用@objc声明
代码语言:javascript复制@objc protocol OptionalProtocol {
@objc optional func protocolMethod1()
@objc optional func protocolMethod2()
}
3,用weak修饰delegate属性时,协议前要用@objc声明
代码语言:javascript复制@objc protocol ViewControllerDelegate {
func protocolMethod1()
func protocolMethod2()
}
class ViewController: UIViewController {
weak var delegate : ViewControllerDelegate?
}
4,类的前面加上@objcMembers,则该类以及它的子类、延展里面的方法都会隐式地加上@objc。
代码语言:javascript复制@objcMembers
class NormanClass {
func work(){}
}
extension NormanClass {
func eat() {}
func sleep() {}
}
如果此时扩展里面不想加@objc,那么可以使用@nonobjc关键字来修饰
代码语言:javascript复制@objcMembers
class NormanClass {
func work(){}
}
@nonobjc extension NormanClass {
func eat() {}
func sleep() {}
}
5,扩展前面加上@objc,那么该扩展里面的方法都会隐式加上@objc
代码语言:javascript复制class NormanClass {
func work(){}
}
@objc extension NormanClass {
func eat() {} // 包含隐式的@objc
func sleep() {} // 包含隐式的@objc
}
where关键字
在Swift中,很多地方都用到了where关键字,这里的where和数据库中的where差不多,都是用于条件过滤。where关键字在Swift中非常强大,那么在Swift中哪些地方用到了这个关键字呢?
1,switch-case中
代码语言:javascript复制 let names = ["张三", "李四", "王五", "李六"]
for name in names {
switch name {
case let x where x.hasPrefix("李"):
print("姓王的有(x)")
default:
print("你好啊,(name)")
}
}
2,for循环中
代码语言:javascript复制 let array = [1,2,3,4,5,6,7,8,9]
for num in array where num%2==0 {
print(num)
}
3,protocol协议中
代码语言:javascript复制protocol SomeProtocol {
}
extension SomeProtocol where Self : UIView {
//只给遵守SomeProtocol协议的UIView添加了扩展
func getInfo() -> String {
return "属于UIView类型"
}
}
extension UIButton : SomeProtocol {
}
let button = UIButton()
button.getInfo() // 属于UIView类型
4,泛型中
代码语言:javascript复制protocol SomeProtocol {
func play()
}
class Student : SomeProtocol {
func play() {
print("student play")
}
}
//泛型必须遵守SomeProtocol协议
func test<T>(company : T) where T : SomeProtocol {
company.play()
}
test(company: Student())
5,do-catch异常处理
代码语言:javascript复制enum ExcentionError : Error {
case httpCode(Int)
}
func throwError() throws {
throw ExcentionError.httpCode(404)
}
do {
try throwError()
} catch ExcentionError.httpCode(let codeNum) where codeNum == 404 {
print("not found error")
}
Key Path
我们可以通过KeyPath来间接设置/获取值。
Swift中没有原生的KVC概念,但是可以利用KeyPath来间接地实现KVC。
如果要使用KeyPath,则类必须继承自NSObject,否则不能用。
哪些属性可以通过KeyPath进行操作,就需要在其前面加上@objc
代码语言:javascript复制class Student : NSObject {
@objc var name : String
@objc var age : Int
var birthday : String
var gender : String
init(name : String, age : Int, birthday : String, gender : String) {
self.name = name
self.age = age
self.birthday = birthday
self.gender = gender
}
}
var student = Student(name: "norman", age: 18, birthday: "19910910", gender: "male")
//获取值
student.name
//Swift 3 之前
student.value(forKey: "name")
//Swift 3
student.value(forKeyPath: #keyPath(Student.name))
//Swift 4
student[keyPath:Student.name]
//设置值
student.age = 20
//Swift 3 之前
student.setValue(22, forKey: "age")
//Swift 3
student.setValue(24, forKeyPath: #keyPath(Student.age))
//Swift 4
student[keyPath:Student.age] = 26
Codable协议
我们在开发中经常会碰到结构体或者类与JSON数据的相互转换,尤其是网络请求数据的时候将服务器返回的JSON转成Model。
我们在使用OC的时候可以使用KVC、NSJSONSerialization实现JSON转Model;在Swift4之后,我们使用Codable协议,通过编解码操作实现JSON与Model之间的互相转换。
代码语言:javascript复制// JSON
let response = """
{
"name":"norman",
"birthday":"19900803",
"gender":"male",
"age":18
}
"""
// 定义结构体实现codable,一般情况下属性名要与JSON的key一致,否则需要额外处理
struct Student : Codable {
let name : String
let birthday : String
let gender : String
let age : Int
}
// JSON 转为结构体、类,解码,decode
let decoder = JSONDecoder()
do {
let student : Student = try decoder.decode(Student.self, from: response.data(using: .utf8)!)
print(student.name, student.birthday, student.gender, student.age) // norman 19900803 male 18
} catch {
print(error)
}
// 编码,encode,结构体、类转成JSON
let student = Student(name: "lavie", birthday: "19910910", gender: "male", age: 29)
let encoder = JSONEncoder()
if let jsonData = try? encoder.encode(student) {
print(String(data: jsonData, encoding: .utf8)!) // {"gender":"male","age":29,"name":"lavie","birthday":"19910910"}
}
字段不匹配处理
注意,上面的例子中,结构体中的属性名称与JSON中的字段名一致。当不一致的时候,需要做特殊处理,如下:
代码语言:javascript复制// JSON
let response = """
{
"name":"norman",
"birthday":"19900803",
"gender":"male",
"age":18
}
"""
// 定义结构体实现codable,一般情况下属性名要与JSON的key一致,否则需要额外处理
struct Student : Codable {
let name : String
let birthDay : String //⚠️这个名称与JSON中的名称不一致,所以需要做特殊处理
let gender : String
let age : Int
//注意,要写全所有的属性
enum CodingKeys : String, CodingKey {
case name
case birthDay = "birthday" // 匹配JSON中的字段和结构体中的字段
case gender
case age
}
}
// JSON 转为结构体、类,解码,decode
let decoder = JSONDecoder()
do {
let student : Student = try decoder.decode(Student.self, from: response.data(using: .utf8)!)
print(student.name, student.birthDay, student.gender, student.age) // norman 19900803 male 18
} catch {
print(error)
}
// 编码,encode,结构体、类转成JSON
let student = Student(name: "lavie", birthDay: "19910910", gender: "male", age: 29)
let encoder = JSONEncoder()
if let jsonData = try? encoder.encode(student) {
print(String(data: jsonData, encoding: .utf8)!) // {"gender":"male","age":29,"name":"lavie","birthday":"19910910"}
}
需要注意的是,当使用CodingKey解决字段不一致的情况的时候,哪怕只有一个字段不一致,也需要在里面将所有字段列出。
访问权限
模块和源文件
模块:独立的单元构建和发布单位,可以理解为一个特定功能的代码集合,并且可以使用Swift的import关键字导入到另一个模块。
需要注意的是,模块不是目录,也不是文件夹,而是某个功能的集合。比如UIKit框架、Foundation框架,还有第三方框架等,都是一个模块。
源文件:单个Swift源代码文件。注意,模块是包含源代码的。
访问权限
访问权限从大到小依次为:
- open:允许实体被定义模块中的任意源文件访问,也可以被另一模块的源文件通过导入该定义模块来访问。也就是说,可以在任何地方访问,包括override和继承
- public:跟open一样,可以在任何地方访问。但是与open不同的一点是,public在其他模块中不可以被override和继承,而在本模块内部可以被override和继承。
- internal(默认):允许实体被定义模块中的任意源文件访问,但是不能被该模块之外的任意源文件访问。新建文件时默认为Internal。
- fileprivate:将实体的使用范围限制在当前源文件内。
- private:只在当前类中能被访问。需要注意的是,Swift4之后,private属性的作用域扩大到了extention中,也就是说,在扩展中也能访问private属性。
playground可视化开发
我们可以在playground中进行界面开发:
代码语言:javascript复制import UIKit
import PlaygroundSupport // 需要导入PlaygroundSupport模块
//UIViewController
class NormanViewController : UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .orange
}
}
extension NormanViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 6
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = String(indexPath.row)
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("did select (indexPath.row)")
}
}
let vc = NormanViewController()
//将显示的内容赋值给PlaygroundPage.current.liveView
PlaygroundPage.current.liveView = vc
混合开发
在iOS开发中,难免会遇到Swift和Objective-C这两种语言同时存在的情况,如果在同一个项目中,两种语言并存,那么该项目就是一个混合项目。在混合项目中,就会存在两种语言相互调用的情况。那么,如何才能在一个项目中让两种语言可以相互调用呢?Apple给我们做好了桥接工作,不过,在OC项目中调用Swift,与在Swift项目中调用OC,处理的方式是不一样的。
Swift项目中调用Objective-C
1,新建一个Swift的iOS项目
2,创建一个OC的类,此时会有如下提示,选择Create Bridging Header:
这个提示的大意是:添加这个文件会创建一个Swift和OC的混合项目,你是否希望Xcode自动配置一个桥接头文件来让两种语言的类文件相互可见?
3,此时项目中会多出三个文件,分别是创建的两个OC文件和一个BridgingHeader文件
4,修改OC类文件如下:
代码语言:javascript复制@interface NormanButton : UIButton
- (void)test;
@end
@implementation NormanButton
- (void)test {
NSLog(@"test");
}
@end
5,在Bridging Header文件中导入所有的需要使用的Objective-C类的头文件,如下所示:
6,直接在Swift文件中使用OC文件中定义的内容:
代码语言:javascript复制import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = NormanButton()
button.test() // test
}
}
OC 项目中调用Swift
1,新建一个Objective-C项目
2,创建一个Swift的类,继承自NSObject,此时也会有上面的提示,选择Create Bridging Header
3,此时项目中会多出两个文件,分别是创建的Swift文件和Bridging Header文件。BridgingHeader文件里虽然什么都没有,但是其内部已经为我们做了很多事情。
4,Swift文件内容如下:
代码语言:javascript复制import UIKit
class NormanPerson: NSObject {
//在Swift代码中,将需要暴露给OC调用的属性和方法前面加上@objc关键字
@objc func eat() {
print("吃饭了~")
}
}
5,在OC类中导入头文件,注意此时导入的头文件是一个命名为 项目名-Swift.h 的头文件,而不是BridgingHeader文件。
6,在OC代码中调用Swift:
代码语言:javascript复制#import "ViewController.h"
//导入"项目名-Swift.h文件"
#import "NormanOC-Swift.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[Person alloc] init];
[person eat];
}
@end
关于Bridging Header文件
如果BridgingHeader文件不是Xcode帮助新建的,而是我们自己手动创建的头文件,那么会因为找不到“桥接文件”而编译失败,此时需要在Building setting里面搜索bridging关键字,将文件的路径值改成桥接文件的实际路径即可。
命名空间
命名空间(nameSpace)在C 、C#中是一个常见的概念,OC中是没有命名空间的概念的,但是在Swift中引入了命名空间的机制。
为什么需要命名空间
简而言之就是一句话:为了避免命名上的冲突。
在开发中,尤其是在多模块开发中,很难保证模块之间的类名不会重复。为了保证不同模块下同名的类可以正常使用而不报错,此时就需要命名空间了。命名空间可以保证即使创建的类名一样,但只要命名空间不一样,这些同名的类也是不一样的。所以,命名空间是一种安全机制,我们可以用它来防止冲突。
Swift中的类名的完整形式是:“命名空间 类名”,我们可以尝试在类中打印当前类来查看一下完整名字:
代码语言:javascript复制class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(self) // <NormanSwift.ViewController: 0x7fd976c0c070>
}
}
命名空间的查看与修改
从上例的打印结果来看,命名空间是我们项目的名字,那么如何查看命名空间的名字呢?
首先,使用源代码的形式打开项目中的info.plist文件;
然后找到CFBundleExecutable,它对应的值就是命名空间。
如果要修改命名空间,注意不要直接编辑info.plist,可以进入Build Settings中搜索product name进行修改。
在程序中获取命名空间
通过上面的介绍我们已经知道可以通过info.plist来获取命名空间的名称,那么如何代码解析info.plist,并拿到CFBundleExecutable所对应的值呢?如下:
代码语言:javascript复制 let nameSpace = Bundle.main.infoDictionary!["CFBundleExecutable"]
//返回的是一个可选型
print(nameSpace) // Optional(LavieSwift)
命名空间在开发中的使用
代码语言:javascript复制 //通过类名来新建一个类
func vcWithName(vcName : String) -> UIViewController? {
//获取命名空间
guard let nameSpace = Bundle.main.infoDictionary!["CFBundleExecutable"] as? String else {
print("获取失败")
return nil
}
//拼接完整的类
guard let vcClass = NSClassFromString(nameSpace "." vcName) else {
print("获取类名失败")
return nil
}
//转换成UIViewController
guard let vcType = vcClass as? UIViewController.Type else {
print("转换失败")
return nil
}
//根据类型创建对应的控制器
let vc = vcType.init()
return vc
}
总结
到这里,我们花了四篇文章的篇幅将常用的Swift基础语法介绍了一遍。接下来我们将开启Swift进阶阶段的学习,加油~