NHK for School API を使った Flutter の習作

Flutter を作って何かアプリを作ってみようと思い、
NHK for School API という API を使って、アプリを作ってみた。

github.com

NHK for School API を使うと NHK for School 公開されているコンテンツを API で取得できる。
(すべてが取得できるわけでなく、現在だと小・中学校の理科と社会科のコンテンツだけのようだ)

www.nhk.or.jp

このアプリ作成を通じて Flutter アプリでの以下の実現方法を学べた。

  • HTTP によるデータ取得
  • アプリ内データ保存(shared_preferences)
  • Widget の使い方(レイアウトの組み方など)

情報提供 NHK

東京ガスの「ずっとも電気1」と「基本プラン」の電力量料金の計算

東京ガス

の「電力量料金」(電力を使用した分の電気代)を計算できる Web ツールを作った。

電力量料金の計算

  • 基本料金、燃料費調整額、再エネ促進賦課金、ガス・電気セット割については計算に含まれていません。
  • ※計算結果が正しいことは保証しませんので、ご注意ください。

「段階の区切りになる使用量」、「段階ごとの 1kWh単位の料金」を変更すると、東京ガス以外にも使用できると思います。

Swift の未使用コードを分析する periphery コマンドのオプションメモ

periphery を使用すると Swift プロジェクトの中で未使用なコードを特定できる。
(未使用なクラス・プロパティ、不要な public がわかる)

インストールして、コマンドのオプションを調べたのでメモする。

インストール

brew 経由だと

brew install peripher

ヘルプを実行

periphery scan --help 

実行結果。オプションが羅列される。

% periphery help scan   
OVERVIEW: Scan for unused code

USAGE: periphery scan [<options>] [<build-arguments> ...]

ARGUMENTS:
  <build-arguments>       Arguments following '--' will be passed to the underlying build tool, which is either 'swift build' or
                          'xcodebuild' depending on your project

OPTIONS:
  --setup                 Enable guided setup
  --config <config>       Path to configuration file. By default Periphery will look for .periphery.yml in the current directory
  --workspace <workspace> Path to your project's .xcworkspace. Xcode projects only
  --project <project>     Path to your project's .xcodeproj - supply this option if your project doesn't have an .xcworkspace. Xcode
                          projects only
  --schemes <schemes>     Comma-separated list of schemes that must be built in order to produce the targets passed to the --targets
                          option. Xcode projects only (default: [])
  --targets <targets>     Comma-separated list of target names to scan. Required for Xcode projects. Optional for Swift Package Manager
                          projects, default behavior is to scan all targets defined in Package.swift (default: [])
  --format <format>       Output format (allowed: xcode, csv, json, checkstyle) (default: xcode)
  --index-exclude <index-exclude>
                          Path glob of source files which should be excluded from indexing. Declarations and references within these
                          files will not be considered during analysis. Multiple globs may be delimited by a pipe (default: [])
  --report-exclude <report-exclude>
                          Path glob of source files which should be excluded from the results. Note that this option is purely
                          cosmetic, these files will still be indexed. Multiple globs may be delimited by a pipe (default: [])
  --index-store-path <index-store-path>
                          Path to index store to use. Implies '--skip-build'
  --retain-public         Retain all public declarations - you'll likely want to enable this if you're scanning a framework/library
                          project
  --disable-redundant-public-analysis
                          Disable identification of redundant public accessibility
  --retain-assign-only-properties
                          Retain properties that are assigned, but never used
  --retain-assign-only-property-types <retain-assign-only-property-types>
                          Comma-separated list of property types to retain if the property is assigned, but never read (default: [])
  --external-encodable-protocols <external-encodable-protocols>
                          Comma-separated list of external protocols that inherit Encodable. Properties of types conforming to these
                          protocols will be retained (default: [])
  --retain-objc-accessible
                          Retain declarations that are exposed to Objective-C implicitly by inheriting NSObject classes, or explicitly
                          with the @objc and @objcMembers attributes
  --retain-unused-protocol-func-params
                          Retain unused protocol function parameters, even if the parameter is unused in all conforming functions
  --clean-build           Clean existing build artifacts before building
  --skip-build            Skip the project build step
  --strict                Exit with non-zero status if any unused code is found
  --disable-update-check  Disable checking for updates
  --verbose               Enable verbose logging
  --quiet                 Only output results
  -h, --help              Show help information.

オプションのメモ

--setup

