2
4

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

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

前提条件

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 GETTest 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)
        }
    }
}

このようなJSONがPOSTで送られるはず

{
    "id": 1,
    "name": "John"
}

非同期通信をするための下準備

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

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

CocoaPodsのセットアップ

sudo gem install cocoapods

CocoaPodsのセットアップする.

pod setup

これで,CocoaPodsのインストールと初期設定が完了した.
ここでXcodeでプロジェクトを閉じることを忘れない
その後,プロジェクトのルートディレクトリに移動し,以下のコマンドを実行する.

pod init

これでPodfileが作成された.

Podfileの中身を以下のように編集する.

Podfile
platform :ios, '17.0'
use_frameworks!

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

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

変更を保存し,Podfile を閉じる.
その後以下のコマンドを入力し,パッケージをインストールする.

pod install

これでプロジェクトのルートディレクトリに .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データとなるはずだ,

{
    "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に設定されているというエラー.

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

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

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4