前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Swift基础语法(四)

Swift基础语法(四)

作者头像
拉维
发布2020-07-06 10:22:13
3.8K0
发布2020-07-06 10:22:13
举报
文章被收录于专栏:iOS小生活iOS小生活

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进阶阶段的学习,加油~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档