Swift 作为现代、高效、安全的编程言语,其背后有许多高档特性为之支撑。

『 Swift 最佳实践 』系列对常用的言语特性逐个进行介绍,助力写出更简练、更优雅的 Swift 代码,快速完成从 OC 到 Swift 的改变。

该系列内容首要包含:

  • Optional
  • Enum
  • Closure
  • Protocol
  • Generic
  • Property Wrapper
  • Structured Concurrent
  • Result builder
  • Error Handle
  • Advanced Collections (Asyncsequeue/OptionSet/Lazy)
  • Expressible by Literal
  • Pattern Matching
  • Metatypes(.self/.Type/.Protocol)

ps. 本系列不是入门级语法教程,需求有必定的 Swift 基础

本文是系列文章的第二篇,介绍 Enum,内容首要包含 Swift Enum 高档特性以及典型运用场景。

Swift 赋以 Enum 十分强大的才能,C-like Enum 与之不可同日而语。

充分利用 Enum 特性写出更优雅、更安全的代码。

Enum 特性


首先,简要罗列一下 Swift Enum 具有的特性:

Value

C-like Enum 中每个 case 都相关一个整型数值,而 Swift Enum 更灵活:

  • 默许,case 不相关任何数值

  • 能够供给类似 C-like Enum 那样的数值 (Raw Values), 但类型更丰厚,能够是 Int、Float、String、Character

    enum Direction: String {
      case east  // 等价于 case east = "east"case west
      case south
      case north = "n"
    }
    

    如上,关于 String 类型的 Raw Value,若没指定值,默许为 case name

    关于 Int/Float 类型,默许值从 0 开始,顺次 +1

    经过 rawValue 特点能够获取 case 对应的 Raw Value

    let direction = Direction.east
    let value = direction.rawValue  // "east"
    

    关于 Raw-Values Enum,编译器会自动生成初始化办法:

    // 因为并不是一切输入的 rawValue 都能找到匹配的 case
    // 故,这是个 failable initializer,在没有匹配的 case 时,回来 nil 
    init?(rawValue: RawValueType)
    let direction = Direction.init(rawValue: "east")
    
  • 还可认为 case 指定恣意类型的相关值 (Associated-Values)

    enum UGCContent {
      case text(String)
      case image(Image, description: String?)
      case audio(Audio, autoPlay: Bool)
      case video(Video, autoPlay: Bool)
    }
    let text = UGCContent.text("Hello world!")
    let video = UGCContent.video(Video.init(), autoplay: true)
    

    还可认为相关值供给默许值:

    enum UGCContent {
      case text(String = "Hello world!")
      case image(Image, description: String?)
      case audio(Audio, autoPlay: Bool = true)
      case video(Video, autoPlay: Bool = true)
    }
    let text = UGCContent.text()
    let content = UGCContent.video(Video.init())
    

    如下,即能够经过 if-case-let,也能够经过 switch-case-let 匹配 enum 并提取相关值:

    if case let .video(video,  autoplay) = content {
      print(video, autoplay)
    }
    switch content {
      case let .text(text):
        print(text)
      case let .image(image, description):
        print(image, description)
      case let .audio(audio, autoPlay):
        print(audio, autoPlay)
      case let .video(video, autoPlay):
        print(video, autoPlay)
    }
    

First-class type

Swift Enum 作为「 First-class type 」,有许多传统上只要 Class 才具有的特性:

  • 能够有计算特点 (computed properties),当然了存储特点是不能有的,如:

    enum UGCContent {
      var description: String {
        switch self {
          case let .text(text):
            return text
          case let .image(_, description):
            return description ?? "image"
          case let .audio(_, autoPlay):
            return "audio, autoPlay: (autoPlay)"
          case let .video(_, autoPlay):
            return "video, autoPlay: (autoPlay)"
        }
      }
    }
    let content = UGCContent.video(Video.init())
    print(content.description)  
    
  • 能够有实例办法/静态办法

  • 能够有初始化办法,如:

    enum UGCContent {
      init(_ text: String) {
        self = .text(text)
      }
    }
    let text = UGCContent.init("Hi!")
    
  • 能够有扩展 (extension),也能够完成协议,如:

    extension UGCContent: Equatable {
      static func == (lhs: Self, rhs: Self) -> Bool {
        return false
      }
    }
    

