Qiita の記事の閲覧数とLGTMの数を表示するiOSアプリを作成

## この記事の内容について

<img width="300" alt="Screen Shot 2020-05-26 at 4.54.30 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/a0ca2ad9-18a0-a163-d972-41ee25e259ac.png">

Open-Source Source code / オープンソース: https://github.com/mszmagic/Qiita-Contribution-Counter

この記事では、いくつかのヒントや私がアプリで使用した技術について説明しています。どちらかというとケーススタディのようなものです。

- ローカルデバイス上でユーザーのトークンを保存する
- 既存のウェブクッキーを使用してモバイルアプリケーションにユーザーをログインさせる
- `URLSession` を使用して `GET requests` を作る
- Swift でJSONレスポンスを解析する
- リモートサーバーから画像をロードする

### もちろん、ここで私はすでにあるものを再発明しませんでした、いくつかの既存のオープンソースフレームワークを使用しました。記事の次の部分でそれについて紹介します。

<img width="300" alt="Screen Shot 2020-05-26 at 4.54.30 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/0f9f9c35-d08f-e92e-8774-11df365a932a.gif">

## 概要

Githubと同じように `Qiita` コントリビューションを表示したかったのです。

<img width="755" alt="Screen Shot 2020-05-26 at 3.59.42 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/9cb8653f-67da-937b-b002-1b275ba438e7.png">

そこで次のことができるオープンソースiOSアプリを作りました。

1.各記事の合計読み取り回数と読み取り回数を提供する

2.各記事の合計 LGTMs と views を提供する

3.Githubに似たコントリビューションブロックを表示して、当月内に記事を公開した日を表示する

## コード構造

### View Controller

<img width="350" alt="Screen Shot 2020-05-26 at 3.59.42 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/1cba145d-0bc5-ba27-2f7b-1d5f2fbcd04b.png">

https://github.com/mszmagic/Qiita-Contribution-Counter/blob/master/QiitaContributionReport/ViewController.swift

ここで、ユーザーは `Qiita APIページに進む` をクリックして、新しいAPIトークンを作成できます。Safariのクッキーを自動的に使用することにご注意ください。

#### `ASWebAuthenticationSession`

ユーザーの既存のブラウザーセッションのクッキーをここで利用できるよう、`ASWebAuthenticationSession` を用います。そうすればユーザーは再度ログインしなくてもよくなります。


```swift
guard let authURL = URL(string: "https://qiita.com/settings/tokens/new") else { return }
let session = ASWebAuthenticationSession(url: authURL, callbackURLScheme: "")
{ callbackURL, error in
    // Handle the callback.
}
session.presentationContextProvider = self
session.start()
```

また、`presentationContextProvider` をセットアップして、 `ASWebAuthenticationSession` がビューを表示する場所を認識できるようにする必要があります。

```swift
/*
 ASWebAuthenticationSession がどこにビューを表示すべきか判断するためです
 */
extension ViewController: ASWebAuthenticationPresentationContextProviding {
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return view.window!
    }
}
```

### ユーザーのトークンをキーチェーンに保存して、次回ユーザーがトークン値を再度入力する必要がないようにします

通常、アプリは機密情報（トークンなど）をキーチェーンに保存します。キーチェーンの使用に関するAppleの公式ドキュメントは以下のとおりです：https://developer.apple.com/documentation/security/keychain_services

キーチェーンサービスを使用するにはある程度のコードが必要なので、私はGithub上のオープンソースのキーチェーンヘルパーを使用しました。

https://github.com/evgenyneu/keychain-swift

```swift
let keychain = KeychainSwift()
keychain.set(tokenTextField.text ?? "", forKey: "qiitaToken")
tokenTextField.text = keychain.get("qiitaToken") ?? ""
```

## userTableView

<img width="350" alt="Screen Shot 2020-05-26 at 3.59.42 PM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/34024a05-b63a-d74c-f19a-7bc7a966a580.png">

https://github.com/mszmagic/Qiita-Contribution-Counter/blob/master/QiitaContributionReport/userTableView.swift

### リモート画像フェッチ

ここでは、`Kingfisher` というオープンソースのフレームワークを使用しました。

https://github.com/onevcat/Kingfisher

```swift
@IBOutlet weak var profileImageView: UIImageView!
//画像をダウンロードして読み込みます
if let imagePath = profileImagePath,
    let convertedURL = URL(string: imagePath) {
    DispatchQueue.main.async {
        self.profileImageView.kf.setImage(with: convertedURL)
    }
}
```

```swift
self.profileImageView.kf.setImage(with: convertedURL)
```

## requestHelper.swift

https://github.com/mszmagic/Qiita-Contribution-Counter/blob/master/QiitaContributionReport/requestHelper.swift

ここでは、`URLSession` を使ってリクエストしています:

```swift
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)

guard let URL = URL(string: "https://qiita.com/api/v2/authenticated_user/items") else {
    delegate?.onTaskFailed(reason: "URL convertion failed!")
    return
}

var request = URLRequest(url: URL)
request.httpMethod = "GET"

// Headers

request.addValue("Bearer \(userID ?? "")", forHTTPHeaderField: "Authorization")

/* Start a new Task */
let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
    if let httpResponse = response as? HTTPURLResponse {
        let statusCode = httpResponse.statusCode
        if error == nil && statusCode >= 200 && statusCode < 400 {
            //成功
            let allItems = try! JSON(data: data!).array
            for item in allItems ?? [] {
                if let id = (item.dictionary?["id"])?.stringValue {
                    self.fetchIndividualArticle(id: id)
                }
            }
            return
        }
    }
    // 失敗
    self.delegate?.onTaskFailed(reason: error?.localizedDescription ?? "Unknown error. Please check your token and try again.")
})

task.resume()
session.finishTasksAndInvalidate()
```

受信結果を解析:

```swift
if let fetchedData = data {
    if let parsedData = try? JSON(data: fetchedData).dictionary {
        //ユーザー名
        let name = parsedData["name"]?.stringValue
        //説明 description
        let description = parsedData["description"]?.stringValue
        //プロフィール画像のURLパス
        let profileImage = parsedData["profile_image_url"]?.stringValue
        //
        completionHandler(profileImage, name, description, nil)
    }
}
```
ここでは、`SwiftyJSON` というオープンソースのフレームワークを使用しました。

https://github.com/SwiftyJSON/SwiftyJSON

### Githubと同じように `Qiita` コントリビューションを表示したかったのです。

![screenshot1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/635330/df58bd71-0075-abda-b511-f75e165c7617.png)

ここでは、`LSHContributionView` というオープンソースのフレームワークを使用しました。

https://github.com/lucashoeft/LSHContributionView

---

:relaxed: [Twitter @MszPro](https://twitter.com/MszPro)

:sunny: [私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。](http://mszpro.com/ioskiji/)
<img width="150" src="https://cdn.mszmagic.com/static-web-content/AppClipImage_small.png">