UIButton.Configuration.Sizeの各サイズごとのデフォルト状態での高さ

iOS 16.1シミュレーターで確認してみた。

UIButton.Configuration.Size

@2x @3x
UIButton.Configuration.Size.large 50.5 50.33…
UIButton.Configuration.Size.medium 34.5 34.33…
UIButton.Configuration.Size.small 28 28
UIButton.Configuration.Size.mini 28 28

特別にHuman Interface Guidelinesの44x44ポイントのヒットターゲットが意識されている・遵守するようになっているわけではないのか。

Buttons - Menus and actions - Components - Human Interface Guidelines - Design - Apple Developer

Make buttons easy for people to choose. On a touchscreen, buttons need a hit target of at least 44x44 points to accommodate a fingertip.

UINavigationController配下でUIHostingControllerによって表示しているSwiftUIのViewからNavigationLinkは使えるのか

結論としては使えます。端的なPlaygroundのコードとしては次のようになります。

import UIKit
import SwiftUI
import PlaygroundSupport

struct RootView: View {
    var body: some View {
        NavigationLink("This is NavigationLink") {
            Text("Hello World!")
        }
    }
}

let hosting = UIHostingController(rootView: RootView())
let navigation = UINavigationController(rootViewController: hosting)
navigation.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)

PlaygroundPage.current.liveView = navigation

NavigationLinkはNavigationStackがなくても(UINavigationControllerがあれば)動く!

iOSシミュレーター・Androidエミュレーターにアプリをインストールだけする(起動までしない)方法

なぜ

アプリの初回起動時専用の特別な処理や画面遷移を行うケースで、そのデバッグ実行・動作確認をするため。

iOS

まずインストールするためのアプリは Build For Running でビルドしておく。次にシミュレーターを起動しておいて

xcrun simctl install booted <~~.appのパス>

のコマンドを実行する。

もしくは、シミュレーターのウィンドウに ~~.app をFinderからドラッグ&ドロップする。

Xcodeのスキーム設定の Wait for the executable to be launched が使えるのでは、と一瞬考える。しかしこれはシミュレーターにインストール済みのアプリには使えるのだが、まだインストールされていないアプリの場合は、アプリのインストールまではやってくれないので、ただ待つだけ(何も起きない)状態になる。ので、別途自分でインストールしてあげる必要がある。

UserDefaultsやKeychainも含めてまっさらな初回起動状態の再現のために、シミュレーターのメニューの Device -> Erase All Content and Settings… も便利。

Android

エミュレーターを起動しておいて

gradlew installDevelopDebug

のコマンドを実行する。DevelopDebug の部分は使いたいbuild variantを指定する。こちらはエミュレーターではなく実機接続でも動作する。

iOSシミュレーター同様に、エミュレーターのウィンドウにapkファイルをドラッグ&ドロップでもインストールできる。

RenovateのGemfileやPodfileの更新でリリースノートを取得する

Renovateは様々なデータソース・パッケージマネージャーに対応していて、もちろんRubyBundlerのGemfileにも対応しているのだが、APIリクエストのレートリミットを回避する関係で、https://rubygems.org をデータソースとする場合はリリースノートの取得ができなくなっている。

これが不便で、自分達のチームではGemfileの更新にはDependabotを併用するという面倒な運用をしていた。最近になってこれを部分的に解消できる設定がRenovateに誕生したのでご紹介。

customChangelogUrlという設定を使うと、あるパッケージのリリースノートを探しにいくURLを指定することができる。これを使うとGitHubやGitLabの該当リポジトリのReleasesや CHANGELOG.md などからリリースノートをリストアップしてくれる。

例えばfastlaneを指定する場合はこうなる:

{
  "packageRules": [
    {
      "matchManagers": ["bundler"],
      "matchPackageNames": ["fastlane"],
      "customChangelogUrl": "https://github.com/fastlane/fastlane",
    },
  ]
}

iOS開発文脈ではCocoaPodsのPodfileでも同様の状態にあり、そちらにも活用することができる。

{
  "packageRules": [
    {
      "matchManagers": ["cocoapods"],
      "matchPackageNames": ["SwiftLint"],
      "customChangelogUrl": "https://github.com/realm/SwiftLint",
    },
  ]
}

これでDependabotに別れを告げてRenovateに統一することができた。めでたしめでたし。

GraphQLスキーマをサーバーサイドからクライアントサイドに自動同期し、変更履歴も確認したい