対話形式でセットアップして分析する。
セットアップした結果は .periphery.yml に構成が保存されるので次回以降はセットアップせずに yml から構成を指定して、分析できる。

--config

.periphery.yml のパスを指定する。
デフォルトはカレントディレクトリ。

--workspace

Xcode ワークスペースへのパス。

--project

Xcode プロジェクトへのパス。
ワークスペースがない場合は、こちらを指定する。

--schemes

対象スキーム。
カンマ区切りで複数指定できる。
単体指定も可能。

--targets

対象ターゲット。
カンマ区切りで複数指定できる。
単体指定も可能。

--format

出力フォーマット。以下が指定可能。

デフォルトは xcode
Xcode で Build Phase に追加して、Xcode のエディタ上で結果を表示したい時は xcode 指定にする。
表として、出力したい時は csv が便利。

--index-exclude

インデックス作成から除外するパス。

--report-exclude

出力される結果から除外するパス。
インデックス作成自体はされることに注意。

--index-store-path

インデックスストアのパスを指定する。
この場合、プロジェクトのビルドをスキップする。
--skip-build と同様に)

--retain-public

public された宣言はすべて外部から使用される想定にする。
フレームワークやライブラリのプロジェクトで、実際にそのインターフェースを使用していないプロジェクトの場合にこれを指定する。

--disable-redundant-public-analysis

冗長なパブリックアクセシビリティの識別を無効にする。

--retain-assign-only-propertie

割り当てられているが使用されていないプロパティを保持する。

--retain-assign-only-property-types

プロパティが割り当てられているが、読み取られない場合に保持するプロパティタイプのコンマ区切りリスト。

--external-encodable-protocols

Encodableを継承する外部プロトコルのコンマ区切りリスト。 これらに準拠するタイプのプロパティプロトコルは保持されます。

--retain-objc-accessible

NSObjectクラスを継承することによって暗黙的に、または@objc属性と@objcMembers属性を使用して明示的に、Objective-Cに公開される宣言を保持します。

--retain-unused-protocol-func-params

パラメータがすべての準拠関数で使用されていない場合でも、未使用のプロトコル関数パラメータを保持します。

--clean-build

既存のビルドをクリアしてからビルドする。

誤った結果が出ている時は、インデックスストアが破損したり・ソースファイルと同期しなくなった可能性がある。
たとえば、スキャンを強制的に終了(^C)した場合にこれが発生することがある。
この時は --clean-build すると良い。

--skip-build

ビルドをスキップする。

--strict

未使用コードが見つかった場合は非ゼロのステータスで終了する。

--disable-update-check

更新のチェックを無効にする。

--verbose

冗長なログを出力。

これを指定すると、

[configuration:begin]

から始まる箇所に、分析の構成が表示されるので、それを .periphery.yml に貼り付けると、構成を永続化できる。

--quiet

結果のみを出力。

-h, --help

ヘルプを表示。

CI で使用する場合

https://github.com/peripheryapp/periphery#continuous-integration

たとえばテストを実行した後に periphery を実行する時は

--skip-build でビルドをスキップできる。

インデックスパスが非標準の場所にあるときは

--index-store-path で指定する。

たとえば以下のスキームがあって

  • A
  • B
  • C

それぞれ以下のターゲットがあるとき

  • A
  • B
  • C

これらをそれぞれ分析して、結果を CSV に出力する時は以下のシェルになる。

array=(A B C)
for i in "${array[@]}"
do
    periphery scan --project test.xcodeproj --schemes ${i} --targets ${i} --format csv > ~/Desktop/test/periphery/${i}.csv
done

iOS 15 から使える GCVirtualController のメモ

GCVirtualControlleriOS 15 から使用できるソフトウェアゲームコントローラー。
これを使うことで、アプリで自作のゲームコントローラーを作成しなくてよい、というメリットがある。

GCVirtualController - Apple Developer

アプリを作る際に GCVirtualController を触ってみて、わかったことを以下にまとめる。

バーチャルコントローラーを作成する

コントローラー作成の簡単なコードが以下になる。

import UIKit
import GameController

class ViewController: UIViewController {

    private var virtualController: GCVirtualController!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let config = GCVirtualController.Configuration()
        config.elements = [
            GCInputDirectionPad,
            GCInputButtonA,
            GCInputButtonB,
        ]
        self.virtualController = GCVirtualController(configuration: config)
        
        self.virtualController.connect { error in
            if let error = error {
                print(error)
            }
        }
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        self.virtualController.disconnect()
    }

}

