78169


# 質問

Swift での`HTTP GET`の「非同期処理」の書き方がわかりません。

## やりたいこと

APIにアクセスして、データをダウンロードできたタイミングでTextに表示させたいです。

`httpGet()`で取得した`Data()`を戻り値としたいのですが、やり方がわかりません。

## エラー

Voidのクロージャーの中で`return data`しており、そのエラーが出ています。

**エラーメッセージ:**

[code]

    Unexpected non-void return value in void function
    
[/code]

**ソースコード:**

[code]

    import SwiftUI
    import Foundation
    
    class API {
        public static func httpGet(_ urlString: String) -> Data?{
            guard let url = URL(string: urlString) else { return nil }
            
            var request = URLRequest(url: url)
            
            request.httpMethod = "GET"
    
            let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
                if let error = error {
                    print(error)
                }
                
                guard let data = data else { return }
                
                return data;
            })
            
            task.resume()
            
            return nil
        }
        
    }
    
    class SampleAPI : API{
        public static func getData() -> String? {
            let urlString = "https://api.isevenapi.xyz/api/iseven/6/"
                    
            if let data = super.httpGet(urlString) {
                let dataString = String(data: data, encoding: .utf8)
                return dataString
            } else {
                return nil
            }
        }
    }
    
    struct ContentView : View {
        @State var text: String = "Hello"
        
        var body : some View {
            VStack {
                Text(self.text)
                
                Button(
                    action: {
                        self.showData()
                    },
                    label: {
                        Text("Button")
                    }
                )
            }
        }
        
        func showData() {
            if let text = SampleAPI.getData() {
                print(text)
                self.text = text
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
[/code]


CC BY-SA 4.0
2021-07-13T15:04:43.297
78162
2021-07-13T23:08:50.943
2021-07-13T16:22:48.110
3060
40856
question
swift http
SwiftでのHTTP GETの非同期処理の書き方がわかりません。


２つの方法があります。

一つ目は、完了ハンドラーのクロージャーがあるクラスに、`view`を弱参照で持ち、完了ハンドラーの中でプロパティを書き替える方法。非同期処理的にはこちらがより理想的な回答だと思います。

ソースにするとこんな感じになります。  
書き替えたいプロパティをメンバーに持たずに、引数で渡されたインスタンスのプロパティを書き替える形にするとより良いかもしれません。

[code]

    import SwiftUI
    import Foundation
    
    class API {
        weak var view: ContentView
    
        public static func httpGet(_ urlString: String) -> Data?{
            guard let url = URL(string: urlString) else { return nil }
            
            var request = URLRequest(url: url)
            
            request.httpMethod = "GET"
    
            let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
                if let error = error {
                    print(error)
                }
                
                guard let data = data else { return }
                
                // data を textに変換
                let dataString = String(data: data, encoding: .utf8)
    
                // textをviewにセット
                view.text = dataString
            })
            
            task.resume()
            
            return nil
        }
        
    }
    
    class SampleAPI : API{
    
        public static func getData() -> String? {
            let urlString = "https://api.isevenapi.xyz/api/iseven/6/"
                    
            if let data = super.httpGet(urlString) {
                let dataString = String(data: data, encoding: .utf8)
                return dataString
            } else {
                return nil
            }
        }
    }
    
    struct ContentView : View {
        @State var text: String = "Hello"
        
        var body : some View {
            VStack {
                Text(self.text)
                
                Button(
                    action: {
                        self.showData()
                    },
                    label: {
                        Text("Button")
                    }
                )
            }
        }
        
        func showData() {
            let api = SampleAPI()
            api.view = self
            if let text = api.getData() {
                print(text)
                self.text = text
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
[/code]

もう一つはセマフォアを使って、`API`クラスの完了ハンドラーが完了するまでリターンするのを待つ方法です。

この方法は非同期処理を同期的に待つ事になるので、行っていることのトレースは楽ですが、非同期処理を行っているという思想的にはあまりお勧め出来ません。

サンプルはこんな感じです

[code]

    import SwiftUI
    import Foundation
    
    class API {
        public static func httpGet(_ urlString: String) -> Data? {
            guard let url = URL(string: urlString) else { return nil }
            
            var request = URLRequest(url: url)
            var text = ""
            let semahpore: DispatchSemaphore = DispatchSemaphore(value: 0)
            
            request.httpMethod = "GET"
    
            let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
                guard { semaphore.signal() }
                if let error = error {
                    print(error)
                }
                
                guard let data = data else { return }
                
                text = String(data: data, encoding: .utf8)
            })
            
            task.resume()
            let timeout = semaphore.wait(timeout: .now + .second(20))
            
            if timeout == .success {
                return text
            } else {
                returan "failed"
            }
        }
        
    }
    
    class SampleAPI : API{
        public static func getData() -> String? {
            let urlString = "https://api.isevenapi.xyz/api/iseven/6/"
                    
            if let data = super.httpGet(urlString) {
                let dataString = String(data: data, encoding: .utf8)
                return dataString
            } else {
                return nil
            }
        }
    }
    
    struct ContentView : View {
        @State var text: String = "Hello"
        
        var body : some View {
            VStack {
                Text(self.text)
                
                Button(
                    action: {
                        self.showData()
                    },
                    label: {
                        Text("Button")
                    }
                )
            }
        }
        
        func showData() {
            if let text = SampleAPI.getData() {
                print(text)
                self.text = text
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
[/code]


CC BY-SA 4.0
2021-07-13T22:38:10.983
78167
2021-07-13T22:38:10.983
14745
78162
answer


> Swift でのHTTP GETの「非同期処理」の書き方がわかりません。

最も一般的なのは、自前の完了ハンドラを導入する形でしょう。

こんな感じになります。

[code]

    import SwiftUI
    
    class API {
        public static func httpGet(_ urlString: String, completion: @escaping(Data?)->Void){
            guard let url = URL(string: urlString) else {
                completion(nil)
                return
            }
            
            var request = URLRequest(url: url)
            
            request.httpMethod = "GET"
            
            let task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
                if let error = error {
                    print(error)
                    completion(nil)
                    return
                }
                
                guard let data = data else {
                    completion(nil)
                    return
                }
                
                completion(data)
            })
            
            task.resume()
        }
    }
    
    class SampleAPI : API{
        public static func getData(completion: @escaping(String?)->Void) {
            let urlString = "https://api.isevenapi.xyz/api/iseven/6/"
            
            super.httpGet(urlString) { data in
                if let data = data,
                   let dataString = String(data: data, encoding: .utf8) {
                    completion(dataString)
                } else {
                    completion(nil)
                }
            }
        }
    }
    
    struct ContentView : View {
        @State var text: String = "Hello"
        
        var body : some View {
            VStack {
                Text(self.text)
                
                Button(
                    action: {
                        self.showData()
                    },
                    label: {
                        Text("Button")
                    }
                )
            }
        }
        
        func showData() {
            SampleAPI.getData { text in
                if let text = text {
                    print(text)
                    self.text = text
                } else {
                    print("Got nil")
                }
            }
        }
    }
    
[/code]

完了ハンドラを使うパターンは、ほんの少し発想を変え、呼び出し方法も変えないといけないので、最初は大変難しく感じられるかもしれませんが、慣れれば書き換えはそれほど大変ではなく、何よりも現時点では最も一般的な方法と言えるでしょう。

* * *

もしSwift5.5(Xcode 13)を使用することができる(今の所ターゲットをiOS15世代以降にする必要がある)なら、Swift
5.5で導入されたasync/awaitを使うのが簡単でしょう。まだ時期尚早かと思うので、ここには詳しくは書きませんが、beta版のXcode
13を導入済みなら、色々試してみられると良いでしょう。


CC BY-SA 4.0
2021-07-13T23:08:50.943
78169
2021-07-13T23:08:50.943
13972
78162
answer