【Swift】進捗をTwitterのアイコンに(Fleetっぽく)表示する

みなさーーーーん!

## 進捗、どうですかーーーー!?

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/7ac1b75d-c064-96fb-ab2e-719abb7679f0.png" />

(´・ω・｀)

## Contributionを自動でツイートしてくれるサービス

GitHubのcontributionを自動でツイートしてくれるサービスとして、[GitHubitter](https://koogawa.sakura.ne.jp/githubitter/)や[Contributter](https://contributter.potato4d.me)などがあります。
私も以前少しの間だけ使ってみたことがあるのですが、**「contribution数: 0」**が連続でツイートされまくるのに心が疲れてしまい(自業自得)数ヶ月で連携を解除してしまいました。
毎日精力的にcontributionし続ける方にはよいツールなのかもしれませんが、ちょっとサボったことまでTLに流れてしまうのはちょっとうーんというところがあります。

でもどうにかして**進捗アピール()したい！**できれば**TLを汚さずに！**

…なら**アイコンをいじるだけなら問題ないんじゃないか?**
ということで(?)作ってみました。

## Fleetっぽい見た目にする

今回はTwitterの新機能「[Fleet](https://blog.twitter.com/ja_jp/topics/product/2020/ntroducing-fleets-new-way-to-join-the-conversation-jp.html?hidden="炎上したらイフリート")」を参考に、**その日生えた草の色のリングをアイコンに重ねる**デザインとしました。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/295bdd47-abdf-bbd2-3abd-752b99c20f10.png" />
(イメージ画像)

## 描画処理

リングの描画処理はSwiftで実装しました。([`CoreGraphics`](https://developer.apple.com/documentation/coregraphics)の勉強も兼ねて…)

※やたら長くなってしまうのを防ぐため、ソースコードは(基本的に)一部を抜粋して掲載しています(コピペでの動作を考慮していません)。実装の詳細はページ最下部にあるGitHubへのリンクを参照してください。

### CGContext生成

まずはTwitterAPIを使用してユーザアイコンの画像URLを取得し、`CIImage(url:)` -> `CIContext.createCGImage(ciImage:, from:)` という流れで`CGImage`を生成します。
(TwitterAPIの使用方法およびCIImageの生成については割愛します)  

次に、以下のコードにより`CGContext`を生成します。
図形の描画やリサイズなど、ピクセル単位の画像処理はこの子がやってくれるようです。

```swift:
/// CGContext生成
///  - Parameters:
///    - cgImage: CGContextの元となる画像
///  - Return: 生成したCGContext
func generateCGContext(cgImage: CGImage) -> CGContext?{
    guard let cgContext = CGContext(
        data: nil,
        width: cgImage.width,
        height: cgImage.height,
        bitsPerComponent: cgImage.bitsPerComponent,
        bytesPerRow: cgImage.bytesPerRow,
        space: cgImage.colorSpace!,
        bitmapInfo: cgImage.bitmapInfo.rawValue
    )else{return nil}
        
    return cgContext
}
```

### 図形描画

次に、生成した`CGContext`を使用して図形を描画していきます。
グラデーションのついた円の描画については[こちら](https://stackoverflow.com/questions/45663087/drawing-a-circle-with-gradient-stroke)を参考にしました。


```swift
/// contributionリングの描画
///  - Parameters:
///    - colors: グラデーションに使用する色
///    - radius: 円の半径
///    - thickness: 線の太さ
///    - completion: 処理完了時に呼ばれる関数
func generateCenterCircleFilteredImage(colors: [CGColor], radius: CGFloat, thickness: CGFloat, completion: (_ image: CGImage?) -> Void){
    
    self.apply { (context) in
        // CGImageを描いて
        let imageRect = CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)
        context.draw(cgImage, in: imageRect)
            
        // 円を描く
        let origin = CGPoint(x: cgImage.width / 2, y: cgImage.height / 2)
        let ellipseRect = CGRect(x: origin.x - radius, y: origin.y - radius, width: radius * 2, height: radius * 2)
        let clipPath = CGPath(ellipseIn: ellipseRect, transform: nil)
        context.saveGState()
        context.setLineWidth(thickness)
        context.addPath(clipPath)
        context.replacePathWithStrokedPath()
        context.clip()
        
        // グラデ
        let offsets = [ CGFloat(0.0), CGFloat(1.0) ]
        let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: offsets)
        
        let start = CGPoint(x: imageRect.minX, y: imageRect.minY)
        let end = CGPoint(x: imageRect.maxX, y: imageRect.maxY)
        
        context.drawLinearGradient(grad!, start: start, end: end, options: [])
        
        context.restoreGState()
        
        completion(self.generateImageFromContext())
    }
}

// contextに任意の処理を加える
func apply(process: (_ context:CGContext) -> Void){
    if let cgContext = self.cgContext{
        process(cgContext)
    }
}

// 現在のContextからCGImageを生成
func generateImageFromContext() -> CGImage?{
    return self.cgContext?.makeImage()
}
```

グラデーションの配色は、

 - 左下から右上にかけて
 - ライトモードの時の草の色からダークモードの時の草の色に変化する

ようにしました。(正直初期状態だと**若干くすんだ色**になりますが、commitするほど綺麗な緑の円になるのでご愛嬌ということで…)
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/60ca2893-475b-1f7e-2ba7-ae0bfa6ba3ba.png" />

### CGContextから画像を生成

処理が終了したら、`CGContext`から編集後の画像を生成して…

```swift
import Foundation
import CoreImage

class ImageFormatter {
    private let ciContext = CIContext()
    
    @available(OSX 10.13, *)
    func generatePNGImageData(image: CIImage, format:CIFormat = .RGBA8, colorSpace: CGColorSpace? = nil, options: [CIImageRepresentationOption: Any]? = nil) -> Data? {
        let ciContext = CIContext()
        return ciContext.pngRepresentation(of: image, format: format, colorSpace: image.colorSpace!, options: options ?? [:])
    }
    
    @available(OSX 10.12, *)
    func generateJPEGImageData(image: CIImage, format:CIFormat = .RGBA8, colorSpace: CGColorSpace? = nil, options: [CIImageRepresentationOption: Any]? = nil) -> Data? {
        let ciContext = CIContext()
        return ciContext.jpegRepresentation(of: image, colorSpace: image.colorSpace!, options: options ?? [:])
    }

}
```

アイコンに割り当てれば、ひとまず処理部分は完成です。(OSX10.12以前ってどうやってCGContextから画像生成してたんでしょう?)

## GitHub Actionsで自動更新

最後に、アイコンの定期更新処理をGitHub Actionsに設定します。
ワークフローを開始するイベントの一つに`schedule`があり、これを設定することで一定時間おきに自動でジョブを実行してくれます。

> [ワークフローをトリガーするイベント - GitHub Docs](https://docs.github.com/ja/actions/reference/events-that-trigger-workflows#scheduled-events)
>
> スケジュールしたイベント
> 
> schedule イベントを使用すると、スケジュールされた時間にワークフローをトリガーできます。
> 
> POSIX クーロン構文を使用して、特定の UTC 時間にワークフローを実行できるようスケジュール設定できます。 スケジュールしたワークフローは、デフォルトまたはベースブランチの直近のコミットで実行されます。 

```yaml:.github/workflows/scheduledIconUpdator.yml
# 定期更新ワークフロー
name: Periodic update

# 5時間ごと、またはmainブランチにpushしたときに起動
on:
  schedule:
    - cron: '0 */5 * * *'

  push:
    branches: [ main ]

jobs:
  build:
    runs-on: macos-latest

    steps:
    - uses: actions/checkout@v2
    # ビルド
    - name: Build
      run: swift build -c release
      
    # 実行
    - name: Run
      env:
        TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
        TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
        TWITTER_OAUTH_TOKEN: ${{ secrets.TWITTER_OAUTH_TOKEN }}
        TWITTER_OAUTH_SECRET: ${{ secrets.TWITTER_OAUTH_SECRET }}

      run: swift run GitGrassIndicator ${TWITTER_CONSUMER_KEY} ${TWITTER_CONSUMER_SECRET} ${TWITTER_OAUTH_TOKEN} ${TWITTER_OAUTH_SECRET}
```

`README.md`にバッジをつけておくと、Actionsを調べに行かなくても状態が分かるので便利かもしれません。

```
![badge](![Periodic update](https://github.com/Enchan1207/GitGrassIndicator/workflows/Periodic%20update/badge.svg))
```

![Periodic update](https://github.com/Enchan1207/GitGrassIndicator/workflows/Periodic%20update/badge.svg)

## 実際のアイコン

実際に反映したものが[こちら](https://twitter.com/EnchanLab)になります(料理番組のようなノリ)。
<img width="500" src="https://user-images.githubusercontent.com/51850597/107844306-9e0db880-6e15-11eb-8fb3-d0885f5b4731.png" />
当初目指したFleetっぽさが…出て…**いないこともないですね！**()

## 最後に

ここまで読んでいただきありがとうございました。

Swift × CLIツールというのはなかなか意外な組み合わせだなーと思っていましたが、
正直**もう全部Swiftでいいんじゃないか**とすら思えるほど快適でした。Swiftは いいぞ。

XCodeの強力なコード補完を利用でき、かつ普段iOSアプリ開発で使っているライブラリがそのまま使えるというのは大きなメリットなのではないでしょうか。(当然ながら**ライブラリが`UIKit`をimportしている**などの場合は詰んでしまうのですが…)

ソースコードは[GitHub](https://github.com/Enchan1207/GitGrassIndicator)にて公開しておりますので、
「ソースの一部じゃなくて全体が見たい!」とか「**`Sources/main.swift` コールバック地獄のクソコードやんけ!**」等ございましたらコメントまたはPRいただければ幸いです。

**2021/04/09追記**: コールバック地獄を改善しました。よろしければ[こちらの記事](https://qiita.com/Enchan/items/1ee331f8a9677e7f0913)もご覧ください。

ではまた!