📚

【Swift】完全版URLSessionメモ 外部ライブラリ未使用で同期非同期通信、JSON、バイナリーファイルを想定【Xcode12】

2022/03/10に公開

はじめに

今記事ではSwiftで使われるWebAPI通信用の処理、URLSessionについてまとめました。

想定されるHTTP通信の使用パターン

  • 非同期処理でJSON形式を取得する処理
  • 非同期処理でバイナリーファイルを取得する処理
  • 非同期処理でデータモデルにデコードして取得する処理
  • 同期処理でJSON形式を取得する処理
  • 同期処理でバイナリーファイルを取得する処理
  • 同期処理でデータモデルにデコードして取得する処理

前提条件

JSON形式をデコードするために用意したデータ型クラスUsersInfoが存在します。
また、エラーハンドリングの助けの為にNetworkErrorが存在します。

// JSONの結果をデコードする為のクラス Decobableクラスを継承するとstruct/classをデコード可能にする。
struct UsersInfo : Decodable {
   var id : Int
   var name : String
}
// 失敗時には単一の戻り値しか用意できないのでErrorの中で分岐させる
enum NetworkError: Error {
   case unknown
   case invalidResponse // デコードなど失敗した場合は無効なレスポンスデータに分類する。
   case invalidURL // 無効なURL
}

※今回使用するサンプルコードです。JSON形式で返ってきます。2個目はバイナリファイルの為に画像を用意しました。
let urlString = "http://www.json-generator.com/api/json/get/bQIUnAqOKW?indent=2"
let photoUrlString = "https://pbs.twimg.com/media/E2iq9orVgAMFxL0?format=jpg&name=large"

非同期処理でJSON形式を取得する処理

これからは呼び出し側と、処理側で2つ書いていきます
処理側はNerworkManagerクラスのメソッドで用意しております。

// 1)非同期処理でJSON形式を取得する処理
NetworkManager.shared.requestAsyncJson(urlstring:urlString , success: {(dictionary) in
   // 通信成功時のクロージャ
   print(dictionary)
}) { (error) in
   // 通信失敗時のクロージャ
   print(error)
}

// 1)非同期処理でJSON形式を取得する処理
   func requestAsyncJson(urlstring: String,
                success: @escaping (Dictionary<String, Any>) -> (), // 通信成功時のクロージャ
                failure: @escaping (Error) -> ()){    // 通信失敗時のクロージャ

       guard let url = URL(string: urlstring) else {
           return failure(NetworkError.invalidURL)
       }
       var request = URLRequest(url: url)  //Requestを生成
       
       /*
       // 下記のような特別な事を行わない場合は以降のrequest部分がURL型でも処理自体は問題なく動作する
       
       // GET,POST,PUT,DELETEなどHTTPメソッドを変更する場合
        request.httpMethod = "GET" // 何も設定しないとGETリクエストになるがコードのサンプルのため、明示的に書いている。
       
       // Bodyを付与する場合
        request.httpBody = "usedid=hoge&password=hoge1234".data(using: .utf8)  // useridとpasswordを付与する場合
       
       // Headerを付与する場合
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")  //jsonでヘッダーに付与
        request.allHTTPHeaderFields = ["usedid": "hoge","password": "hoge1234"]

       // Queryを付与する場合
       var urlComponents = URLComponents(string: "http://www.json-generator.com/api/json/get/bQIUnAqOKW?indent=2")!  //URLComponentsでURLを生成
       urlComponents.queryItems = [
           URLQueryItem(name: "usedid", value: "hoge"),
           URLQueryItem(name: "password", value: "hoge1234")
       ]
       var request = URLRequest(url: urlComponents.url!)  // Qurtyを付与する場合はこちらのrequestを使用する。
       */

       URLSession.shared.dataTask(with: request, completionHandler: {data , response, error in
           
           if let error = error {
               print(error.localizedDescription)
               failure(error)
               return
           }
           
           guard let data = data,
                 let response = response as? HTTPURLResponse else {
               print("データもしくはレスポンスがnilの状態です")
               failure(NetworkError.unknown)
               return
           }
           
           if response.statusCode == 200 {
               do {
                   let object = try JSONSerialization.jsonObject(with: data, options: []) as! Dictionary<String, Any>
                   print(object["id"] as! Int) // 辞書型に変換したJSONから値id(Int)を取り出す。
                   print(object["name"] as! String)  // 辞書型に変換したJSONから値name(String)を取り出す。
                   
                   // コールバックを行う
                   success(object)
               } catch let error {
                   failure(error)
               }
               
           } else {
               print("statusCode:\(response.statusCode)")
               failure(NetworkError.unknown)
           }
       }).resume()
   }

