版本记录
版本号 | 时间 |
---|---|
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(_ request: DataRequest, didParseResponse response: DataResponse) {
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) -> 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框架的基本概览,感兴趣的给个赞或者关注~~~