SwiftUI.ViewとUIHostingControllerの初期化順序の調整

UIHostingControllerを使ってSwiftUI.Viewを表示する時に、Viewの引数に渡すクロージャーの中でViewControllerを使いたいことが稀によくある(本当に?)。

素直に考えるとこうなる。

// 順序的にvcを使えない
let view = FooView(onTap: {
    // vcで何かしたい
})
let vc = UIHostingController(rootView: view)

少し頭を捻るとこうなる。

let view = FooView()
let vc = UIHostingController(rootView: view)
vc.rootView.onTap = { [weak vc] in ... }

UIHostingController.rootViewpublic var rootView: Content なので、rootViewのプロパティを変更することはできる。だけどViewのプロパティを自分で書き換えるのはなんだかしっくりこない。

rootViewがvarということは別のことが考えられる。

let vc = UIHostingController<FooView?>(rootView: nil)
let view = FooView(onTap: { [weak vc] in ... })
vc.rootView = view

rootViewをnilで初期化しておき、後で差し替える作戦。 open class UIHostingController<Content> : UIViewController where Content : View という定義に対してnilを渡せるのか?という気持ちに一瞬なるが、これはOptional(とNever)がconditonal conformanceでViewに準拠しているのでvalidである。

extension Optional : View where Wrapped : View {
    public typealias Body = Never
}

という感じで、rootViewにnilを渡すことによる遅延初期化を上手く使っていきましょうというお話でした。

RenovateのHandlebarsテンプレート用の`lowercase`ヘルパーを追加した

RenovateとHandlebarsのテンプレート

Renovateでは様々な設定オプション正規表現を用いるregex managerHandlebarsのテンプレートを使用できる箇所があります。

例えばPRのdescriptionの内容を追加できるprBodyNotesなどがありますが、RenovateがPRを作る際のブランチ名を決めるbranchNameもデフォルト値は次のようなテンプレートになっています。

"{{{branchPrefix}}}{{{additionalBranchPrefix}}}{{{branchTopic}}}"

次のリンクはテンプレート中で使えるフィールド(変数)のドキュメントです。

repology datasourceと大文字小文字

さて、最近regex managerと組み合わせてrepology datasourceを使いたいことがありました。repology datasourceの中で使われているRepologyのSingle project API*1 https://repology.org/api/v1/project/$project の問い合わせでは、プロジェクト名が全て小文字である必要があります。

https://repology.org/api/v1/project/firefox なら結果が得られますが、https://repology.org/api/v1/project/Firefox だと結果は空になってしまいます。

正規表現で何かしらのファイルからキャプチャできるライブラリーの名称に大文字が含まれていると、そのままではrepology datasourceで問い合わせることができません。またRenovateが組み込みで提供するHandlebarsのヘルパー(テンプレート内で使える関数のようなもの)は当時、次の6つでした。

  • stringToPrettyJSON
  • encodeURIComponent
  • replace
  • containsString
  • and
  • or

replaceを複数回使って個々のライブラリーを列挙するわけにもいかないので、lowercaseヘルパーを追加しようと思い立ちました。まずIssueを立てて方針に問題がないかを確認してから、自分で実装まで行いました。

実装自体はJavaScripttoLowerCase()を呼んでヘルパーとして登録するだけで至極簡単ですね。

まとめ

これで packageNameTemplate: "{{{lowercase depName}}}" のようにして、キャプチャしたライブラリー名を小文字に変換してRepologyへの問い合わせに使えるようになりました。めでたしめでたし。

regex managerとrepology datasourceの組み合わせについては、後日また実用例の紹介をする予定です。乞うご期待(自分へのプレッシャー)。

Renovateの「About us」というドキュメントにcontributorとしてノミネートされた

はい。

こちらのページからご確認いただけます。

これまでのPR一覧を見ると、社内でRenovateを導入していくにあたってGradleとかsbt周りをいじったり、ちょっとしたリファクタリングなどもやっていた感じですね。今後も何かしら少しずつ貢献していきたい所存です。

あわせて読みたい