似たような話が先日ありましたね。

こちらはGraphQLではなくてOpenAPI、かつサーバーサイド側のリポジトリからクライアントサイド側のリポジトリにプッシュする方式でした。

今関わっているプロダクトでは、1つのGraphQLスキーマが複数のリポジトリから必要とされていたり、それぞれで同期タイミングを制御できたらいいかなということもあったりで、クライアントサイドからGitHub Actionsの定期実行でプルする方式で更新をしています。

イメージは次のとおり:

name: Update GraphQL Schema

on:
  schedule:
    - cron: "0 0 * * 1-5" # JSTで月曜〜金曜の9時、これは適当です
  workflow_dispatch:

jobs:
  UpdateGraphQLSchema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: make update-graphql-schema
      - uses: peter-evans/create-pull-request@v4
        with:
          commit-message: Update schema.graphqls
          title: Update schema.graphqls
          body: ""
          branch: update-graphql-schema
          token: ${{ secrets.GITHUB_PAT }}

トリガーとしては、scheduleでの定期実行 OR workflow_dispatchでの手動実行になっています。

実際の更新部分は例として make update-graphql-schema としていますが、中身としてはApollo Clientのコマンドを実行してスキーマをダウンロードする(実態としてはIntrospectionを実行してスキーマを得る)ようにしています。ここについては、サーバーサイドのリポジトリをチェックアウトしてきて、元のスキーマファイルをコピーするようにしてきてもよいでしょう。

更新したスキーマをゲットできたら、あとは peter-evans/create-pull-request actionを使ってプルリクエスト(以下、PR)を作成しています。

ここで扱っているのはApollo Clientを使ったネイティブアプリで、Apollo iOSApollo Kotlinもアプリのビルド時にオンデマンドでコード生成を行うので、スキーマ更新と同時にコード生成をしてPRに含める、ということはありません。

さて、これはこれで単純でいいのですが、スキーマの変更内容がPRで確認できたとしても、その詳細(変更の意図や経緯)がこれだけでは分かりません。ということで、サーバーサイドのリポジトリの関連するPRを拾い上げて、スキーマ更新のPRに変更履歴という体で貼り付けていました。

しかしさすがに人力で関連PRを探しに行くのは面倒なので自動化したい。ということで、チームメンバーであるところの id:mangano-ito さんがやってくれました。

name: Update GraphQL Schema

on:
  schedule:
    - cron: "0 0 * * 1-5" # JSTで月曜〜金曜の9時、これは適当です
  workflow_dispatch:

jobs:
  UpdateGraphQLSchema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - id: generate-associated-pr-list
        uses: ./.github/actions/generate-associated-pr-list
        with:
          our-path: path/to/client/schema.graphqls
          their-path: path/to/server/schema.graphql
          their-owner: ...
          their-repo: ...
          github-token: ${{ secrets.GITHUB_PAT }}
      - run: make update-graphql-schema
      - uses: peter-evans/create-pull-request@v4
        with:
          commit-message: Update schema.graphqls
          title: Update schema.graphqls
          body:  |
            ## Associated Pull Requests
            ${{ steps.generate-associated-pr-list.outputs.result }}
          branch: update-graphql-schema
          token: ${{ secrets.GITHUB_PAT }}

generate-associated-pr-list というのがお手製のComposite Actionです。自分方のリポジトリのファイルと、相手方のリポジトリのファイルの変更日時から、相手方のファイルのその間のコミット・それに紐付くPRを拾い上げて、Markdownのリストとして出力してくれるものです。パラメーター名も、gitの衝突解消時のours, theirsという用語を基にしていてオシャレですね。

その実装の大事な部分だけ抜粋してみます:

