【iOS/SwiftUI】Alamofireを使用してサーバーから受け取ったデータをカスタムクラスに変換

目次

概要

前回でAlamofireを使用してサーバーからデータを取得したが、エンコードされていてそのまま使えなかったりしていた。
今回は、取得したjsonデータを自分で作成したクラスに格納することをしていく。
これで実際の実装で使えるようになるるはずだ。

ソースコード

アプリ実行部分

import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            CallZipCodeView()
        }
    }
}

JSONのデータを格納するクラス

import Foundation

struct ZipCodeResponseBase: Decodable {
    var message: String?
    var results: [Address]
    var status: Int
    
    struct Address: Decodable {
        var address1: String
        var address2: String
        var address3: String
        var kana1: String
        var kana2: String
        var kana3: String
        var prefcode: String
        var zipcode: String
    }
}

View表示部分

import SwiftUI

struct CallZipCodeView: View {
    @ObservedObject private var viewModel = CallZipCodeViewModel()
    @FocusState private var focusState: Bool
    
    var body: some View {
        ZStack {
            VStack {
                zipcodeView()
                resultTextField()
                jsonTextView()
            }
            if viewModel.isShownProgressView {
                progressView()
            }
        }
    }
    
    private func progressView() -> some View {
        return ZStack {
            Color.gray.opacity(0.2)
            ProgressView()
                .progressViewStyle(.circular)
                .padding()
                .tint(Color.white)
                .background(Color.black)
                .cornerRadius(8)
                .scaleEffect(1.2)
        }
        .edgesIgnoringSafeArea(.all)
    }
    
    private func zipcodeView() -> some View {
        return VStack {
            Text("郵便番号を入力してください")
            Text("(ハイフン不要)")
            TextField("郵便番号", text: $viewModel.zipCode)
                .padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
                .textFieldStyle(.roundedBorder)
                .focused($focusState)
                .keyboardType(.numberPad)
            Button {
                focusState = false
                viewModel.pushedSearchButton()
            } label: {
                Text("郵便番号取得")
            }
        }
        .onTapGesture {
            focusState = false
        }
    }
    
    private func resultTextField() -> some View {
        return VStack(alignment: .leading, spacing: 0) {
            HStack(alignment: .lastTextBaseline) {
                Text("〒")
                    .padding(EdgeInsets(top: 10, leading: 20, bottom: 0, trailing: 0))
                TextField("郵便番号", text: $viewModel.zipText)
                    .textFieldStyle(.roundedBorder)
                    .disabled(true)
                    .frame(width: 120)
            }
            Text("住所")
                .padding(EdgeInsets(top: 10, leading: 20, bottom: 0, trailing: 20))
            TextField("住所", text: $viewModel.addressText)
                .textFieldStyle(.roundedBorder)
                .disabled(true)
                .padding()
            Text("住所カナ")
                .padding(EdgeInsets(top: 10, leading: 20, bottom: 0, trailing: 20))
            TextField("住所カナ", text: $viewModel.addressKana)
                .textFieldStyle(.roundedBorder)
                .disabled(true)
                .padding()
        }
    }
    
    private func jsonTextView() -> some View {
        return VStack {
            TextEditor(text: $viewModel.jsonData)
                .disabled(true)
                .padding()
        }
    }
}

処理部分

import Alamofire
import Foundation

class CallZipCodeViewModel: ObservableObject {
    @Published var zipCode: String = ""
    @Published var jsonData = ""
    @Published var isShownProgressView = false
    
    @Published var zipText: String = ""
    @Published var addressText: String = ""
    @Published var addressKana: String = ""
    
    private let baseUrl = "https://zipcloud.ibsnet.co.jp/api/search"
    