これを実行すると、このようにボタンが表示される。

f:id:daisuke-t-jp:20220302205010p:plain

使えるボタンの種類はここに書いてあり、 https://developer.apple.com/documentation/gamecontroller/gcvirtualcontroller/configuration/3752038-elements

以下が使えるボタンのようだ。

  • GCInputButtonA
  • GCInputButtonB
  • GCInputButtonX
  • GCInputButtonY
  • GCInputDirectionPad
  • GCInputLeftThumbstick
  • GCInputRightThumbstick
  • GCInputLeftShoulder
  • GCInputRightShoulder
  • GCInputLeftTrigger
  • GCInputRightTrigger

ボタンの入力を知りたい

以下のようにボタンに valueChangedHandler を設定することで、「ボタンが押された」を取得することができる。

        if let dpad: GCControllerDirectionPad = self.virtualController.controller?.extendedGamepad?.dpad {
            dpad.valueChangedHandler = { (dpad: GCControllerDirectionPad, xValue: Float, yValue: Float) in
                if dpad.up.isPressed {
                    print("↑")
                }
                if dpad.down.isPressed {
                    print("↓")
                }
                if dpad.left.isPressed {
                    print("←")
                }
                if dpad.right.isPressed {
                    print("→")
                }
            }
        }
        
        if let buttonA: GCControllerButtonInput = self.virtualController.controller?.extendedGamepad?.buttonA {
            buttonA.valueChangedHandler = { (button: GCControllerButtonInput, value: Float, pressed: Bool) in
                if buttonA.isPressed {
                    print("A")
                }
            }
        }
        
        if let buttonB: GCControllerButtonInput = self.virtualController.controller?.extendedGamepad?.buttonB {
            buttonB.valueChangedHandler = { (button: GCControllerButtonInput, value: Float, pressed: Bool) in
                if buttonB.isPressed {
                    print("B")
                }
            }
        }

valueChangedHandler は「ボタンが押された」だけでなく「ボタンが離された」タイミングでも呼ばれるため、どのボタンが離されたかも判断できる。

また valueChangedHandler を使わずに Timer などで周定期で各ボタンの isPressed を確認してボタン状態を得ることもできる。
(たとえば、「Nフレームボタンが押されたら」という処理がしたい場合は、周定期でボタン状態を監視することになる)

ボタンの見た目・位置は変えれるか?

ボタンの要素は ElemntConfiguration で変更できる。

これを見ると

  • ボタンの位置は変えられない。
  • ボタンの位置、大きさはそのままに、中の表示を UIBezierPath で変更することはできる。

ボタンの見た目の変更は、たとえば以下のコードで

        self.virtualController.updateConfiguration(forElement: GCInputButtonA, configuration: { _ in
            let elementConfiguration: GCVirtualController.ElementConfiguration = GCVirtualController.ElementConfiguration()
            
            elementConfiguration.path = UIBezierPath()
            
            let bezierPath: UIBezierPath = UIBezierPath()
            bezierPath.move(to: CGPoint(x: 0, y: 0))
            bezierPath.addLine(to: CGPoint(x: -10, y: -10))
            bezierPath.addLine(to: CGPoint(x: 10, y: -10))
            bezierPath.addLine(to: CGPoint(x: 10, y: 10))
            bezierPath.addLine(to: CGPoint(x: -10, y: 10))
            bezierPath.close()
            
            elementConfiguration.path?.append(bezierPath)

            return elementConfiguration
        })

このように A ボタンの見た目が、パックマンに変わる。

f:id:daisuke-t-jp:20220302205331p:plain

各ボタンは途中で非表示にできるか?

ElementConfiguration の isHidden で非表示にすることができる。

たとえば、 A ボタンを押したら B ボタンを非表示にするコードは以下だ。

        if let buttonA: GCControllerButtonInput = self.virtualController.controller?.extendedGamepad?.buttonA {
            buttonA.valueChangedHandler = { (button: GCControllerButtonInput, value: Float, pressed: Bool) in
                if buttonA.isPressed {
                    self.virtualController.updateConfiguration(forElement: GCInputButtonB, configuration: { _ in
                        let elementConfiguration: GCVirtualController.ElementConfiguration = GCVirtualController.ElementConfiguration()
                        elementConfiguration.isHidden = true
                        return elementConfiguration
                    })
                }
            }
        }

B ボタンが非表示になると見た目がこうなる。