runs:
  using: composite
  steps:
    - id: get-last-updated-date
      shell: bash
      env:
        TZ: UTC0
        PATH_TO_FILE: ${{ inputs.our-path }}
      run: |
        value=$(\
          git log -1 \
            --date="format-local:%Y-%m-%dT%H:%M:%SZ" \
            --pretty="format:%cd" \
            "${PATH_TO_FILE}" \
        )
        echo "::set-output name=value::$value"
    - id: find-prs
      uses: octokit/graphql-action@v2.x
      env:
        GITHUB_TOKEN: ${{ inputs.github-token }}
      with:
        query: |
          query FindAssociatedPullRequests(
            $since: GitTimestamp!,
            $path: String!,
            $owner: String!,
            $repo: String!,
          ) {
            repository(owner: $owner, name: $repo) {
              defaultBranchRef {
                target {
                  ... on Commit {
                    history(since: $since, path: $path) {
                      nodes {
                        associatedPullRequests(first: 1) {
                          nodes {
                            number
                            url
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        since: ${{ steps.get-last-updated-date.outputs.value }}
        path: ${{ inputs.their-path }}
        owner: ${{ inputs.their-owner }}
        repo: ${{ inputs.their-repo }}

GitHubのGraphQL API (v4) を上手く使って、特定のファイルの特定日時以降のコミット・それに紐付くPRをリストアップしています。これで人力で関連PRを探しに行かなくてよくなりました。めでたしめでたし。

このようなワークフロー・改善により、自分達のGraphQL APIとこれからも仲良く付き合っていきたい。

期間の扱い方とその名前

とあるAPIスキーマの叩き台をクライアントサイドとして検討している際に、コンテンツの公開期間やイベントの開催期間のような期間について議論が少し盛り上がった。

要件としては、期間の開始と終了の日時をそれぞれ取得できたい。

期間を考える時、開始と終了がそれぞれinclusiveなのかexclusiveなのかをまず考慮すべきであるが、開始日時の重複や終了日時に隙間を発生させないためには、開始はinclusive、終了はexclusive、つまり半開区間(左閉右開)にするのが望ましいだろう。

終了をexclusiveにすると、例えば8月の1ヶ月間、つまり8月1日0時0分〜9月1日0時0分という期間の場合、ユーザー向けの表示としては終了日時は「8月31日23時59分まで」と表示したくはなるが、これはプレゼンテーションロジックとしてクライアントサイドの責務としてやる。基本的には-1秒してからフォーマットすればよい。

次にその名前を簡単に考えてみるとsince / until のような名前が思い付く。ここであるメンバーから「untilにはinclusiveな意味合いを感じてしまうがどうだろうか」という意見が出た。

untilはinclusiveかexclusiveなのかを少し調べてみると、どちらの意味にも取り得て、文脈依存であるようだ。

ここではuntilはexclusiveとして扱いますよ、ということでもいいのだが、『リーダブルコード』にもそういう話がありましたね、ということをまた別のメンバーが提示してくれた。該当箇所は「3.5 包含/排他的範囲にはbeginとendを使う」で、

 ここに使う仮引数の名前は何がいいだろうか? プログラミングの命名規約では、包含/排他的範囲に begin と end を使うことが多い。

 でも、end は少しあいまいだ。例えば、「本の終盤(the end of the book)を読んでいる」の「end」は包含的だ。残念ながら英語には「ちょうど最後の値を超えたところ」を意味する簡潔な言葉がない。

 begin と end の対はイディオムになっている(少なくとも C++ の標準ライブラリではこれが使われている。また、配列がこのように「スライス」されることも多い)ので、これが最善の選択と言える。

begin / end というイディオムが示されていた。我々の扱うAPIでは開始日時としてstartAtという命名例もあったので、これを考慮すると startAt / endAt ということになるだろうか(DBの日時系のカラムで created_atupdated_at とするように、日時系のデータに At サフィックスを付けている)。

また別の例として、半開区間の話にも絡むが、java.timePeriodDurationAPIはこのようになっていた。

APIの仕様として左閉右開の半開区間であることを明にして、引数名にもInclusiveとExclusiveを入れていて冗長だが分かりやすい。今回のケースではそこまで冗長にはせず startAt / endAt に落ち着きそうになったのであった。

学びある議論で楽しかった。

Mockolo 1.7.0のビルド済みバイナリがarm64にしか対応していない

https://github.com/uber/mockolo/releases/download/1.7.0/mockolo.tar.gz に含まれる mockolo バイナリがarm64アーキテクチャーにしか対応していなかった。これだとAppleシリコンMacでは実行できるが、Intel Macでは実行できない。

$ file mockolo
mockolo: Mach-O 64-bit executable arm64

その前はどうかと思って1.6.3を確認してみたら、そちらはx86_64だけだった。これは本当はx86_64でもarm64でも実行できるユニバーサルバイナリになっていてほしい。

ということで修正PRを出してみている。SwiftPMでは swift build の引数に --arch arm64 --arch x86_64 を足すだけでユニバーサルバイナリにできるので、それをやっただけです。

github.com

追記

このPRを含む1.7.1がリリースされていました。Intel Macの方もAppleシリコンMacの方もどうぞご利用ください。