SwiftUIで郵便番号検索APIを使う

## 読んでほしい人
- SwiftUIを勉強している人
- 郵便番号検索APIを使ってみたい人

## 補足情報
こちらのサイトのAPIを使って今回はアプリを作っていこうと思います。
https://zipcloud.ibsnet.co.jp/doc/api


## 記事の内容
SwiftUIでAPIを使ったアプリの学習をしていて、アウトプットのために色々作ってみたいなと思い入門レベルの郵便番号検索ができるアプリを作ってみました。

今回はこんな構造のJSONに合わせて、structを定義します。
```json
{
	"message": null,
	"results": [
		{
			"address1": "東京都",
			"address2": "三鷹市",
			"address3": "",
			"kana1": "ﾄｳｷｮｳﾄ",
			"kana2": "ﾐﾀｶｼ",
			"kana3": "",
			"prefcode": "13",
			"zipcode": "1810000"
		}
	],
	"status": 200
}
```

モデルを作成する。構造体をネストして、JSONが`results: [{}]`ネストしている構造に合わせて、２個作成する。
```swift
struct ZipCode: Codable {
    var message: String?
    var results: [Result]
    var status: Int
}

struct Result: Codable {
    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側に今回は、ロジックも書いてます。練習用のアプリということで、メソッドをクラスに書いて分けるのまではやってないです。検索Formがあって、郵便番号`-`なしの7桁ではなかったら、エラーメッセージを表示する正規表現を使っております。入力してボタンを押すと、検索をするメソッドが実行されます。
```swift
struct ContentView: View {
    @State private var zipCode = ""
    @State private var results = [Result]()
    @State private var errorMessage = ""
    
    var body: some View {
        VStack {
            TextField("郵便番号を入力してください", text: $zipCode)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button(action: {
                if validate(zipCode: zipCode) {
                    fetchResults()
                }
            }) {
                Text("Search")
            }
            
            if !errorMessage.isEmpty {
                Text(errorMessage)
                    .foregroundColor(.red)
            }
            
            List(results, id: \.zipcode) { result in
                VStack(alignment: .leading) {
                    Text("郵便番号: \(result.zipcode + result.prefcode)")
                    Text("都道府県コード: \(result.prefcode)")
                    Text("住所カナ: \(result.kana1 + result.kana2 + result.kana3)")
                    Text("住所: \(result.address1 + result.address2 + result.address3)")
                }
            }
        }
    }
    
    func validate(zipCode: String) -> Bool {
        let zipCodePattern = "^[0-9]{3}-?[0-9]{4}$"
        let zipCodePredicate = NSPredicate(format: "SELF MATCHES %@", zipCodePattern)
        let isValid = zipCodePredicate.evaluate(with: zipCode)
        if !isValid {
            errorMessage = "郵便番号は-なしで7桁で入力してください!"
        } else {
            errorMessage = ""
        }
        return isValid
    }
    
    func fetchResults() {
        guard let url = URL(string: "https://zipcloud.ibsnet.co.jp/api/search?zipcode=\(zipCode)") else {
            return
        }
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                do {
                    let decodedData = try JSONDecoder().decode(ZipCode.self, from: data)
                    DispatchQueue.main.async {
                        self.results = decodedData.results
                    }
                } catch {
                    print("Failed to decode JSON: \(error.localizedDescription)")
                }
            }
        }.resume()
    }
}
```

こちらが全体のコード
```swift
import SwiftUI

struct ZipCode: Codable {
    var message: String?
    var results: [Result]
    var status: Int
}

struct Result: Codable {
    var address1: String
    var address2: String
    var address3: String
    var kana1: String
    var kana2: String
    var kana3: String
    var prefcode: String
    var zipcode: String
}

struct ContentView: View {
    @State private var zipCode = ""
    @State private var results = [Result]()
    @State private var errorMessage = ""
    
    var body: some View {
        VStack {
            TextField("郵便番号を入力してください", text: $zipCode)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            Button(action: {
                if validate(zipCode: zipCode) {
                    fetchResults()
                }
            }) {
                Text("Search")
            }
            
            if !errorMessage.isEmpty {
                Text(errorMessage)
                    .foregroundColor(.red)
            }
            
            List(results, id: \.zipcode) { result in
                VStack(alignment: .leading) {
                    Text("郵便番号: \(result.zipcode + result.prefcode)")
                    Text("都道府県コード: \(result.prefcode)")
                    Text("住所カナ: \(result.kana1 + result.kana2 + result.kana3)")
                    Text("住所: \(result.address1 + result.address2 + result.address3)")
                }
            }
        }
    }
    
    func validate(zipCode: String) -> Bool {
        let zipCodePattern = "^[0-9]{3}-?[0-9]{4}$"
        let zipCodePredicate = NSPredicate(format: "SELF MATCHES %@", zipCodePattern)
        let isValid = zipCodePredicate.evaluate(with: zipCode)
        if !isValid {
            errorMessage = "郵便番号は-なしで7桁で入力してください!"
        } else {
            errorMessage = ""
        }
        return isValid
    }
    
    func fetchResults() {
        guard let url = URL(string: "https://zipcloud.ibsnet.co.jp/api/search?zipcode=\(zipCode)") else {
            return
        }
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data = data {
                do {
                    let decodedData = try JSONDecoder().decode(ZipCode.self, from: data)
                    DispatchQueue.main.async {
                        self.results = decodedData.results
                    }
                } catch {
                    print("Failed to decode JSON: \(error.localizedDescription)")
                }
            }
        }.resume()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

## 最後に
今回は、郵便番号検索アプリを作ってAPIの使い方について学習してみました。ご興味あるかたは参考にして使ってみてください。