非同期処理でバイナリーファイルを取得し、UIImage型に変換する処理

NetworkManager.shared.requestAsyncJson(urlstring:photoUrlString , success: {(image) in
// 通信成功時のクロージャ
print(image)
}) { (error) in
// 通信失敗時のクロージャ
print(error)
}
// 2)非同期処理でバイナリーファイルを取得する処理
func requestAsyncBinaryFile(urlstring: String,
             success: @escaping (UIImage) -> (), // 通信成功時のクロージャ
             failure: @escaping (Error) -> ()){    // 通信失敗時のクロージャ

    guard let url = URL(string: urlstring) else {
        return failure(NetworkError.invalidURL)
    }
    var request = URLRequest(url: url)  //Requestを生成
    
    /*
    // 下記のような特別な事を行わない場合は以降のrequest部分がURL型でも処理自体は問題なく動作する
    
    // GET,POST,PUT,DELETEなどHTTPメソッドを変更する場合
     request.httpMethod = "GET" // 何も設定しないとGETリクエストになるがコードのサンプルのため、明示的に書いている。
    
    // Bodyを付与する場合
     request.httpBody = "usedid=hoge&password=hoge1234".data(using: .utf8)  // useridとpasswordを付与する場合
    
    // Headerを付与する場合
     request.setValue("application/json", forHTTPHeaderField: "Content-Type")  //jsonでヘッダーに付与
     request.allHTTPHeaderFields = ["usedid": "hoge","password": "hoge1234"]

    // Queryを付与する場合
    var urlComponents = URLComponents(string: "http://www.json-generator.com/api/json/get/bQIUnAqOKW?indent=2")!  //URLComponentsでURLを生成
    urlComponents.queryItems = [
        URLQueryItem(name: "usedid", value: "hoge"),
        URLQueryItem(name: "password", value: "hoge1234")
    ]
    var request = URLRequest(url: urlComponents.url!)  // Qurtyを付与する場合はこちらのrequestを使用する。
    */

    URLSession.shared.dataTask(with: request, completionHandler: {data , response, error in
        
        if let error = error {
            print(error.localizedDescription)
            failure(error)
            return
        }
        
        guard let data = data,
              let response = response as? HTTPURLResponse else {
            print("データもしくはレスポンスがnilの状態です")
            failure(NetworkError.unknown)
            return
        }
        
        if response.statusCode == 200 {
            guard let image = UIImage(data: data) else {
                failure(NetworkError.invalidResponse)
                return
            }
            success(image)
        } else {
            print("statusCode:\(response.statusCode)")
            failure(NetworkError.unknown)
        }
    }).resume()
}

非同期処理でデータモデルにデコードして取得する処理

