SwiftUIで書くiOSアプリハンズオン（QiitaClientを作ってみよう）

SwiftUIを使ったハンズオン用に記事を作成しました

## 使用するAPI
今回はQiitaAPiを題材とします。([ドキュメント](https://qiita.com/api/v2/docs))

登録不要で使えて、例えば、 https://qiita.com//api/v2/items で、投稿を取得できます。

```javascript
[{
    rendered_body: "...",
    coediting: false,
    comments_count: 0,
    created_at: "2017-11-09T22:50:51+09:00",
    group: null,
    id: "fe412a67b64e793d138b",
    likes_count: 0,
    private: false,
    reactions_count: 0,
    tags: [{
            name: "processing",
            versions: []
        },
        {
            name: "openFrameworks",
            versions: []
        }
    ],
    title: "【openFrameworks 初心者冒険記5】逃げまくってた...三角関数の扉を再ノック。sin() cos() サインコサイン....。メディアアートで避けては通れない？最初の壁",
    updated_at: "2017-11-09T22:50:51+09:00",
    url: "http://qiita.com/39_isao/items/fe412a67b64e793d138b",
    user: {
        description: "鮭とメロンパンが好物な奥田民生になりたいボーイです。 最近はてなブログ始めました http://sudara-bluse.hatenablog.com/",
        facebook_id: "",
        followees_count: 52,
        followers_count: 44,
        github_login_name: null,
        id: "39_isao",
        items_count: 103,
        linkedin_id: "",
        location: "",
        name: "",
        organization: "",
        permanent_id: 76843,
        profile_image_url: "https://qiita-image-store.s3.amazonaws.com/0/76843/profile-images/1508030106",
        twitter_screen_name: "sudara_bluse",
        website_url: "http://sudara-bluse.hatenablog.com/"
    }
}, 
{,,,}
]
```

今回は、以下のデータを使ってiOSアプリを作ってみます。

- 投稿タイトル
- ユーザアイコン
- URL

完成したアプリ

![20200221170444.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/95dea727-f26b-fa3e-0b6d-30af9bd630b5.gif)

## プロジェクトを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/066ac583-a15b-c051-f7d5-cf973ab4ea37.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/810db820-fac8-4076-b6fb-a473f4fb911b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/d1a28660-c68e-6ff7-bca7-c844bbcc1056.png)

## 使うライブラリをインストールする

file -> swift package から使用するライブラリを追加します
https://github.com/dmytro-anokhin/url-image.git

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/d1a28660-c68e-6ff7-bca7-c844bbcc1056.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/0b8dc3c5-6c66-97c3-942a-2a5b9dd5981e.png)


## 通信周りのクラスを作成する

### JSONを変換するための構造体を作成

```swift
import Foundation

struct Item: Decodable, Identifiable {
    let id: String
    let title: String
    let body: String?
    let url: String
    let user: User
}

struct User: Decodable {
    let profileImageUrl: String
}
```

### APIからデータを取得するクラスを実装

SwiftUIの画面から参照するため`ObservableObject` を継承し、画面に表示する値を `@Published` にする

```swift
import Foundation

class ApiFetcher: ObservableObject {
    @Published var items = [Item]()
    
    private let baseURL = "https://qiita.com/api/v2"
    
    func fetchItems(query: String) {
        let url = URL(string: "\(baseURL)/items?query=\(query)&page=1&per_page=50")!
    
        URLSession.shared.dataTask(with: url) {(data,response,error) in
            do {
                if let data = data {
                    let decoder = JSONDecoder()
                    decoder.keyDecodingStrategy = .convertFromSnakeCase
                    let items = try decoder.decode([Item].self, from: data)
                    DispatchQueue.main.async {
                        self.items = items
                    }
                }
            } catch {
                // ignore
            }
            
        }.resume()
    }
}
```

## 一覧画面を作る

### 一覧のセルを作成する

新しく ItemRow.swift というファイルを作成します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/b85a9e6d-8695-f923-9219-887caa3f6a19.png)

```swift
import SwiftUI
import URLImage

struct ItemRow: View {
    let item: Item
    
    var body: some View {
        HStack {
            URLImage(URL(string: item.user.profileImageUrl)!) { proxy in
                proxy.image
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .clipped()
            }.frame(width: 60.0, height: 60.0)

            Text(item.title)
        }
    }
}

struct ItemRow_Previews: PreviewProvider {
    static var previews: some View {
        ItemRow(item: Item(
            id: "d040cf8b2d15bd7e507d",
            title: "[Angular] Angular アプリの構成をみる",
            body: "Angular に関する自身の勉強の復習がてらの備忘録記事｡",
            url: "https://qiita.com/ksh-fthr/items/d040cf8b2d15bd7e507d",
            user: User(
                profileImageUrl: "https://qiita-user-profile-images.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F193342%2Fprofile-images%2F1500888159?ixlib=rb-1.2.2&auto=compress%2Cformat&lossless=0&w=300&s=9a22e880d804d67db66a33ac2e7671b5"
            )
        ))
    }
}
```

### 一覧画面を作成する

新しく ItemRow.swift というファイルを作成します

```swift
import SwiftUI

struct ItemList: View {
    @ObservedObject var fetcher = ApiFetcher()
    @State var keyword: String = ""
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Search", text: $keyword) {
                    self.fetcher.fetchItems(query: self.keyword)
                }.padding(16)
                
                List(fetcher.items) { item in
                    ItemRow(item: item)
                }
            }
            .navigationBarTitle(Text("投稿一覧"))
        }
    }
}

struct ItemList_Previews: PreviewProvider {
    static var previews: some View {
        ItemList()
    }
}
```

ここまで作成するとプレビューが以下のように表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33499/b314a2b9-d95a-c083-b61d-5e4b01854cd7.png)

### 起動画面を一覧画面にする

もともと作成されている ContentView を修正して一覧画面を表示するようにします

```swift
struct ContentView: View {
    var body: some View {
        ItemList()
    }
}
```

### 起動する

ここまで作ったら一度起動して入力した文字で検索できることを確認してみましょう

## 詳細画面を作る

詳細画面は一覧でタップした投稿をWebViewで表示します
新しくItemDetail.swiftを作成します

```swift
import SwiftUI
import WebKit

struct ItemDetail: View {
    let title: String
    let url: String

    var body: some View {
        WebView(url: URL(string: url)!)
        .navigationBarTitle(Text(title), displayMode: .inline)
    }
}

struct WebView : UIViewRepresentable {
    var url: URL

    func makeUIView(context: Context) -> WKWebView  {
        return WKWebView(frame: .zero)
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        let req = URLRequest(url: url)
        uiView.load(req)
    }
}

struct ItemDetail_Previews: PreviewProvider {
    static var previews: some View {
        ItemDetail(title: "テスト", url: "https://www.apple.com")
    }
}
```

### 一覧から詳細画面に遷移できるようにする

ItemList.swift の List を以下のように修正して画面遷移できるようにします

```swift
List(fetcher.items) { item in
    ItemRow(item: item)
}

List(fetcher.items) { item in
    NavigationLink(
        destination: ItemDetail(
            title: item.title,
            url: item.url
        )
    ) {
        ItemRow(item: item)
    }
}
```

## 実行してみましょう

## 最後に

完成はこちらにあります。
https://github.com/decoch/swiftui_example_qiita_client


おつかれさまでした。