Recursive Enum

Enum 相关值类型能够是其枚举自身,称其为递归枚举 (Recursive Enum)。

如下,界说了 Enum 类型的链表接点 LinkNode,包含 2 个 case:

  • end,表示尾部节点
  • link,其相关值 next 的类型为 LinkNode
// 关键词 indirect 也能够放在 enum 前面
// indirect enum LinkNode<NodeType> {
enum LinkNode<NodeType> {
  case end(NodeType)
  indirect case link(NodeType, next: LinkNode)
}
func sum(rootNode: LinkNode<Int>) -> Int {
  switch rootNode {
    case let .end(value):
      return value
    case let .link(value, next: next):
      return value + sum(rootNode: next)
  }
}
let endNode = LinkNode.end(3)
let childNode = LinkNode.link(9, next: endNode)
let rootNode = LinkNode.link(5, next: childNode)
let sum = sum(rootNode: rootNode)

Iterating over Enum Cases

有时,我们希望遍历 Enum 的一切 cases,或是获取第一个 case,此时 CaseIterable 派上用场了:

public protocol CaseIterable {
    /// A type that can represent a collection of all values of this type.
    associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element
    /// A collection of all values of this type.
    static var allCases: Self.AllCases { get }
}

能够看到,CaseIterable 协议有一个静态特点:allCases

关于没有相关值 (Associated-Values) 的枚举,当声明其恪守 CaseIterable 时,会自动合成 allCases 特点:

enum Weekday: String, CaseIterable {
  case sunday, monday, tuesday, wednesday, thursday, friday, saturday
  /* 自动合成的完成
  static var allCases: Self.AllCases { [sunday, monday, tuesday, wednesday, thursday, friday, saturday] }
  */
}
// sunday, monday, tuesday, wednesday, thursday, friday, saturday
let weekdays = Weekday.allCases.map{ $0.rawValue }.joined(separator: ", ")

关于有相关值的枚举,不会自动合成 allCases,因为相关值无法确定

Swift 最佳实践之 Enum

此时,需求手动完成 CaseIterable 协议:

enum UGCContent: CaseIterable {
  case text(String = "ugc")
  case image(Image, description: String? = nil)
  case audio(Audio, autoPlay: Bool = false)
  case video(Video, autoplay: Bool = true)
  static var allCases: [UGCContent] {
    [.text(), image(Image("")), .audio(Audio()), .video(Video())]
  }
}

Equatable

没有相关值的枚举,默许能够履行判等操作 (==),无需声明恪守 Equatable 协议:

let sun = Weekday.sunday
let mon = Weekday.monday
sum == Weekday.sunday // true
sum == mon // false

关于有相关值的枚举,若需求履行判等操作,需显式声明恪守 Equatable 协议:

Swift 最佳实践之 Enum

// 因为 NodeType:Equatable
// 故,系统会为 LinkNode 自动合成 static func == (lhs: Self, rhs: Self) -> Bool
// 无需手写 == 的完成,只需显式声明 LinkNode 恪守 Equatable 即可
enum LinkNode<NodeType: Equatable>: Equatable {
  case end(NodeType)
  indirect case link(NodeType, next: LinkNode)
}

运用


相关值能够说极大丰厚了 Swift Enum 的运用场景,而 C-like Enum 限于只是个 Int 型值,只能用于一些简单的状态、分类等。

因此,我们需求改变思维,善用 Swift Enum。关于一组相关的「值」、「状态」、「操作」等等,都能够经过 Enum 封装,附加信息用 Associated-Values 表示。