f:id:daisuke-t-jp:20220302205335p:plain

ちなみに最初から B ボタンがない(GCVirtualController の init(configuration:) の時点で B ボタンの要素を含めない)場合は、見た目がこうなる。

f:id:daisuke-t-jp:20220302205342p:plain

上の2つのボタン配置位置が違うことから

  • 最初からボタンがない場合
  • 途中でボタンを非表示にした場合

はそれぞれ配置が異なることがわかるので、注意したい。
(途中でボタンを非表示にしても、その分、配置が詰められる訳ではない)

もし、ボタンを非表示にして、かつボタン配置も最初からボタンがないものと同じにしたい場合は、GCVirtualController init(configuration:) でバーチャルコントローラー自体を作り直せば良い。

おわり

↑のようにいろいろ GCVirtualController を調べて作ったゲームがあったりしますよ。

HARVEST - CATCH THE FRUIT -

HARVEST - CATCH THE FRUIT -

  • Daisuke Tonosaki
  • Games
  • Free

SwiftUI で SKStoreProductViewController を使って AppStore を表示する

SKStoreProductViewController を使うと、アプリから離脱せずに、AppStore で特定のアプリを表示することができる。これを使うと、アプリ内で他のアプリのインストールを促すことに使えたりする。

ただし、SKStoreProductViewController に delegate を設定するとき、SwiftUI ではそのままでは使えないので、UIViewControllerRepresentable を使ったパータンを考えてみる。(delegate を設定しない場合は、UIViewControllerRepresentable は不要で、そのまま使える)

まず、SKStoreProductViewController で AppStore を開く機能を持った ViewController を用意する。

import UIKit
import SwiftUI
import StoreKit

class StoreProductViewController: UIViewController {
    
    private var isPresentStoreProduct: Binding<Bool>
    private let appId: Int
    
    init(isPresentStoreProduct: Binding<Bool>, appId: Int) {
        self.isPresentStoreProduct = isPresentStoreProduct
        self.appId = appId
        
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
    }
    
    func presentStoreProduct() {
        let storeProductViewController = SKStoreProductViewController()
        storeProductViewController.delegate = self
        
        let parameters = [SKStoreProductParameterITunesItemIdentifier: self.appId]
        storeProductViewController.loadProduct(withParameters: parameters) { status, error -> Void in
            if status {
                self.present(storeProductViewController, animated: true, completion: nil)
            } else {
                if let error = error {
                    print("Error: \(error.localizedDescription)")
                }
            }
        }
        
        DispatchQueue.main.async {
            self.isPresentStoreProduct.wrappedValue = false
        }
    }
}


// MARK: - SKStoreProductViewControllerDelegate
extension StoreProductViewController: SKStoreProductViewControllerDelegate {
    func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) {
        viewController.presentingViewController?.dismiss(animated: true, completion: nil)
    }
}

上記の ViewController の UIViewControllerRepresentable を用意する。

import SwiftUI

struct StoreProductViewControllerRepresentable: UIViewControllerRepresentable {
    typealias UIViewControllerType = StoreProductViewController
    
    var isPresentStoreProduct: Binding<Bool>
    let appId: Int
 
    func makeUIViewController(context: Context) -> UIViewControllerType {
        let viewController = StoreProductViewController(isPresentStoreProduct: isPresentStoreProduct,
                                                        appId: appId)
        return viewController
        
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        if self.isPresentStoreProduct.wrappedValue {
            uiViewController.presentStoreProduct()
        }
    }
}

作成した StoreProductViewControllerRepresentable を使った SwiftUI の View が以下になる。

import SwiftUI

struct ContentView: View {
    @State private var isPresentStoreProduct: Bool = false
    
    var body: some View {
        VStack {
            Button {
                self.isPresentStoreProduct = true
            } label: {
                Text("App Store Link")
            }
            
            
            StoreProductViewControllerRepresentable(isPresentStoreProduct: self.$isPresentStoreProduct,
                                                    appId: 284602850)
                .frame(width: 0, height: 0)
        }
    }
}

処理の流れはこうなる。

SwiftUI, UIViewControllerRepresentable, UIViewController 間で、Bool のフラグ isPresentStoreProduct を Binding しておいて

  1. SwiftUI 側で isPresentStoreProduct を true にする。
  2. UIViewControllerRepresentable で isPresentStoreProduct が true になったら ViewController の AppStore を開くメソッドを実行する。
  3. UIViewController で、AppStore を開いた後、 isPresentStoreProduct を false に戻す。
    • このとき、即時に false にすると UIViewControllerRepresentable 側で連続してイベントが発生してしまうため、 非同期に DispatchQueue.main.async で false にする。

