iOS開発でhttp通信や非同期通信を使おう！

iOSソフトウェアを開発するようになってネットワーク接続を必要になってくるのではないだろうか．例えばHTTP通信や非同期通信をしたいことがあるだろう．

iOSではデフォルトで許可されてるのは`https通信`のみで，http通信は許可されていない．開発中はhttpと通信したいことも多いので，http通信の有効化を行う手順を紹介する．

# 前提条件
- [XCode のインストール](https://apps.apple.com/jp/app/xcode/id497799835?mt=12)
- Swiftで記述します

# iOSデバイスでネットワーク接続許可をする
## XCodeでの設定

> 今回は私用のプロジェクトを使います

まずXCodeの自分のプロジェクトのボタンをクリックする．

次に自分のプロジェクトのターゲットをクリックする．


`info`を選択し，`Custom macOS Application Target Properties`の欄の`+`ボタンを押し，`App Transport Security Settings`を追加する．

`App Transport Security Settings`の欄で`+`ボタンを押し，`Allow Arbitrary Loads`を追加する．

その後，`Allow Arbitary Loads`のvalueを`YES`にする．


これでネットワーク接続の許可は完成である．

# SwiftでのHTTP通信部分の作成

アプリを開いたときのエントリーを書く．
```YourAppNameApp.swift
import SwiftUI

@main
struct YourAppNameApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```

Userモデルを定義する
```User.swift
struct User: Codable {
    let id: Int
    let name: String
}
```

APIリクエストを行う．GETとPOSTのメソッドを含む．
```APIService.swift
import Foundation

class APIService {
    func testGet(_ url: String, completion: @escaping (Result<Any, Error>) -> Void) {
        let request = URLRequest(url: URL(string: url)!)
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                do {
                    let jsonData = try JSONSerialization.jsonObject(with: data, options: [])
                    completion(.success(jsonData))
                } catch {
                    completion(.failure(error))
                }
            }
        }
        task.resume()
    }
    
    func testPost(_ url: String, user: User, completion: @escaping (Result<Any, Error>) -> Void) {
        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        let encoder = JSONEncoder()
        let jsonData = try? encoder.encode(user)
        request.httpBody = jsonData
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
            } else if let data = data {
                do {
                    let jsonData = try JSONSerialization.jsonObject(with: data, options: [])
                    completion(.success(jsonData))
                } catch {
                    completion(.failure(error))
                }
            }
        }
        task.resume()
    }
}
```
`Test GET`と`Test POST`のボタンがあり，それぞれのリクエストの結果を表示する．
```ContentView.swift
import SwiftUI

struct ContentView: View {
    @State private var getResult = ""
    @State private var postResult = ""
    
    var body: some View {
        VStack {
            Button("Test GET") {
                let apiService = APIService()
                apiService.testGet("http://[getするためのエンドポイントを記載したURL]") { result in
                    switch result {
                    case .success(let data):
                        getResult = "GET Success: \(data)"
                    case .failure(let error):
                        getResult = "GET Error: \(error)"
                    }
                }
            }
            
            Text(getResult)
            
            Button("Test POST") {
            // POSTするUserインスタンスを設定
                let user = User(id: 1, name: "John")
                let apiService = APIService()
                apiService.testPost("http://[postするためのエンドポイントを記載したURL]", user: user) { result in
                    switch result {
                    case .success(let data):
                        postResult = "POST Success: \(data)"
                    case .failure(let error):
                        postResult = "POST Error: \(error)"
                    }
                }
            }
            
            Text(postResult)
        }
    }
}
```

:::note info
このようなJSONがPOSTで送られるはず
```json
{
    "id": 1,
    "name": "John"
}
```
:::


# 非同期通信をするための下準備
今回はボタンを押すと文字列を送信し，同意にデータを受信し続ける状態を作成する．
非同期通信をするためにCoCoaPodsというものをインストール

:::note info
**CocoaPodsとは**
iOSアプリ開発におけるライブラリ管理ツール．CocoaPodsを使用することで，簡単にサードパーティのライブラリをプロジェクトに統合することができる．
:::

## CocoaPodsのセットアップ
```bash
sudo gem install cocoapods
```

CocoaPodsのセットアップする．
```bash
pod setup
```

これで，CocoaPodsのインストールと初期設定が完了した．
ここで**Xcodeでプロジェクトを閉じることを忘れない**
その後，プロジェクトのルートディレクトリに移動し，以下のコマンドを実行する．
```bash
pod init
```
これでPodfileが作成された．

Podfileの中身を以下のように編集する．
```md:Podfile
platform :ios, '17.0'
use_frameworks!

target 'YourAppName' do
    pod 'Socket.IO-Client-Swift', '~> 16.0.1'
end
```

> `YourAppName` の部分は、実際のプロジェクト名に置き換えること．

変更を保存し，Podfile を閉じる．
その後以下のコマンドを入力し，パッケージをインストールする．
```
pod install
```
:::note info
これでプロジェクトのルートディレクトリに `.xcworkspace` ファイルが生成される．**以降は`.xcworkspace` ファイルを開いて**プロジェクトを開くようにしよう．
:::


# Swiftでの非同期通信部分の作成
アプリを開いたときのエントリーを書く．
```YourAppNameApp.swift
import SwiftUI

@main
struct YourAppNameApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
```
アプリを開いた瞬間から、非同期通信をするのはよろしくないので，一旦タイトル画面を作成する．
```ContentView.swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            VStack {
                NavigationLink(destination: DataView()) {
                    Text("データ送受信画面へ")
                }
            }
            .navigationBarTitle("メイン画面", displayMode: .inline)
        }
    }
}
```
通信をする画面の定義と、その機能の実装．
```DataView.swift
import SwiftUI

struct DataView: View {
    @ObservedObject var client: WebSocketClient
    
    init() {
        client = WebSocketClient()
        client.setup(url: "ws://websocketのURL")
    }
    
    let options = [1, 2, 3, 4, 5]
    @State private var selectedOption1 = 1
    @State private var selectedOption2 = 1
    
    var body: some View {
        VStack {
            HStack {
                Picker("id1", selection: $selectedOption1) {
                    ForEach(options, id: \.self) { option in
                        Text("\(option)")
                    }
                }
                .pickerStyle(MenuPickerStyle())
                
                Picker("id2", selection: $selectedOption2) {
                    ForEach(options, id: \.self) { option in
                        Text("\(option)")
                    }
                }
                .pickerStyle(MenuPickerStyle())
            }
            
            Button("スタート") {
                let message = ["id1": selectedOption1, "id2": selectedOption2]
                let jsonData = try? JSONEncoder().encode(message)
                if let jsonString = String(data: jsonData!, encoding: .utf8) {
                    client.send(jsonString)
                }
            }
            .disabled(!client.isConnected)
        }
        .onAppear {
            client.connect()
        }
        .onDisappear {
            client.disconnect()
        }
    }
}
```
このビューではid1,id2に1から5を選択して入れられるようにして，`スタート`を押したら送信をする．
このようなJSONデータとなるはずだ，
```json 
{
    "id1": 1,
    "id2": 2
}

```

リクエスト行うためのデータ通信の定義
```DataViewModel.swift
import Foundation

class WebSocketClient: NSObject, ObservableObject {

    private var webSocketTask: URLSessionWebSocketTask?

    @Published var messages: [String] = []
    @Published var isConnected: Bool = false

    func setup(url: String) {
        let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
        webSocketTask = urlSession.webSocketTask(with: URL(string: url)!)
    }

    func connect() {
        webSocketTask?.resume()
        receive()
    }

    func disconnect() {
        webSocketTask?.cancel(with: .goingAway, reason: nil)
    }

    func send(_ message: String) {
        let msg = URLSessionWebSocketTask.Message.string(message)
        webSocketTask?.send(msg) { error in
            if let error = error {
                print(error)
            }
        }
    }

    private func receive() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    print("Received text message: \(text)")
                    DispatchQueue.main.async {
                        self?.messages.append(text)
                    }
                case .data(let data):
                    print("Received binary message: \(data)")
                @unknown default:
                    fatalError()
                }
                self?.receive()
            case .failure(let error):
                print("Failed to receive message: \(error)")
            }
        }
    }
}

extension WebSocketClient: URLSessionWebSocketDelegate {
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
        print("didOpenWithProtocol")
        DispatchQueue.main.async {
            self.isConnected = true
        }
    }
    
    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        print("didCloseWith: closeCode: \(closeCode) reason: \(String(describing: reason))")
        DispatchQueue.main.async {
            self.isConnected = false
        }
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("didCompleteWithError error: \(String(describing: error))")
        DispatchQueue.main.async {
            self.isConnected = false
        }
    }
}
```

これで非同期通信ができる

# よく起こりうるエラー
Starscreamモジュールの最小デプロイメントターゲットがiOS 12.0であるのに対し，現在のプロジェクトのデプロイメントターゲットがiOS 1.0に設定されているというエラー．

https://github.com/socketio/socket.io-client-swift/issues/1485

SocketEngineがWebSocketDelegateプロトコルに準拠していないというエラー

https://stackoverflow.com/questions/76938393/xcode-socket-io-swift-type-socketengine-does-not-conform-to-protocol-websock

Xcodeのサンドボックスによって引き起こされている．rsync.sambaプロセスがSocketIO.framework内の特定のファイルへの書き込みアクセスを試みているが，サンドボックスによってブロックされているというエラー

https://stackoverflow.com/questions/76590131/error-while-build-ios-app-in-xcode-sandbox-rsync-samba-13105-deny1-file-w