美文网首页
Alamofire框架详细解析(三) —— 高级用法(二)

Alamofire框架详细解析(三) —— 高级用法(二)

作者: 刀客传奇 | 来源:发表于2020-10-19 09:27 被阅读0次

    版本记录

    版本号 时间
    V1.0 2020.10.16 星期五

    前言

    关于网络请求有很多优秀的三方框架,比较常用的比如说OC的AFNetworking,这里我们就一起学习一下Swift的网络请求框架 - Alamofire。感兴趣的可以看下面几篇文章。
    1. Alamofire框架详细解析(一) —— 基本概览(一)
    2. Alamofire框架详细解析(二) —— 高级用法(一)

    源码

    1. Swift

    首先看下工程组织结构

    接着,我们看下sb中的内容:

    接着就是源码了

    1. SecureStore.swift
    
    import Foundation
    import Security
    
    struct SecureStore {
      let secureStoreQueryable: SecureStoreQueryable
    
      init(secureStoreQueryable: SecureStoreQueryable) {
        self.secureStoreQueryable = secureStoreQueryable
      }
    
      func setValue(_ value: String, for userAccount: String) throws {
        guard let encodedPassword = value.data(using: .utf8) else {
          throw SecureStoreError.stringToDataConversionError
        }
        var query = secureStoreQueryable.query
        query[String(kSecAttrAccount)] = userAccount
    
        var status = SecItemCopyMatching(query as CFDictionary, nil)
        switch status {
        case errSecSuccess:
          var attributesToUpdate: [String: Any] = [:]
          attributesToUpdate[String(kSecValueData)] = encodedPassword
    
          status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
          if status != errSecSuccess {
            throw error(from: status)
          }
        case errSecItemNotFound:
          query[String(kSecValueData)] = encodedPassword
          status = SecItemAdd(query as CFDictionary, nil)
          if status != errSecSuccess {
            throw error(from: status)
          }
        default:
          throw error(from: status)
        }
      }
    
      func getValue(for userAccount: String) throws -> String? {
        var query = secureStoreQueryable.query
        query[String(kSecMatchLimit)] = kSecMatchLimitOne
        query[String(kSecReturnAttributes)] = kCFBooleanTrue
        query[String(kSecReturnData)] = kCFBooleanTrue
        query[String(kSecAttrAccount)] = userAccount
    
        var queryResult: AnyObject?
        let status = withUnsafeMutablePointer(to: &queryResult) {
          SecItemCopyMatching(query as CFDictionary, $0)
        }
    
        switch status {
        case errSecSuccess:
          guard
            let queriedItem = queryResult as? [String: Any],
            let passwordData = queriedItem[String(kSecValueData)] as? Data,
            let password = String(data: passwordData, encoding: .utf8)
            else {
              throw SecureStoreError.dataToStringConversionError
          }
          return password
        case errSecItemNotFound:
          return nil
        default:
          throw error(from: status)
        }
      }
    
      func removeValue(for userAccount: String) throws {
        var query = secureStoreQueryable.query
        query[String(kSecAttrAccount)] = userAccount
        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
          throw error(from: status)
        }
      }
    
      func removeAllValues() throws {
        let query = secureStoreQueryable.query
        let status = SecItemDelete(query as CFDictionary)
        guard status == errSecSuccess || status == errSecItemNotFound else {
          throw error(from: status)
        }
      }
    
      func error(from status: OSStatus) -> SecureStoreError {
        let message = SecCopyErrorMessageString(status, nil) as String? ?? NSLocalizedString("Unhandled Error", comment: "")
        return SecureStoreError.unhandledError(message: message)
      }
    }
    
    2. SecureStoreError.swift
    
    import Foundation
    
    enum SecureStoreError: Error {
      case stringToDataConversionError
      case dataToStringConversionError
      case unhandledError(message: String)
    }
    
    // MARK: - LocalizedError
    extension SecureStoreError: LocalizedError {
      var errorDescription: String? {
        switch self {
        case .stringToDataConversionError:
          return NSLocalizedString("String to Data conversion error", comment: "")
        case .dataToStringConversionError:
          return NSLocalizedString("Data to String conversion error", comment: "")
        case .unhandledError(let message):
          return NSLocalizedString(message, comment: "")
        }
      }
    }
    
    3. SecureStoreQueryable.swift
    
    import Foundation
    
    protocol SecureStoreQueryable {
      var query: [String: Any] { get }
    }
    
    struct GenericPasswordQueryable {
      let service: String
      let accessGroup: String?
    
      init(service: String, accessGroup: String? = nil) {
        self.service = service
        self.accessGroup = accessGroup
      }
    }
    
    // MARK: - SecureStoreQueryable
    extension GenericPasswordQueryable: SecureStoreQueryable {
      var query: [String: Any] {
        var query: [String: Any] = [:]
        query[String(kSecClass)] = kSecClassGenericPassword
        query[String(kSecAttrService)] = service
        // Access group if target environment is not simulator
        #if !targetEnvironment(simulator)
        if let accessGroup = accessGroup {
          query[String(kSecAttrAccessGroup)] = accessGroup
        }
        #endif
        return query
      }
    }
    
    4. Repository.swift
    
    struct Repository {
      let name: String
      let fullName: String
      let description: String?
    
      enum CodingKeys: String, CodingKey {
        case name
        case description
        case fullName = "full_name"
      }
    }
    
    // MARK: - Decodable
    extension Repository: Decodable {
      init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        fullName = try container.decode(String.self, forKey: .fullName)
        description = try? container.decode(String.self, forKey: .description)
      }
    }
    
    5. Commit.swift
    
    struct Commit {
      let authorName: String
      let message: String
    
      enum CodingKeys: String, CodingKey {
        case authorName = "name"
        case message
        case commit
        case author
      }
    }
    
    // MARK: - Decodable
    extension Commit: Decodable {
      init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let commit = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .commit)
        message = try commit.decode(String.self, forKey: .message)
        let author = try commit.nestedContainer(keyedBy: CodingKeys.self, forKey: .author)
        authorName = try author.decode(String.self, forKey: .authorName)
      }
    }
    
    6. Repositories.swift
    
    struct Repositories: Decodable {
      let items: [Repository]
    }
    
    7. GitHubConstants.swift
    
    enum GitHubConstants {
      static let clientID = "ENTER_CLIENT_ID"
      static let clientSecret = "ENTER_CLIENT_SECRET"
      static let redirectURI = "gitonfire://"
      static let scope = "repo user"
      static let authorizeURL = "https://github.com/login/oauth/authorize"
    }
    
    8. GitHubAccessToken.swift
    
    struct GitHubAccessToken: Decodable {
      let accessToken: String
      let tokenType: String
    
      enum CodingKeys: String, CodingKey {
        case accessToken = "access_token"
        case tokenType = "token_type"
      }
    }
    
    9. TokenManager.swift
    
    class TokenManager {
      let userAccount = "accessToken"
      static let shared = TokenManager()
    
      let secureStore: SecureStore = {
        let accessTokenQueryable = GenericPasswordQueryable(service: "GitHubService")
        return SecureStore(secureStoreQueryable: accessTokenQueryable)
      }()
    
      func saveAccessToken(gitToken: GitHubAccessToken) {
        do {
          try secureStore.setValue(gitToken.accessToken, for: userAccount)
        } catch let exception {
          print("Error saving access token: \(exception)")
        }
      }
    
      func fetchAccessToken() -> String? {
        do {
          return try secureStore.getValue(for: userAccount)
        } catch let exception {
          print("Error fetching access token: \(exception)")
        }
        return nil
      }
    
      func clearAccessToken() {
        do {
          return try secureStore.removeValue(for: userAccount)
        } catch let exception {
          print("Error clearing access token: \(exception)")
        }
      }
    }
    
    10. GitAPIManager.swift
    
    import Foundation
    import Alamofire
    
    class GitAPIManager {
      static let shared = GitAPIManager()
    
      let sessionManager: Session = {
        let configuration = URLSessionConfiguration.af.default
        configuration.requestCachePolicy = .returnCacheDataElseLoad
        let responseCacher = ResponseCacher(behavior: .modify { _, response in
          let userInfo = ["date": Date()]
          return CachedURLResponse(
            response: response.response,
            data: response.data,
            userInfo: userInfo,
            storagePolicy: .allowed)
        })
    
        let networkLogger = GitNetworkLogger()
        let interceptor = GitRequestInterceptor()
    
        return Session(
          configuration: configuration,
          interceptor: interceptor,
          cachedResponseHandler: responseCacher,
          eventMonitors: [networkLogger])
      }()
    
      func fetchPopularSwiftRepositories(completion: @escaping ([Repository]) -> Void) {
        searchRepositories(query: "language:Swift", completion: completion)
      }
    
      func fetchCommits(for repository: String, completion: @escaping ([Commit]) -> Void) {
        sessionManager.request(GitRouter.fetchCommits(repository))
          .responseDecodable(of: [Commit].self) { response in
            guard let commits = response.value else {
              return
            }
            completion(commits)
          }
      }
    
      func searchRepositories(query: String, completion: @escaping ([Repository]) -> Void) {
        sessionManager.request(GitRouter.searchRepositories(query))
          .responseDecodable(of: Repositories.self) { response in
            guard let repositories = response.value else {
              return completion([])
            }
            completion(repositories.items)
          }
      }
    
    
      func fetchAccessToken(accessCode: String, completion: @escaping (Bool) -> Void) {
        sessionManager.request(GitRouter.fetchAccessToken(accessCode))
          .responseDecodable(of: GitHubAccessToken.self) { response in
            guard let token = response.value else {
              return completion(false)
            }
            TokenManager.shared.saveAccessToken(gitToken: token)
            completion(true)
          }
      }
    
      func fetchUserRepositories(completion: @escaping ([Repository]) -> Void) {
        sessionManager.request(GitRouter.fetchUserRepositories)
          .responseDecodable(of: [Repository].self) { response in
            guard let repositories = response.value else {
              return completion([])
            }
            completion(repositories)
          }
      }
    }
    
    11. GitNetworkLogger.swift
    
    import Foundation
    import Alamofire
    
    class GitNetworkLogger: EventMonitor {
      let queue = DispatchQueue(label: "com.raywenderlich.gitonfire.networklogger")
    
      func requestDidFinish(_ request: Request) {
        print(request.description)
      }
    
      func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        guard let data = response.data else {
          return
        }
        if let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) {
          print(json)
        }
      }
    }
    
    12. GitRequestInterceptor.swift
    
    import Foundation
    import Alamofire
    
    class GitRequestInterceptor: RequestInterceptor {
      let retryLimit = 5
      let retryDelay: TimeInterval = 10
    
      func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var urlRequest = urlRequest
        if let token = TokenManager.shared.fetchAccessToken() {
          urlRequest.setValue("token \(token)", forHTTPHeaderField: "Authorization")
        }
        completion(.success(urlRequest))
      }
    
      func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        let response = request.task?.response as? HTTPURLResponse
        //Retry for 5xx status codes
        if
          let statusCode = response?.statusCode,
          (500...599).contains(statusCode),
          request.retryCount < retryLimit {
            completion(.retryWithDelay(retryDelay))
        } else {
          return completion(.doNotRetry)
        }
      }
    }
    
    13. GitRouter.swift
    
    import Foundation
    import Alamofire
    
    enum GitRouter {
      case fetchUserRepositories
      case searchRepositories(String)
      case fetchCommits(String)
      case fetchAccessToken(String)
    
      var baseURL: String {
        switch self {
        case .fetchUserRepositories, .searchRepositories, .fetchCommits:
          return "https://api.github.com"
        case .fetchAccessToken:
          return "https://github.com"
        }
      }
    
      var path: String {
        switch self {
        case .fetchUserRepositories:
          return "/user/repos"
        case .searchRepositories:
          return "/search/repositories"
        case .fetchCommits(let repository):
          return "/repos/\(repository)/commits"
        case .fetchAccessToken:
          return "/login/oauth/access_token"
        }
      }
    
      var method: HTTPMethod {
        switch self {
        case .fetchUserRepositories:
          return .get
        case .searchRepositories:
          return .get
        case .fetchCommits:
          return .get
        case .fetchAccessToken:
          return .post
        }
      }
    
      var parameters: [String: String]? {
        switch self {
        case .fetchUserRepositories:
          return ["per_page": "100"]
        case .searchRepositories(let query):
          return ["sort": "stars", "order": "desc", "page": "1", "q": query]
        case .fetchCommits:
          return nil
        case .fetchAccessToken(let accessCode):
          return [
            "client_id": GitHubConstants.clientID,
            "client_secret": GitHubConstants.clientSecret,
            "code": accessCode
          ]
        }
      }
    }
    
    // MARK: - URLRequestConvertible
    extension GitRouter: URLRequestConvertible {
      func asURLRequest() throws -> URLRequest {
        let url = try baseURL.asURL().appendingPathComponent(path)
        var request = URLRequest(url: url)
        request.method = method
        if method == .get {
          request = try URLEncodedFormParameterEncoder()
            .encode(parameters, into: request)
        } else if method == .post {
          request = try JSONParameterEncoder().encode(parameters, into: request)
          request.setValue("application/json", forHTTPHeaderField: "Accept")
        }
        return request
      }
    }
    
    14. GitNetworkReachability.swift
    
    import UIKit
    import Alamofire
    
    class GitNetworkReachability {
      static let shared = GitNetworkReachability()
      let reachabilityManager = NetworkReachabilityManager(host: "www.google.com")
      let offlineAlertController: UIAlertController = {
        UIAlertController(title: "No Network", message: "Please connect to network and try again", preferredStyle: .alert)
      }()
    
      func startNetworkMonitoring() {
        reachabilityManager?.startListening { status in
          switch status {
          case .notReachable:
            self.showOfflineAlert()
          case .reachable(.cellular):
            self.dismissOfflineAlert()
          case .reachable(.ethernetOrWiFi):
            self.dismissOfflineAlert()
          case .unknown:
            print("Unknown network state")
          }
        }
      }
    
      func showOfflineAlert() {
        let rootViewController = UIApplication.shared.windows.first?.rootViewController
        rootViewController?.present(offlineAlertController, animated: true, completion: nil)
      }
    
      func dismissOfflineAlert() {
        let rootViewController = UIApplication.shared.windows.first?.rootViewController
        rootViewController?.dismiss(animated: true, completion: nil)
      }
    }
    
    15. AppDelegate.swift
    
    import UIKit
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        //GitNetworkReachability.shared.startNetworkMonitoring()
        return true
      }
    }
    

    后记

    本篇主要讲述了Alamofire框架的基本概览,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:Alamofire框架详细解析(三) —— 高级用法(二)

          本文链接:https://www.haomeiwen.com/subject/weozpktx.html