Swift基础语法(四)

2020-07-06 10:22:13 浏览数 (1)

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源代码文件。注意,模块是包含源代码的。

访问权限

访问权限从大到小依次为:

  1. open:允许实体被定义模块中的任意源文件访问,也可以被另一模块的源文件通过导入该定义模块来访问。也就是说,可以在任何地方访问,包括override和继承
  2. public:跟open一样,可以在任何地方访问。但是与open不同的一点是,public在其他模块中不可以被override和继承,而在本模块内部可以被override和继承。
  3. internal(默认):允许实体被定义模块中的任意源文件访问,但是不能被该模块之外的任意源文件访问。新建文件时默认为Internal。
  4. fileprivate:将实体的使用范围限制在当前源文件内。
  5. 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进阶阶段的学习,加油~

0 人点赞