7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-02-14

みなさーーーーん!

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

(´・ω・`)

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

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

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

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

Fleetっぽい見た目にする

今回はTwitterの新機能「Fleet」を参考に、その日生えた草の色のリングをアイコンに重ねるデザインとしました。

(イメージ画像)

描画処理

リングの描画処理はSwiftで実装しました。(CoreGraphicsの勉強も兼ねて…)

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

CGContext生成

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

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

/// 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を使用して図形を描画していきます。
グラデーションのついた円の描画についてはこちらを参考にしました。

/// 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するほど綺麗な緑の円になるのでご愛嬌ということで…)

CGContextから画像を生成

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

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

スケジュールしたイベント

schedule イベントを使用すると、スケジュールされた時間にワークフローをトリガーできます。

POSIX クーロン構文を使用して、特定の UTC 時間にワークフローを実行できるようスケジュール設定できます。 スケジュールしたワークフローは、デフォルトまたはベースブランチの直近のコミットで実行されます。

.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

実際のアイコン

実際に反映したものがこちらになります(料理番組のようなノリ)。

当初目指したFleetっぽさが…出て…いないこともないですね!()

最後に

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

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

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

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

2021/04/09追記: コールバック地獄を改善しました。よろしければこちらの記事もご覧ください。

ではまた!

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?