Accompanistの開発版のバージョニングが独特で、Renovateと相性が悪い

Accompanistとは

Google公式の、Jetpack Composeを補完する拡張ライブラリーです。

Accompanistの開発版のバージョニング

直近の0.24系の開発版が次のようになっている。

  1. v0.24.0-alpha
  2. v0.24.1-alpha
  3. v0.24.2-alpha
  4. v0.24.3-alpha
  5. v0.24.4-alpha
  6. v0.24.5-alpha
  7. v0.24.6-alpha
  8. v0.24.7-alpha
  9. v0.24.8-beta(ここからbetaに変わった)
  10. v0.24.9-beta
  11. v0.24.10-beta

SemVer的に見ると、v0.24.z のz部分はmajor・minor・patchのpatch部分に見えるが、Accompanistは現状SemVer(のpatch)を使ったバージョニングではなさそうで、z部分は開発版として何番目のリリースか、という数字でしかないようである。alphaの中で何番目、betaの中で何番目、という情報はなく、開発版全体として何番目で、その時の状態がalphaかbetaなのかが最後に付く、となっている。

SemVerなら

  1. v0.24-alpha.1
  2. v0.24-alpha.2
  3. v0.24-alpha.3
  4. v0.24-alpha.4
  5. v0.24-alpha.5
  6. v0.24-alpha.6
  7. v0.24-alpha.7
  8. v0.24-alpha.8
  9. v0.24-beta.1
  10. v0.24-beta.2
  11. v0.24-beta.3

という付け方になるであろう。

Renovateでの開発版のバージョンへの更新

RenovateはデフォルトでignoreUnstableという設定がオンになっていて、安定版から開発版(非安定版)への更新がされないようになっている。ただし例外があって、現在使用中のバージョンが安定版ではなく開発版であり、更新対象となる最新版とmajor・minor・patchが一致する場合は(SemVerでいうところのpre-release version以降だけが更新されたら)更新してくれる。

By default, Renovate won't update any package versions to unstable versions (e.g. 4.0.0-rc3) unless the current version has the same major.minor.patch and was already unstable (e.g. it was already on 4.0.0-rc2).

ここでAccompanistの開発版のバージョニングに戻ってみると、SemVerの観点では毎回patch部分が更新されていることになる。従って先述の条件を満たさず、例えば v0.24.8-beta から v0.24.10-beta への更新のPRは用意されないことになっている。この、ある開発版から、major・minor・patchが一致しない別の開発版への更新が起きないことはドキュメントにも明記されている。

Renovate will also not "jump" unstable versions automatically, e.g. if you are on 4.0.0-rc2 and newer versions 4.0.0 and 4.1.0-alpha.1 exist then Renovate will update you to 4.0.0 only.

ということで、このようなバージョニングでも開発版に追従していきたい時*1ignoreUnstable を明示的にオフ(false)にしましょう。

  "packageRules": [
    {
      "matchPackagePatterns": ["^com\\.google\\.accompanist:"],
      "ignoreUnstable": false
    }
  ]

*1:Jetpack Composeの開発版を使う時は、Accompanistも対応する開発版を使うことになる

potatotips #77で「RenovateによるiOSライブラリーの自動更新」という発表をしました&いくつかの補足

発表資料はこちらです 👇

発表の中では時間の都合で触れられなかったところをいくつか補足しようと思います。

CocoaPodsでRealmを使っているとPodfile.lockが更新できない件

github.com

Renovateの実行環境はDockerコンテナの中のUbuntuです。Renovateは Podfile を書き換えた後に pod install を実行してPodfile.lock を更新します。CocoaPodsはLinuxで動くのか?と思われるかもしれませんが、pod gem(や依存の xcodeproj)もLinuxで問題なく動作するようです。ほとんどのライブラリーは問題ないのですが、Realmのpodspecには少し特殊なところがあります。

https://github.com/realm/realm-swift/blob/8c1cbf124c5025da9220595e7764059963065891/Realm.podspec#L110

  s.prepare_command         = 'sh scripts/setup-cocoapods.sh'

