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

2020-10-19  本文已影响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框架的基本概览,感兴趣的给个赞或者关注~~~

上一篇下一篇

猜你喜欢

热点阅读