    func pushedSearchButton() {
        isShownProgressView = true
        jsonData = ""
        zipText = ""
        addressText = ""
        addressKana = ""
        
        let params: [String : String] = ["zipcode" : zipCode]
        
        AF.request(baseUrl, method: .get, parameters: params).responseData { [weak self] response in
            guard let self else {
                return
            }
            
            self.isShownProgressView = false
            guard let data = response.data else {
                print("No Data")
                return
            }
            do {
                let decoder: JSONDecoder = JSONDecoder()
                let zipCodeResponseBase: ZipCodeResponseBase = try decoder.decode(ZipCodeResponseBase.self, from: data)
                guard let zipCodeData = zipCodeResponseBase.results.first else {
                    print("results transfer error")
                    return
                }
                
                self.jsonData = "\(zipCodeResponseBase)"
                
                self.zipText = zipCodeData.zipcode
                self.addressText = zipCodeData.address1 + zipCodeData.address2 + zipCodeData.address3
                self.addressKana = zipCodeData.kana1 + zipCodeData.kana2 + zipCodeData.kana3
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

デモ動画

詳細

レスポンスクラスの作成

ただデータを受け取っただけでは実装で利用するためのデータとしては使えない。
今回の例で使用した郵便番号検索APIでは以下のようなデータが返ってくる。

{
	"message": null,
	"results": [
		{
			"address1": "北海道",
			"address2": "美唄市",
			"address3": "上美唄町協和",
			"kana1": "ホッカイドウ",
			"kana2": "ビバイシ",
			"kana3": "カミビバイチョウキョウワ",
			"prefcode": "1",
			"zipcode": "0790177"
		}
	],
	"status": 200
}

jsonデータは上記のように「:」の左にあるキーの部分と「:」の右にある値の羅列になっている。
そして、「[ ]」で囲まれている箇所が配列部分になる。
構造は以下のようになっている。

レスポンスデータ

キー名(型名)内容
message(String?型)メッセージ文字列
results(住所データ型(仮))データの中身
status(Int型)通信のステータスコード

住所データ型(仮)

キー名(型名)内容
address1(String型)住所(都道府県)
address2(String型)住所(市区町村)
address3(String型)住所(町名)
kana1(String型)住所フリガナ(都道府県)
kana2(String型)住所フリガナ(市区町村)
kana3(String型)住所フリガナ(町名)
prefcode(String型)都道府県の番号
zipcode(String型)郵便番号

上記の表を元に、自作のクラスに格納するには以下のように作成する。

struct ZipCodeResponseBase: Decodable {
    var message: String?
    var results: [Address]
    var status: Int
    
    struct Address: Decodable {
        var address1: String
        var address2: String
        var address3: String
        var kana1: String
        var kana2: String
        var kana3: String
        var prefcode: String
        var zipcode: String
    }
}

変数名はキー名と同じに、そして、型名もjsonのデータから推測する。基本的に、数字と数値の違いは「”」(ダブルクォーテーション)に囲まれているかどうかで判別する。

そして、今回の配列の内部のように、オリジナルの型がまた別にあれば新たに作成する必要がある。

加えて、jsonデータを格納するクラスはDecodableを継承しておく必要がある。
では、今度はレスポンス部分を見ていこう。

レスポンス処理部分でjsonファイルをカスタムクラスに格納する

jsonデータをカスタムクラスに格納する場合は以下のように実装する。

do {
    let decoder: JSONDecoder = JSONDecoder()
    let zipCodeResponseBase: ZipCodeResponseBase = try decoder.decode(ZipCodeResponseBase.self, from: data)
    guard let zipCodeData = zipCodeResponseBase.results.first else {
        print("results transfer error")
        return
    }
    
    self.jsonData = "\(zipCodeResponseBase)"
    
    self.zipText = zipCodeData.zipcode
    self.addressText = zipCodeData.address1 + zipCodeData.address2 + zipCodeData.address3
    self.addressKana = zipCodeData.kana1 + zipCodeData.kana2 + zipCodeData.kana3
} catch {
    print(error.localizedDescription)
}
  1. JSONDecoderのインスタンスを作成
  2. JSONDecoderクラスのdecodeメソッドを使用する
    第一引数にカスタムクラスの型を、第二引数にjsonデータを設定する。

今回、jsonのデータの中身はZipCodeResponseBaseクラスのresults部分のため、その部分だけ抽出する。
また、results部分は配列のため、先頭のデータを使用する。

あとは、取得したデータを元にデータを格納している。

参考ページ