動作させるとこんな感じになる。
ちなみに id 284602850 のアプリは、 Texas Hold’em である。

コードは GitHub にもあります。
https://github.com/daisuke-t-jp/SwiftUIPresentAppStoreProduct

Swift Dictionary の key, value を weak 参照したい

たとえば

UIView を key にして、Datevalue にした Dictionary を使いたいと考えたとき。

ここで問題があるのは Dictionary の key, valuestrong で参照するため、 UIView を key にするとメモリリークが発生する。(本来の UView のライフサイクルが終了しても、解放されない)

この対応策が以下。

2つ方法を書いたが、他にもあると思う。

1. NSMapTable を使用する

NSMapTable は Dictionary と似ているが、key と value の参照を weak, strong からそれぞれ選ぶことができる。

https://developer.apple.com/documentation/foundation/nsmaptable

key を weak, value を strong にすると以下のコードになる。

        let table: NSMapTable = NSMapTable<UIView, NSDate>(keyOptions: .weakMemory, valueOptions: .strongMemory)

        autoreleasepool {
            let view1 = UIView()
            table.setObject(Date() as NSDate, forKey: view1)
        }
        
        let view2 = UIView()
        table.setObject(Date() as NSDate, forKey: view2)

        let view3 = UIView()
        table.setObject(Date() as NSDate, forKey: view3)
        
        // 追加した要素は3個だが、うち1つは autoreleasepool ブロック内でライフサイクルが終わっている(=weak 参照の NSMapTable の要素も消える)ので、count は 2 となる。
        print(table.count)

2. NSCache を使用する

要素の数が限られていて良い。という場合は、NSCache を使う方法も取れる。

その場合、UIView をそのままキーにするのではなく ObjectIdentifier を key にする。

https://developer.apple.com/documentation/swift/objectidentifier

ObjectIdentifier を key にすることで、UIView の参照カウントを増やさずに、UIView を一意なキーとして Dictionary で扱える。

コードはこんな感じ。

        let cache = NSCache<AnyObject, NSDate>()
        cache.countLimit = 2
        
        let view1 = UIView()
        cache.setObject(Date() as NSDate, forKey: ObjectIdentifier(view1) as AnyObject)
        
        let view2 = UIView()
        cache.setObject(Date() as NSDate, forKey: ObjectIdentifier(view2) as AnyObject)
        
        let view3 = UIView()
        cache.setObject(Date() as NSDate, forKey: ObjectIdentifier(view3) as AnyObject)
        
        // キャッシュ最大個数(2)を超えているので、一番最初に追加した要素はなくなっている
        print("view1 -> \(cache.object(forKey: ObjectIdentifier(view1) as AnyObject))")    // view1 -> nil
        print("view2 -> \(cache.object(forKey: ObjectIdentifier(view2) as AnyObject))")   // view2 -> Optional(2022-01-09 06:12:40 +0000)
        print("view3 -> \(cache.object(forKey: ObjectIdentifier(view3) as AnyObject))")   // view3 -> Optional(2022-01-09 06:12:40 +0000)

Swift プロジェクトの テスト CI を Travis CI から GitHub Actions へ移行

Travis CI を使って GitHub のプロジェクトに push した時に、自動でテストが実行されるようになったいたのを GitHub Actions に変えてみた。

GitHub actions の yaml の書き方や、Travis CI からの移行については以下のドキュメントが参考になる。

実施した手順のメモ。

1. プロジェクトのルートに .github フォルダを作成
2. .github フォルダ内に workflows フォルダを作成
3. workflows に yml ファイルを作成し、そこにテストの内容を記述していく。
たとえば、こんな感じ。

name: ci

on: 
  push:
    branches: 
      - master
  pull_request:
    branches: 
      - '*'

jobs:
  testing_macos:
    runs-on: macos-11
    steps:
    - uses: actions/checkout@v1
    - name: "Xcode13 iOS"
      run:  xcodebuild clean test -project Test.xcodeproj -scheme Test-iOS -sdk iphonesimulator -destination "platform=iOS Simulator,OS=15.0,name=iPhone 8" -configuration Debug