规范库中的 Enum

Enum 在 Swift 规范库中有很多运用,典型的如:

  • Optional,在 「 Swift 最佳实践之 Enum 」中有详细介绍,不再赘述

    @frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
        /// The absence of a value.
        ///
        /// In code, the absence of a value is typically written using the `nil`
        /// literal rather than the explicit `.none` enumeration case.
        case none
        /// The presence of a value, stored as `Wrapped`.
        case some(Wrapped)
    }
    
  • Result,用于封装成果,如网络恳求、办法回来值等

    @frozen 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)
    }
    
  • Error Handle,如 EncodingErrorDecodingError

    public enum DecodingError : Error {
        /// An indication that a value of the given type could not be decoded because
        /// it did not match the type of what was found in the encoded payload.
        ///
        /// As associated values, this case contains the attempted type and context
        /// for debugging.
        case typeMismatch(Any.Type, DecodingError.Context)
        /// An indication that a non-optional value of the given type was expected,
        /// but a null value was found.
        ///
        /// As associated values, this case contains the attempted type and context
        /// for debugging.
        case valueNotFound(Any.Type, DecodingError.Context)
        /// An indication that a keyed decoding container was asked for an entry for
        /// the given key, but did not contain one.
        ///
        /// As associated values, this case contains the attempted key and context
        /// for debugging.
        case keyNotFound(CodingKey, DecodingError.Context)
        /// An indication that the data is corrupted or otherwise invalid.
        ///
        /// As an associated value, this case contains the context for debugging.
        case dataCorrupted(DecodingError.Context)
    }
    
  • Never,是一个没有 case 的枚举,用于表示一个办法永远不会正常回来

    /// The return type of functions that do not return normally, that is, a type
    /// with no values.
    ///
    /// Use `Never` as the return type when declaring a closure, function, or
    /// method that unconditionally throws an error, traps, or otherwise does
    /// not terminate.
    ///
    ///     func crashAndBurn() -> Never {
    ///         fatalError("Something very, very bad happened")
    ///     }
    @frozen public enum Never {
      // ...
    }
    