NetworkManager.shared.requestAsyncDecodeDatamodel(urlstring:urlString , success: {(userinfo) in
// 通信成功時のクロージャ
print(userinfo)
}) { (error) in
// 通信失敗時のクロージャ
print(error)
}
// 3)非同期処理でデータモデルにデコードして取得する処理
func requestAsyncDecodeDatamodel(urlstring: String,
             success: @escaping (UsersInfo) -> (), // 通信成功時のクロージャ
             failure: @escaping (Error) -> ()){    // 通信失敗時のクロージャ

    guard let url = URL(string: urlstring) else {
        return failure(NetworkError.invalidURL)
    }
    var request = URLRequest(url: url)  //Requestを生成
    
    /*
    // 下記のような特別な事を行わない場合は以降のrequest部分がURL型でも処理自体は問題なく動作する
    
    // GET,POST,PUT,DELETEなどHTTPメソッドを変更する場合
     request.httpMethod = "GET" // 何も設定しないとGETリクエストになるがコードのサンプルのため、明示的に書いている。
    
    // Bodyを付与する場合
     request.httpBody = "usedid=hoge&password=hoge1234".data(using: .utf8)  // useridとpasswordを付与する場合
    
    // Headerを付与する場合
     request.setValue("application/json", forHTTPHeaderField: "Content-Type")  //jsonでヘッダーに付与
     request.allHTTPHeaderFields = ["usedid": "hoge","password": "hoge1234"]

    // Queryを付与する場合
    var urlComponents = URLComponents(string: "http://www.json-generator.com/api/json/get/bQIUnAqOKW?indent=2")!  //URLComponentsでURLを生成
    urlComponents.queryItems = [
        URLQueryItem(name: "usedid", value: "hoge"),
        URLQueryItem(name: "password", value: "hoge1234")
    ]
    var request = URLRequest(url: urlComponents.url!)  // Qurtyを付与する場合はこちらのrequestを使用する。
    */

    URLSession.shared.dataTask(with: request, completionHandler: {data , response, error in
        
        if let error = error {
            print(error.localizedDescription)
            failure(error)
            return
        }
        
        guard let data = data,
              let response = response as? HTTPURLResponse else {
            print("データもしくはレスポンスがnilの状態です")
            failure(NetworkError.unknown)
            return
        }
        
        if response.statusCode == 200 {
            do {
                // 結果:UserInfo構造体(Struct)を取得できる -> UserInfo
                let userinfo = try JSONDecoder().decode(UsersInfo.self, from: data)
                success(userinfo)
            } catch let error {
                failure(NetworkError.invalidResponse)
                print(":エラー:\(error)") // JSONの値がIDがIntなのに、StructでIDをStringと宣言している時などエラーになる。
            }
        } else {
            print("statusCode:\(response.statusCode)")
            failure(NetworkError.unknown)
        }
    }).resume()
}

同期処理でJSON形式を取得する処理

let semaphore = DispatchSemaphore(value: 0)
NetworkManager.shared.requestAsyncJson(urlstring:urlString , success: {(dictionary) in
// 通信成功時のクロージャ
print(dictionary)
semaphore.signal()
}) { (error) in
// 通信失敗時のクロージャ
print(error)
semaphore.signal()
}
semaphore.wait()

同期処理でバイナリーファイルを取得する処理

let semaphoreHoge = DispatchSemaphore(value: 0)
NetworkManager.shared.requestAsyncJson(urlstring:photoUrlString , success: {(image) in
  // 通信成功時のクロージャ
  print(image)
  semaphoreHoge.signal()
}) { (error) in
  // 通信失敗時のクロージャ
  print(error)
  semaphoreHoge.signal()
}
semaphoreHoge.wait()

同期処理でデータモデルにデコードして取得する処理

let semaphorefuga = DispatchSemaphore(value: 0)

NetworkManager.shared.requestAsyncDecodeDatamodel(urlstring:urlString , success: {(userinfo) in
  // 通信成功時のクロージャ
  print(userinfo)
  semaphorefuga.signal()
}) { (error) in
  // 通信失敗時のクロージャ
  print(error)
  semaphorefuga.signal()
}
semaphorefuga.wait()

参考

こちらの記事を複合的に参考しました。

https://qiita.com/shungo_m/items/64564fd822a7558ac7b1

https://qiita.com/shocho0101/items/1cabe4aeb5edc9181328

https://qiita.com/shtnkgm/items/d552bd3cf709266a9050

Discussion