podspecのprepare_commandスクリプトを実行しています。そのスクリプトではrealm-coreをダウンロードしているのですが、その先頭でset -euo pipefailとしており、bashではなくshだとエラーになってしまいます。

build.sh: 13: set: Illegal option -o pipefail

さらにここをbashで実行したとしてもスクリプト内部でxcode-selectコマンドが使われているので、どちらにせよ詰んでしまいます。

ここで我々ができることは、発表のSwiftPMの箇所で「Package.resolvedの更新に非対応」でも触れた

  • PRに自分で追いコミットをするか、もしくは
  • CIで(例えばGitHub Actionsで)更新するワークフローを用意する

という感じになりそうです。

on:
  pull_request:
    paths:
      - Podfile
jobs:
  precheck:
    runs-on: ubuntu-latest
    outputs:
      files_changed: ${{ steps.file_changes.outputs.files }}
    steps:
      - uses: actions/checkout@v3
      - id: file_changes
        uses: trilom/file-changes-action@v1.2.4
  update:
    runs-on: macos-latest
    needs: precheck
    if: contains(fromJson(needs.precheck.outputs.files_changed), 'Podfile.lock') == false
    steps:
      - ...

CocoaPodsとXcodeGenを併用しているとPodfile.lockが更新できない件

XcodeGenを使用していると、通常生成したXcodeプロジェクトはリポジトリにコミットせず、CIでも初期セットアップでコマンドを実行してXcodeプロジェクトを生成すると思います。これがRenovateでは困ったことになります。Renovateの実行時に任意の初期化処理を挟むことができず(そもそも実行環境にXcodeGenも存在しない)、Xcodeプロジェクトが存在しないままだとpod installの実行に失敗してしまうためです。

これを回避する手段として、CocoaPodsの:integrate_targetsという設定を使用できます。Podfileのルートではinstall!というオプションを設定することができます。その引数に :integrate_targets => false と指定することで、Xcodeプロジェクトへの組み込みステップが省略されるため、Xcodeプロジェクトがなくても pod install を実行できるようになります。

この設定をRenovateでの実行時だけ有効にするように、以下のようにハックします。

is_running_in_renovate = ENV['HOME'] == '/home/ubuntu'
if is_running_in_renovate
  # Renovateでの実行時にはXcodeGenが実行できず、xcodeprojが存在しないので `:integrate_targets => false` とする。
  install! 'cocoapods', :integrate_targets => false
else
  install! 'cocoapods'
end

ただこの設定には注意点があり、:integrate_targets => falseだとアプリが使用しているSwiftのバージョンをXcodeプロジェクトから抜き出すことできなくなり、ライブラリー側のビルド設定のSWIFT_VERSIONが設定されなくなってしまいます。

これも回避策があり、Podfile内の各ターゲットで current_target_definition.swift_version = '5.0' と明示的に定義してしまえば問題ありません。

target 'FooBarApp' do
  # https://github.com/CocoaPods/CocoaPods/issues/8653#issuecomment-488767262
  current_target_definition.swift_version = '5.0'
  ...
end

おわりに

モバイルアプリ開発でもどんどんRenovateを活用していきたいですね!!

その他の方の発表資料はconnpassのページからどうぞ!

actions/checkoutのアップデートで、gitのsafe.directoryの件が対策されていました

先週盛り上がっていた、gitのCVE-2022-24765に対する修正でreviewdogなど一部のGitHub Actions(Dockerコンテナのアクション)が動かなくなっていた問題ですが

actions/checkout側でチェックアウトしたディレクトリをsafe.directoryにセットしてくれる対策が入っていました。

Issue:

リリース:

set-safe-directoryというinputで制御できるようにもなっています。デフォルトで有効ですが、

- uses: actions/checkout@v3
  with:
    set-safe-directory: false

として無効にすることもできます。

これで安心して使えそうですね。こちらからは以上です。

GitHub ActionsのVirtual Environmentの更新でnpmのバージョンが変わったことでreviewdog/action-eslintが動かなくなったという面白事例