[swift] APIを使った検索機能の実装

お菓子の虜 web APIの情報を使ってiOSで表示する

お菓子の虜 Web API

e.g.: https://www.sysbird.jp/webapi/?apikey=guest&keyword=%E3%82%AB%E3%83%AC%E3%83%BC%E5%91%B3&format=json
https://www.sysbird.jp/webapi/?apikey=guest&keyword=%E3%82%AB%E3%83%AC%E3%83%BC%E5%91%B3&format=json&max=10

request parameters: id, type, year, keyword, max, order
response: status, count, item(id, name, maker, price, type, regist, url, image, comment)

@StateObject, @State, @Bindingを学ぶ
検索画面はContentView.swiftで一覧表示処理、OkashiData.swiftでカスタムクラスで実装

OkashiData.swift
L StructではObservableOjectは利用できない、 classで定義する必要がある
  L ObservableOjectはカスタムクラス内でデータの状態を管理するために利用

class OkashiData: ObservableObject {
    
    func searchOkashi(keyword: String) async {
        print(keyword)
    }
}

Taskは一連の流れを処理する
ContentView.swift

struct ContentView: View {
    
    @StateObject var okashiDataList = OkashiData()
    @State var inputText = ""
    
    var body: some View {
        VStack {
            TextField("キーワード", text: $inputText,
                      prompt: Text("キーワードを入力してください。"))
                .onSubmit {
                    Task {
                        await okashiDataList.searchOkashi(keyword: inputText)
                    }
                }
                .submitLabel(.search)
                .padding()
        }
    }
}

WebAPIのリクエストURLを組み立てる
OkashiData.swift

    func searchOkashi(keyword: String) async {
        print(keyword)
        
        guard let keyword_encode = keyword.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
            return
        }
        
        guard let req_url = URL(string:
            "https://www.sysbird.jp/webapi/?apikey=guest&format=json&keyword=\(keyword_encode)&max=10&order=r") else {
            return
        }
        print(req_url)
    }

レスポンスデータ(JSON)を記憶する構造体
L Itemとすることで複数の構造体を保持できる配列として保存
  L ?を付与してnilを許容するオプショナル型として宣言する

    struct ResultJson: Codable {
        struct Item: Codable {
            let name: String?
            let url: URL?
            let image: URL?
        }
        let item: [Item]?
    }

URLSessionでデータをダウンロード
URLSession.sharedで簡素に実行

        do {
            let(data, _) = try await URLSession.shared.data(from: req_url)
            
            let decoder = JSONDecoder()
            let json = try decoder.decode(ResultJson.self, from: data)
            
            print(json)
        } catch {
            print("エラーが出ました")
        }

最近のiOSはマルチコアプロセッサが搭載されている

### 取得したデータをListで一覧表示
Itemの構造体を作成し、List表示
L Identifiableに準拠すると、一意に識別できる型として定義できる
  L uuidを用いてランダムな一意の値を生成

import SwiftUI
import UIKit

struct OkashiItem: Identifiable {
    let id = UUID()
    let name: String
    let link: URL
    let image: URL
}

@StateObject、ObservableObjectを使用すると@Publishedを使用できる
プロパティラッパーはプロパティをラップして機能を追加する

            guard let items = json.item else {return}
            
            DispatchQueue.main.async {
                self.okashiList.removeAll()
            }
            
            for item in items {
                if let name = item.name,
                   let link = item.url,
                   let image = item.image {
                   let okashi = OkashiItem(name: name, link: link, image: image)
                    DispatchQueue.main.async {
                        self.okashiList.append(okashi)
                    }
                }
            }
            print(self.okashiList)

### リストで一覧表示

    var body: some View {
        VStack {
            TextField("キーワード", text: $inputText,
                      prompt: Text("キーワードを入力してください。"))
                .onSubmit {
                    Task {
                        await okashiDataList.searchOkashi(keyword: inputText)
                    }
                }
                .submitLabel(.search)
                .padding()
            List(okashiDataList.okashiList) { okashi in
                HStack {
                    AsyncImage(url: okashi.image) { image in
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(height: 40)
                    } placeholder: {
                        ProgressView()
                    }
                    Text(okashi.name)
                }
            }
        }
    }

### Webページの表示
SFSafariViewControllerでWebページを表示

SafariView.swift
L SafariServicesでアプリの中でsafariを起動する

import SafariServices

struct SafariView: UIViewControllerRepresentable {
    
    var url: URL
    
    func makeUIViewController(context: Context) -> SFSafariViewController {
        return SFSafariViewController(url: url)
    }
    
    func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context){
        
    }
}

struct ContentView: View {
    
    @StateObject var okashiDataList = OkashiData()
    @State var inputText = ""
    @State var showSafari = false
    
    var body: some View {
        VStack {
            TextField("キーワード", text: $inputText,
                      prompt: Text("キーワードを入力してください。"))
                .onSubmit {
                    Task {
                        await okashiDataList.searchOkashi(keyword: inputText)
                    }
                }
                .submitLabel(.search)
                .padding()
            List(okashiDataList.okashiList) { okashi in
                
                Button(action: {
                    showSafari.toggle()
                }){
                    HStack {
                        AsyncImage(url: okashi.image) { image in
                            image
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .frame(height: 40)
                        } placeholder: {
                            ProgressView()
                        }
                        Text(okashi.name)
                    }
                }
            }
        }
    }
}

これは凄い