实践中的运用

  • Error Handle

    Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated.

    (Documentation-errorhandling)

    正如 Apple 官方文档所言,界说过错模型是 Enum 的典型运用场景之一,如上节说到的 EncodingErrorDecodingError

    将不同的过错类型界说为 case,过错相关信息以相关值的形式附加在相应 case 上。

    在闻名网络库 Alamofire 中也有很好的运用,如:

    public enum AFError: Error {
        /// The underlying reason the `.multipartEncodingFailed` error occurred.
        public enum MultipartEncodingFailureReason {
            /// The `fileURL` provided for reading an encodable body part isn't a file `URL`.
            case bodyPartURLInvalid(url: URL)
            /// The filename of the `fileURL` provided has either an empty `lastPathComponent` or `pathExtension.
            case bodyPartFilenameInvalid(in: URL)
            /// The file at the `fileURL` provided was not reachable.
            case bodyPartFileNotReachable(at: URL)
            // ...
        }
        /// The underlying reason the `.parameterEncoderFailed` error occurred.
        public enum ParameterEncoderFailureReason {
            /// Possible missing components.
            public enum RequiredComponent {
                /// The `URL` was missing or unable to be extracted from the passed `URLRequest` or during encoding.
                case url
                /// The `HTTPMethod` could not be extracted from the passed `URLRequest`.
                case httpMethod(rawValue: String)
            }
            /// A `RequiredComponent` was missing during encoding.
            case missingRequiredComponent(RequiredComponent)
            /// The underlying encoder failed with the associated error.
            case encoderFailed(error: Error)
        }
        // ...
    }
    
  • 命名空间

    命名空间有助于提高代码结构化,Swift 中命名空间是隐式的,即以模块 (Module) 为边界,不同的模块属于不同的命名空间,无法显式界说命名空间 (没有 namespace 关键词)。

    我们能够经过 no-case Enum 创立自界说(伪)命名空间,完成更小粒度的代码结构化

    为什么是用 Enum,而不是 Struct 或 Class?

    原因在于,没有 case 的 Enum 是无法实例化

    而 Struct、Class 必定是能够实例化的

    如,Apple 的 Combine 库用 Enum 界说了命名空间 PublishersSubscribers

    Swift 最佳实践之 Enum
    Swift 最佳实践之 Enum

    如上,经过命名空间 Publishers 将相关功能组织在一起,代码更加结构化

    也不需求在每个详细类型前重复增加前缀/后缀,如:

    • MulticastPublisher –> Publishers.Multicast
    • SubscribeOnPublisher –> Publishers.SubscribeOn
    • FirstPublisher –> Publishers.First
  • 界说常量

    思维跟上面说到的命名空间是一样的,能够将一组相关常量界说在一个 Enum 中,如:

    class TestView: UIView {
      enum Dimension {
        static let left = 18.0
        static let right = 18.0
        static let top = 10
        static let bottom = 10
      }
      // ...
    }
    enum Math {
      static let  = 3.14159265358979323846264338327950288
      static let e = 2.71828182845904523536028747135266249
      static let u = 1.45136923488338105028396848589202744
    }
    
  • API Endpoints

    App/Module 内网络恳求 (API) 模型也能够用 Enum 来界说,API 参数经过相关值绑定

    闻名网络库 Moya 便是根据这个思维:

    public enum GitHub {
      case zen
      case userProfile(String)
      case userRepositories(String)
    }
    extension GitHub: TargetType {
        public var baseURL: URL { URL(string: "https://api.github.com")! }
        public var path: String {
            switch self {
            case .zen:
                return "/zen"
            case .userProfile(let name):
                return "/users/(name.urlEscaped)"
            case .userRepositories(let name):
                return "/users/(name.urlEscaped)/repos"
            }
        }
        public var method: Moya.Method { .get }
        public var task: Task {
            switch self {
            case .userRepositories:
                return .requestParameters(parameters: ["sort": "pushed"], encoding: URLEncoding.default)
            default:
                return .requestPlain
            }
        }
    }
    let provider = MoyaProvider() 
    provider.request(.zen) { result in 
      // ... 
    }
    

    如上,将 GitHub 相关的 API (zenuserProfileuserRepositories) 封装在 enum GitHub 中。

    终究经过 provider.request(.zen) 的方式建议恳求。

    强烈建议大家读一下 Moya 源码

  • UI State

    能够将页面各种状态封装在 Enum 中:

    enum UIState<DataType, ErrorType> {
      case loading
      case empty
      case loaded(DataType)
      case failured(ErrorType)
    }
    
  • Associated-Values case 能够当作函数用

    一般用于函数式 mapflatMap,如 Array、Optional、Combine 等:

    func uiState(_ loadedData: String?) -> UIState<String, Error> {
      // 等价于 loadedData.map { UIState.loaded($0) } ?? .empty
      loadedData.map(UIState.loaded) ?? .empty
    }
    // loadedTmp 本质上便是个函数:(String) -> UIState<String, Error> 
    let loadedTmp = UIState<String, Error>.loaded)
    

    Swift 最佳实践之 Enum

小结

Swift Enum 功能十分强大,具有许多传统上只要 Class 才有的特性。

相关值进一步极大丰厚了 Enum 的运用场景。

关于一组具有相关性的 「值」、「状态」、「操作」等,都能够用 Enum 封装。

鼓舞优先考虑运用 Enum。

参考资料

Documentation-enumerations

GitHub – Moya/Moya: Network abstraction layer written in Swift

GitHub – Alamofire/Alamofire: Elegant HTTP Networking in Swift

www.swiftbysundell.com/articles/po…

appventure.me/guides/adva…