4. ブランチを新たに作り、今までの作業内容をコミットして、プルリクエストする。

  on: 
  push:
    branches: 
      - master
  pull_request:
    branches: 
      - '*'

この部分の記述で pull request した時も GitHub アクションが実施されるようにしているので、このプルリクエストのタイミングで実際にテストが開始されるのを確認できる。

5.プルリクエストのテストが成功したら、main ブランチにマージする。


GitHub のページから UI でワークフロー・GitHub アクションも作れるのだけど、上のような感じで UI を介さずに、作ることもできる。

実際 Travis CI と GitHub Actions だとどう yaml が変わったというと、、、

Travis CI の時(macOS 上で iOS, macOS, tvOS のテスト+Linux 上のテスト)

branches:
  only: 
    - master

matrix:
  include:
    - os: osx
      language: swift
      osx_image: xcode12
      script:
        # iOS
        - xcodebuild clean -project "MurmurHash.xcodeproj" 
        - xcodebuild test -project "MurmurHash.xcodeproj" -scheme "MurmurHash-iOS" -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 11 Pro Max" -configuration Debug
        
        # macOS
        - xcodebuild clean -project "MurmurHash.xcodeproj" 
        - xcodebuild test -project "MurmurHash.xcodeproj" -scheme "MurmurHash-macOS" -destination "platform=OS X" -configuration Debug
        
        # tvOS
        - xcodebuild clean -project "MurmurHash.xcodeproj" 
        - xcodebuild test -project "MurmurHash.xcodeproj" -scheme "MurmurHash-tvOS" -sdk appletvsimulator -destination "OS=12.2,name=Apple TV 4K" -configuration Debug
        
    - os: linux
      language: generic
      dist: xenial
      sudo: false
      addons:
        apt:
          packages:
            - wget
            # Ubuntu needs following packages to build Swift.
            # 
            # Reference:
            # https://github.com/apple/swift#System-Requirements
            - git
            - cmake
            - ninja-build
            - clang
            - python
            - uuid-dev
            - libicu-dev
            - icu-devtools
            - libbsd-dev 
            - libedit-dev
            - libxml2-dev
            - libsqlite3-dev
            - swig
            - libpython-dev
            - libncurses5-dev
            - pkg-config
            - libblocksruntime-dev
            - libcurl4-openssl-dev
            - systemtap-sdt-dev
            - tzdata
            - rsync
      script:
        - wget https://swift.org/builds/swift-5.3-release/ubuntu1604/swift-5.3-RELEASE/swift-5.3-RELEASE-ubuntu16.04.tar.gz
        - tar xvfz swift-5.3-RELEASE-ubuntu16.04.tar.gz
        - export PATH=$(pwd)/swift-5.3-RELEASE-ubuntu16.04/usr/bin:"${PATH}"
        - swift build
        - swift test

GitHub Actions の時(テストの内容は、Travis CI と同じ。Xcode や OS のバージョンは上げている)

name: ci

on: 
  push:
    branches: 
      - master
  pull_request:
    branches: 
      - '*'

jobs:
  testing_macos:
    runs-on: macos-11
    steps:
    - uses: actions/checkout@v1
    - name: "Xcode13 iOS"
      run:  xcodebuild clean test -project MurmurHash.xcodeproj -scheme MurmurHash-iOS -sdk iphonesimulator -destination "platform=iOS Simulator,OS=15.0,name=iPhone 8" -configuration Debug
      env:
        DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
    - name: "Xcode13 macOS"
      run: xcodebuild clean test -project MurmurHash.xcodeproj -scheme MurmurHash-macOS -destination "platform=OS X" -configuration Debug
      env:
        DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer
    - name: "Xcode13 tvOS"
      run: xcodebuild clean test -project MurmurHash.xcodeproj -scheme MurmurHash-tvOS -sdk appletvsimulator -destination "platform=tvOS Simulator,OS=15.0,name=Apple TV" -configuration Debug
      env:
        DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer

  testing_linux:
    runs-on: ubuntu-20.04
    steps:
    - uses: actions/checkout@v1
    - name: "Linux"
      run: swift test

となった。

まあ、そこまで変わらない感じでかけた。(使っていないけど、GitHub actions でも matrix を書けるらしい)
あと、Travis CI の時は、Linux で Swift 実行環境を作るためにパッケージインストールをしていたのが、GitHub だと最初から入っているので、楽。

GitHub で使える OS 環境は以下を見た。

ちなみに GitHub アクションでも、ビルドステータスのバッチがつけれます。