ChatGPTを使ってOpenAPIの定義を作成してモックサーバを立てる

こんにちは、エンジニアの黒岩(@kro96_xr)です。

はじめに

ChatGPT、流行ってますね。
openai.com

VSCodeの拡張を使ってコードレビューをしたり
qiita.com

SQLを書いてもらったり

上手く活用することで開発生産性が上がっていくのではないかと思っています。
実際、私も面倒なOpenAPIのymlファイルの記述などを書いてもらったりしています。
というわけで今回はChatGPTを活用してモックサーバを爆速で立てていきたいと思います。

なお、ChatGPTの使い方は割愛させていただきます。

また、ChatGPTに関する著作権を調べていたところ、商業目的での使用はNGのようでした。
ブログへの掲載も広義で見れば宣伝とも言え、グレーなのかもしれないと思っていますが、詳しい方がいらっしゃいましたらご教示いただけますと幸いです。

note.com

とりあえずTODOアプリのAPIを定義する

今回はサンプルなので設計からChatGPTに投げてしまいます。
実際に使う場合は、ドメイン知識を事前に教えたり、自身で作成した具体的なjsonのレスポンスを教えたりしてからお願いした方が良いです。

普通のCRUDのエンドポイントですね。ymlにしてもらいましょう。

出力されたファイルをyml形式で保存しておきます。
Swagger Editorにコピペすればドキュメント形式でも見ることができますね。

モックサーバを立てる

次に、保存したymlファイルからモックサーバを立てましょう。今回はdockerを使います。
公式で配布されているイメージを使って、yamlファイルを指定するだけです。

openapi.ymlと同じディレクトリに以下のようなdocker-compose.ymlを作って

version: "3"
services:
  mock:
    image: stoplight/prism:latest
    container_name: "prism-mock"
    ports:
      - "4010:4010"
    command: mock -h 0.0.0.0 /openapi.yml
    volumes:
      - ./openapi.yml:/openapi.yml

upコマンドでコンテナを立ち上げればOKです。

docker-compose up
prism-mock | [5:12:29 AM] › [CLI] … awaiting Starting Prism…
prism-mock | [5:12:30 AM] › [CLI] ℹ info GET http://0.0.0.0:4010/todos
prism-mock | [5:12:30 AM] › [CLI] ℹ info POST http://0.0.0.0:4010/todos
prism-mock | [5:12:30 AM] › [CLI] ℹ info GET http://0.0.0.0:4010/todos/404
prism-mock | [5:12:30 AM] › [CLI] ℹ info PUT http://0.0.0.0:4010/todos/657
prism-mock | [5:12:30 AM] › [CLI] ℹ info DELETE http://0.0.0.0:4010/todos/878
prism-mock | [5:12:30 AM] › [CLI] ▶ start Prism is listening on http://0.0.0.0:4010

これでモックサーバを立てることが出来ました。

GETも

POSTも

大丈夫そうですね。

あとはスキーマ駆動開発という形でバックエンドとフロントエンドが同時並行で開発できると良い感じなのではないでしょうか!

おまけ

最近Locustでの負荷試験のためのコードを見たりしているのですが、こちらもできるかどうか確認してみました。

古いバージョンのコードを出してきたので…

修正してもらって動くことが確認できました!

おわりに

今回はChatGPTを活用して、API定義を作りモックサーバを立ててみました。
チャットでやり取りするだけで動くものが出来るのでサクッと試したいときに便利ですし、うまく活用すればちゃんとしたシステムも作れる気がしますね。

※なお質問する内容によっては普通に間違ったことが返ってくるのでご注意ください。

技術的な質問ではありませんが間違った例です。
スタジアムは等々力陸上競技場が正しく、創設年は前身の富士通サッカー部の創部年になってますね。

Enhanced Scroller × MVPパターン × UniRxを使ってUnityで高機能なScrollerをいい感じに実装する

こんにちは、エンジニアリングマネージャーの渡辺(@mochi_neko_7)です。

現在Synamonが開発しているサービスのSYNMNでは、2DUI(uGUI)のScrollerの実装にはUnity標準のScrollRectではなく、有料アセットのEnhanced Scrollerを使用しています。*1

Scrollerはサーバーから可変長の要素を取得して表示する際など割と高頻度で使うUIです。

Enhanced Scrollerは便利なアセットなのですが、意外と日本語の記事などが少ない印象です。

ちょうど最近自分がEnhanced Scrollerを色々触っていたので、意識すべき設計の一例としてMVPパターン、UniRxを組み合わせた高機能なScrollerの快適な実装例を紹介します。

こんな方におすすめの記事となります。

  • 高機能なScrollerのアセットの導入を検討している方
  • Enhanced Scrollerを使用したサンプルコードを見たい方
  • Enhanced ScrollerとUniRxを組み合わせて便利に使い回すプラクティスが見たい方

*Enhanced Scrollerの宣伝みたいな書き方になっていますが、そういう訳ではありません...!

本記事内のサンプルコードの使用環境は下記になります。

  • Unity 2021.3.0f1
  • Enhanced Scroller 2.33
  • UniRx 7.1.0

注意点としてサンプルコードのイベント処理でUniRxを多用しますが、そこは本質ではありませんのであまり解説はしません。

UniRxの使い方などは別の解説記事を参考にしてください。

qiita.com

それでは早速本題に入りましょう。

Enhanced Scrollerの簡単な紹介

Enhanced ScrollerはUnity Asset Storeで購入可能な高機能Scrollerのアセットの一つです。

assetstore.unity.com

Enhanced Scrollerのメリット、デメリットを簡潔にまとめると下記になります。

メリット

  • Scroll内のUIの表示要素(View)を内部でPoolingしてくれるため、要素数が増えてもパフォーマンスが落ちにくい
  • Pull to RefreshやPaginationなど、よくある直感的なUXが簡単に実装できる

デメリット

  • Scriptでのセットアップが必要なため、デザイナーだけで作成するのは少しハードルが高い

基本的な使い方やサンプルは既に紹介記事があるため、こちらを参考にしてください。

light11.hatenadiary.com

light11.hatenadiary.com

この記事では基本的な使い方からもう一歩踏み込んで、実際にEnhanced Scrollerを組み込む際に意識すると良い設計、使い勝手が更に良くなる点を紹介します。

Enhanced ScrollerとMVPパターンの組み合わせ

Enhanced Scrollerはメリットに

Scroll内のUIの表示要素(View)を内部でPoolingしてくれるため、要素数が増えてもパフォーマンスが落ちにくい

と挙げたように、表示要素を使い回して最適化をしています。

そのため、UI(View)とデータ(Model)は一対一の対応関係にないため、UIとデータを明確に切り分けて管理する必要があります。

これはMVPパターンを始めとする、View(UI)とModel(データやロジック)を分離する設計方針と相性が良いです。

言葉で説明するだけだと少し分かりにくいかもしれませんので、実装例を見てみましょう。

Enhanced Scrollerのシンプルな使用例として、下記を考えます。

  • Scrollの要素
    • Button
    • そのButtonを押した回数の表示
  • 要素数は可変
  • 要素の高さは固定

ここではMVPパターンの使用例として、UniRxを用いたMV(R)Pパターンを採用します。

qiita.com

まずはパターンに則って、ModelとViewを作成しましょう。

今回の使用例のModelはシンプルです。

  • Countを増やす操作ができる
  • CountをPropertyとして保持している
using UniRx;

public sealed class CounterModel
{
    private readonly ReactiveProperty<int> count = new();
    public IReadOnlyReactiveProperty<int> Count => count;

    public void CountUp()
    {
        count.Value++;
    }
}

MonoBehaviour を継承する理由もないのでシンプルなclassとして実装します。

Viewの要件もシンプルです。

  • Countを増やす操作のためのButtonを持っていて、Buttonが押されたイベントを提供する
  • Countを表示するText(TextMeshPro)を持っていて、表示処理を提供する
using System;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UI;

public sealed class CounterView : EnhancedScrollerCellView
{
    [SerializeField] private Button countUpButton = null;
    [SerializeField] private TMP_Text countText = null;

    private readonly Subject<Unit> onCountUpButtonClicked = new();
    public IObservable<Unit> OnCountUpButtonClicked => onCountUpButtonClicked;

    private void Awake()
    {
        Assert.IsNotNull(countUpButton);
        Assert.IsNotNull(countText);

        countUpButton
            .OnClickAsObservable()
            .Subscribe(onCountUpButtonClicked)
            .AddTo(this);
    }

    public void DisplayCount(int count)
    {
        countText.text = count.ToString();
    }
}

Enhanced Scrollerの仕様のために EnhancedScrollerCellView を継承します。

MVPアーキテクチャに則ってModelとViewがお互いのことを知らずに完結していることが分かると思います。

最後に本題であるEnhanced Scrollerを使用したModelとViewの繋ぎ込みとScrollerの実装を見ていきましょう。

Modelをどこでいくつ生成するかは実装側で任意に選択できるのですが、今回は雑に Start() の中で適当な数だけ生成します。

using System.Collections.Generic;
using EnhancedUI.EnhancedScroller;
using UniRx;
using UnityEngine;
using UnityEngine.Assertions;

public sealed class CounterScrollPresenter : MonoBehaviour, IEnhancedScrollerDelegate
{
    [SerializeField] private EnhancedScroller enhancedScroller = null;
    [SerializeField] private CounterView viewPrefab = null;

    private readonly List<CounterModel> models = new();
    private readonly List<CompositeDisposable> eventSubscriptions = new();

    int IEnhancedScrollerDelegate.GetNumberOfCells(EnhancedScroller scroller)
        => models.Count;

    private float? cellViewSize = null;

    float IEnhancedScrollerDelegate.GetCellViewSize(EnhancedScroller scroller, int dataIndex)
    {
        if (cellViewSize == null)
        {
            cellViewSize = viewPrefab.GetComponent<RectTransform>().rect.height;
        }

        return cellViewSize.Value;
    }

    EnhancedScrollerCellView IEnhancedScrollerDelegate.GetCellView(EnhancedScroller scroller, int dataIndex, int cellIndex)
        => scroller.GetCellView(viewPrefab);

    private void Start()
    {
        Assert.IsNotNull(enhancedScroller);
        Assert.IsNotNull(viewPrefab);

        // Modelのリストの生成
        for (var i = 0; i <= 10; i++)
        {
            models.Add(new CounterModel());
            eventSubscriptions.Add(new CompositeDisposable());
        }

        // Enhanced Scrollerのセットアップ
        enhancedScroller.Delegate = this;

        enhancedScroller
            .CellViewVisibilityChangedAsObservable<CounterView>()
            .Where(view => view.active)
            .Subscribe(ActivateCell)
            .AddTo(this);

        enhancedScroller
            .CellViewWillRecycleAsObservable<CounterView>()
            .Subscribe(DeactivateCell)
            .AddTo(this);

        enhancedScroller.ReloadData();
    }

    private void OnDestroy()
    {
        models.Clear();

        foreach (var eventSubscription in eventSubscriptions)
        {
            eventSubscription.Dispose();
        }

        eventSubscriptions.Clear();
    }

    private void ActivateCell(CounterView view)
    {
        var model = models[view.dataIndex];
        var eventSubscription = eventSubscriptions[view.dataIndex];
        eventSubscription.Clear();

        // View -> Modelの繋ぎ込み
        view
            .OnCountUpButtonClicked
            .Subscribe(_ => model.CountUp())
            .AddTo(eventSubscription);

        // Model -> Viewの繋ぎ込み
        model
            .Count
            .Subscribe(view.DisplayCount)
            .AddTo(eventSubscription);
    }

    private void DeactivateCell(CounterView view)
    {
        eventSubscriptions[view.dataIndex].Clear();
    }
}

見た目は適当ですが実際に動かしてみるとこのような形になります。

ソースコードはちょっと長いですが、ポイントをいくつか挙げます。

  • ActivateCell() でCell(Scrollの一要素)に対応するModelとViewの繋ぎ込みをしていますが、ここがPresenterとしての主な役割です
  • DeactivateCell() でCellのイベント登録を解除しています
  • 上記の操作をEnhanced Scrollerのイベントのそれぞれ、CellViewVisibilityChangedCellViewWillRecycle で実行しています
  • 上のソースコードではObservableに変換する拡張メソッドを使用していますが、次の章で解説します

特にEnhanced Scrollerのイベントを利用する部分が重要です。

前述した通りEnhanced ScrollerはViewを使い回す(CellViewのオブジェクトをPoolingする)ため、ModelとViewのインスタンスは一対一対応ではありません。

そのため、CellがActiveになったタイミングでViewとModelの繋ぎ込みをし、Cellが再使用されたタイミングでその繋ぎ込みを解除しています。

この処理を適切に行わないと、1つのViewに対して複数のModelが繋ぎ込まれて1つのButtonを触っただけで2つ以上のModelにイベントが飛んでしまう、といったことが起こり得ますので注意してください。

他のコードは通常通りのEnhanced Scroller、UniRxの使用方法通りですので、ModelやViewのロジックが多少変わってもほぼ同様の流れで実装ができるかと思います。

必ずしもMVPアーキテクチャである必要はありませんが、ModelとViewの分離を意識した設計をすると見通しの良い画面の実装ができるため、Enhanced Scrollerを使用する際は意識してみると良いかもしれません。

Enhanced ScrollerのイベントをUniRxで扱う

先ほど挙げたサンプルコードの中の、Enhanced Scrollerのイベントを利用する部分に関して補足をします。

Enhanced Scrollerの提供するイベントはどれも delegete 型で、そのままではUniRxのStreamとしては扱うことができません。

ですがMV(R)PでUniRxを活かした実装をする時には、これらのイベントもStreamとして扱いたくなります。

この場面で便利なのが、UniRxの Observable.FromEvent です。

実際に EnhancedScroller.CellViewVisibilityChanged のイベントを IObservable<T> に変換する拡張メソッドとして定義したものがこちらになります。

// CellViewVisibilityChangedDelegate -> IObservable<TCellView>
public static IObservable<TCellView> CellViewVisibilityChangedAsObservable<TCellView>(this EnhancedUI.EnhancedScroller.EnhancedScroller scroller)
            where TCellView : EnhancedScrollerCellView
    => Observable
            .FromEvent<CellViewVisibilityChangedDelegate, TCellView>(
                conversion:h => cellView => h.Invoke(cellView as TCellView),
                addHandler:h => scroller.cellViewVisibilityChanged += h,
                removeHandler:h => scroller.cellViewVisibilityChanged -= h);

パラメータの指定部分が型パズルみたいなことになっていてやや複雑ではあるのですが、FromEvent<T1, T2> の型引数がそれぞれ元の delegate、変換後のStreamの扱う型に対応していることが分かります。

Enhanced Scrollerの他のイベントも同様の実装が可能です。

Push to Refresh、Paginationをするときに便利な拡張メソッドも紹介します。

// ScrollerScrolledDelegate -> IObservable<(Vector2 value, float scrollPosition)>
public static IObservable<(Vector2 value, float scrollPosition)> ScrollerScrolledAsObservable(this EnhancedUI.EnhancedScroller.EnhancedScroller scroller)
    => Observable
        .FromEvent<ScrollerScrolledDelegate, (Vector2 value, float scrollPosition)>(
            conversion:h => (enhancedScroller, value, scrollPosition) => h.Invoke((value, scrollPosition)),
            addHandler:h => scroller.scrollerScrolled += h,
            removeHandler:h => scroller.scrollerScrolled -= h);

// Scrollの上端に到達した時にイベントを発行する
public static IObservable<Unit> ScrollerReachedUpperLimit(this EnhancedScroller scroller)
    => scroller
        .ScrollerScrolledAsObservable()
        .Where(value => value.value.y >= 1f) // 0(下) <= value.y <= 1(上)が通常のScrollの可動域
        .Select(_ => Unit.Default);

// Scrollの下端に到達した時にイベントを発行する
public static IObservable<Unit> ScrollerReachedLowerLimit(this EnhancedScroller scroller)
    => scroller
        .ScrollerScrolledAsObservable()
        .Where(value => value.value.y <= 0f)
        .Select(_ => Unit.Default);

// Scrollの上端より下に引っ張って放した時にイベントを発行する
public static IObservable<Unit> EndDraggedOverUpperLimit(this EnhancedScroller scroller)
{
    var endDragStream = GetOrAddComponent<ObservableEndDragTrigger>(scroller.gameObject)
        .OnEndDragAsObservable()
        .Select(_ => Unit.Default);

    return Observable
        .Zip(scroller.ScrollerReachedUpperLimit(), endDragStream) // Streamの合成をして、OnEndDragとScrollerReachedUpperLimitが両方呼ばれている時にイベントを発行する
        .Select(_ => Unit.Default);
}

Zipの挙動の把握があまり自信がないため、もし使い方が間違っていたらご指定いただけるとありがたいです。

これらのような拡張メソッドを用意しておくことで、Enhanced ScrollerとUniRxを組み合わせた際のイベント周りの実装がより快適になると思います。

おわりに

Enhanced Scrollerが便利なのはもとより、UniRxを使ったMV(R)Pパターンを用いることでデータやロジックの見通しが良くなり、イベントの取り回しも快適になることが伝わりましたでしょうか。

ModelのUnit Testの実装も容易なため、データやロジックが複雑な場面ではより効果が大きいと思います。

あくまで一実装例に過ぎませんが、Enhanced Scrollerの使用例、MV(R)Pパターンの応用例としても参考になれば幸いです。

*1:正確にはEnhanced Scrollerの内部ではScrollRectを使用しているのですが

GitHub Actionsを使ってアプリを開発するときのワークフロー記述の個人的ベストプラクティス

皆さん、あけましておめでとうございます! エンジニアの岡村です。

今年もSynamon's Engineer blogでは皆様の開発の参考になることを願って情報を発信していきます!

GitHub Actions

弊社ではマルチプラットフォーム向けのアプリをビルドすることが多いため、CIは重要です。以前はJenkinsを使っていましたが、SYNMNの開発にあたりGitHub Actionsへの移行を行いました。

開発もある程度軌道に乗り、GitHub Actionsもそこそこ慣れてきたので、現時点で感じているワークフローを運用する際に便利だったことを紹介しておきます。

GITHUB_STEP_SUMMARYに、後から確認出来るようにワークフローのパラメータを書き出しておく

GitHub Actionsのサマリー画面はデフォルトではごく僅かな情報しか載っていませんが、ワークフロー中にワークフローコマンドを実行することで追加情報を載せることが出来ます。

docs.github.com

特にworkflow_dispatchなどでワークフローを手動実行した場合などは、ワークフローの引数を後から確認することが難しい為、そういった情報を出力しておくことで後で混乱せずに済みます。

GITHUB_STEP_SUMMARYはmarkdownが使えるため、以下のように出力しておけば表形式になって視認性が向上します。

        run: |
          echo "|Properties|Value|" >> $GITHUB_STEP_SUMMARY
          echo "|---|---|" >> $GITHUB_STEP_SUMMARY
          echo "|GitHub Ref|\`${{ github.ref }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Commit Hash|\`${{ github.sha }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Some Workflow Input|${{ inputs.hoge }}|" >> $GITHUB_STEP_SUMMARY

以下の画像は一例ですが、GITHUB_STEP_SUMMARYに記述した内容は、このようにArtifactsの下に表示されます。

ツールを動かすときはProblem Matchersを設定して、エラーの詳細をSummaryに出力するようにする

GitHubにはproblem_matcherという機能があり、設定しておくとログ出力から正規表現で一致するログを検出して、わかりやすくSummeryに出してくれたりインラインコメントを付けてくれます。

github.com

例えばUnityビルドを行う場合、以下のようなMatcherを適用しておくとコンパイルエラーを拾ってくれます。

{
  "problemMatcher": [
    {
      "owner": "Unity",
      "severity": "error",
      "pattern": [
        {
          "regexp": "^(.*\\.cs)\\((\\d+),(\\d+)\\):\\serror\\s(.+):\\s(.+)$",
          "file": 1,
          "line": 2,
          "column": 3,
          "code": 4,
          "message": 5
        }
      ]
    }
  ]
}

Matcherを有効化するには、そのステップの直前に以下のようなコマンドを実行します。

      - run: echo ::add-matcher::.github/workflows/unity-problem-matcher.json

以下の画像はこれまた一例ですが、上の設定をすることで以下のようにコンパイルエラー発生時に検出してくれます。

コンパイルが成功し、テストが失敗した場合はテスト結果ファイルを解析するAction(こちらなど)に流す事が出来るのですが、テスト実行以前のエラーに関しては、通常はステップの中までログを見に行かなければならないので、Matcherを設定しておくことで時間が短縮できます。

あと、上記画像では何故か同じエラーが4つも出力されていますが、まあ何も出ないよりはマシという事で一旦放置しています……。

workflow_callでトリガーと処理を分割する

一つのワークフローを、ワークフローの実行条件等を記述する呼び出し側と、ビルドなどの実際の処理を書く側に分割したところ、見通しが良くなりました。

背景として、GitHub Actionsはyamlベースで記述し、基本的には問題なく使えるのですが、JenkinsのGroovy Scriptに比べてコードが重複しやすく、特にマルチプラットフォームで微妙にmatrix化し辛いような処理を書いているとすぐに同じコードだらけになってしまっていました。

JenkinsのGroovy Scriptなら関数を使って同じ処理を纏めることが出来るのですが、Actionsではそれが出来ません。yamlにはアンカーやエイリアスといった、定義したブロックを使いまわせる機能があるのですが、GitHub Actionsではそれが動作しない(2023年1月時点)ためです。

github.com

workflowは基本的にGitHub上でしか動かない(ローカルで検証しづらい)為、ローカルで叩いても問題ないような処理単位は外部スクリプトにまとめた方が良いのですが、キャッシュや成果物のアップロード、シークレット情報の管理などのGitHub Actions上で動かすためのステップに関しても中々の量になります。

そこで、最低限の分割として、Workflow callという機能を使い、トリガー専用のworkflowと処理専用のworkflowを分けてみました。

これによりdevelopブランチならテストとビルドを両方実施、featureブランチならテストだけ実施というように、実行タイミングの制御も各ワークフローに分散してしまうことがなくなりました。

サンプル

上で紹介したような機能を使い、現状クライアントアプリ用のリポジトリ内のActionsは以下のようになっています(見せられない所が多いので大幅にカットしていますが……)全体の形はこうなるという参考程度にしていただければ幸いです。

トリガー用ワークフロー

name: on-develop

on:
  push:
    branches:
      - "develop"
    paths-ignore:
      - "**.md"
  workflow_dispatch:

jobs:
  build:
    uses: ./.github/workflows/build.yml
    secrets: inherit
    with:
      environment-context: Development
      custom-build-number: ${{ github.run_number }}
      development-build: true
  deploy:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
    needs: build
    steps:
      # 成果物をテスト環境にデプロイする処理など

処理用ワークフロー

name: build

on:
  workflow_call:
    inputs:
      environment-context:
        required: true
        type: string
      custom-build-number:
        required: false
        type: string
        default: "0"
      development-build:
        required: false
        type: boolean
        default: false
      include-platforms:
        required: false
        type: string
        default: "Windows, macOS, Android, iOS"

#workflow_dispatchを設定しておくとbuild単体を手動で実行できるのでおススメ
  workflow_dispatch: 
    inputs:
      environment-context:
        description: Environment Context
        required: true
        type: choice
        options:
          - Release
          - Development
      custom-build-number:
        description: Custom Build Number
        required: false
        type: string
        default: ""
      include-platforms:
        description: Include Platforms
        required: false
        type: string
        default: "Windows, macOS, Android, iOS"

# run-nameを設定しておくと手動実行した際にGitHub上の実行履歴で表示される名前をカスタマイズできるのでおススメ
# workflow_callで実行された時は無視される
run-name: Build as ${{ inputs.context }} on ${{ github.ref_type }} ${{ github.ref_name }} [${{ github.sha }}] 

jobs:
  dump-properties:
    runs-on: ubuntu-latest
    steps:
      - name: Build arguments summary
        run: |
          echo "|Build Properties|Value|" >> $GITHUB_STEP_SUMMARY
          echo "|---|---|" >> $GITHUB_STEP_SUMMARY
          echo "|GitHub Ref|\`${{ github.ref }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Commit Hash|\`${{ github.sha }}\`|" >> $GITHUB_STEP_SUMMARY
          echo "|Environment Context|${{ inputs.environment-context }}|" >> $GITHUB_STEP_SUMMARY
          echo "|Custom Build Number|${{ inputs.custom-build-number }}|" >> $GITHUB_STEP_SUMMARY
          echo "|Include Platforms|${{ inputs.include-platforms }}|" >> $GITHUB_STEP_SUMMARY

  windows-build:
    runs-on: [self-hosted, windows, x64]
    if: contains(inputs.include-platforms, 'Windows')
    timeout-minutes: 90
    steps:
      # Windowsプラットフォーム向けのビルド処理

  android-build:
    runs-on: [self-hosted, windows, x64]
    if: contains(inputs.include-platforms, 'Android')
    timeout-minutes: 90
    steps:
      # Androidプラットフォーム向けのビルド処理

  ios-build:
    runs-on: [self-hosted, macOS, x64]
    if: contains(inputs.include-platforms, 'iOS')
    timeout-minutes: 90
    steps:
      # iOSプラットフォーム向けのビルド処理

  macos-build:
    runs-on: [self-hosted, macOS, x64]
    if: contains(inputs.include-platforms, 'macOS')
    timeout-minutes: 90
    steps:
      # macOSプラットフォーム向けのビルド処理

以上

GitHub Actionsは去年あたりからだいぶ使いやすくなってきたように感じます。github-hosted runnerの選択肢や日本語ドキュメントの量、まだ欲しい機能などは色々あるのですが、現状でも他のCIサービスに引けを取らない充実度だと思います。ネット上の記事も充実してきたので、今年からGitHub Actionsを触り始めて見るのも良いのではないでしょうか。また何か面白い使い方などを発見したら記事にしようと思います。

記事を読んでいただきありがとうございました!今年もSynamonとSYNMNを宜しくお願いいたします!

twitter.com

twitter.com

ポエム - .NET MAUI はモダンな "Write Once, Run Anywhere" を確立してほしいという希望

こんにちは、エンジニアの庭山(@rkoubou_jp)です。

本記事は Synamon Advent Calendar 2022 19日目の記事になります。

去年の Synamon Advent Calendar 2021 で MAUI の記事を書いたのですが、2022年5月に MAUI が GA(正式版) リリースとなり、先日リリースされた .NET 7 にも対応となったので続編的な記事を書こうと思います。

技術的な記事というよりかは、ほぼ個人的な主観なのでご了承ください。

synamon.hatenablog.com

目次

TL;DR

  • Preview版ではビルドするのも一苦労だったけど、難なくビルドできた
  • 色々設計思想などがあるのだろうが目指しているのは恐らく "Write (Code per Platform & Construct SDKs) Once, Run Anywhere" (*この記事での造語)っぽい
    • 個人的に Java Swing フレームワークは "Write Once, Run Anywhere" を追い求め、体現していた良い設計思想だったと思う
      • → 自分は設計思想やGUIプログラミング全般における考え方をSwingで学んだつもり
    • マルチプラットフォーム対応のフレームワーク開発、というのは大変(過去の経験)

MAUI 正式版がリリースされていた!

前回記事を書いた時点ではPreview版で、想定以上に四苦八苦してしまいました。その後 2022年5月に MAUI が GA(正式版) リリースとなりました。

github.com

Preview版ではプロジェクト作成時に組み込まれているサンプルのビルドするだけでも一苦労でした。

この記事を書いた時の環境

Windows PC と MacBook Pro 2021 (M1) で環境を構築を行いました。

  • Windows 10 Pro (x64)

    • .NET 7.0
    • Visual Studio 2022
    • Android SDK
      • ANdroid Simulator
  • macOS Montery

    • .NET 7.0
    • Visual Studio for Mac 17.4.1
    • Xcode 14.1
      • iOS Simulator
    • Android SDK
      • Android Simulator

アプリのプロジェクトは Windows の Visual Studio 上で新規プロジェクト(.NET MAUI アプリ)を作成した直後の状態のファイルです。

前回からの再挑戦 - アプリのビルドが普通にできた!

「そりゃそうだろう」と思われるでしょうが、Preview版で手こずっていた過去があるのでこれだけでも嬉しかったですね。

デバイスを指定して実行ボタンをクリックするだけで

Android シミュレータ、あっさり起動しました。

Windows マシンから Mac にリモート接続して iOS ビルド(シミュレータ)

Microsoftのサイトに手順が載っていたのでその通りにやって、トラブルなくビルド・実行ができました。

learn.microsoft.com

Mac, Windows 版ビルド

それぞれのOS上のVisual Studio でビルド、実行ができました。

Windows

Mac

MAUI は "Write Once, Run Anywhere" に「近い路線」を目指している(と勝手に解釈している)

"Write Once, Run Anywhere" というワードは元々 Java が登場した頃のJava開発者のスローガンでした。

ja.wikipedia.org

Write once, run anywhere(WORA、「一度〔プログラムを〕書けば、どこでも実行できる」[要出典])または Write once, run everywhere (WORE) とは、Javaのプログラムがオペレーティングシステム (OS) などのプラットフォームに依存しないという意味の、サン・マイクロシステムズのJavaのスローガンである。

先発の Java Runtime でも完全に達成したわけではないですが、Swingフレームワーク(GUI)はかなり良い線を行っていたと思っています。 個人的に Swing フレームワークを初めて触ったマルチプラットフォーム対応のGUIプログラミング環境だったこともあり、思い入れが強く、一部MAUIとの比較対象にしています。

MAUIは "Write Once, Run Anywhere" そのものを目指しているわけではないけど、なるべくそれに近い設計思想なのだろうと感じています。

"Write (Code per Platform & Construct SDKs) Once, Run Anywhere"

改めて短時間ながら MAUI を触ってみた感想は

"Write (Code per Platform & Construct SDKs) Once, Run Anywhere" (*この記事内の造語です)

かな、という感触です。 比喩ではないです。極力プラットフォームの事情を剥き出しにしないよう Visual Studio 側でかなりプラットフォーム固有の部分をフォローしてるんだろうなぁと感じています。

Code per Platform

Microsoftのドキュメントでは #if#endif などで条件分岐でプラットフォーム固有のコードや クロスプラットフォーム API 作成のアプローチを示しています。

learn.microsoft.com

MAUIではネイティブの機能にアクセスも可能にするため、標準のAPI仕様では抽象化をほどほどにしてアプリケーション開発側で 実現したいことベースに抽象化をどうするかを委ねている のだろうな、と感じています。 現状の対応プラットフォームがPCのみではなくモバイルOSやWebなど幅が広いので MAUI のAPI仕様でカバーしようとするとどんどんMAUI本体が肥大化するのでそこは抱え込まないようにしたんだろうな、と思っています。絶妙なアーキテクチャのバランスだなー、と感心しています。

learn.microsoft.com

Java Swing では

比較対象の Java Swing の良かった点は、プラットフォーム固有の部分は抽象化し、標準のAPIを豊富に整備されていました。ランタイム実装側でJNIなどネイティブのAPIコールを駆使して踏ん張っていました。 アプリケーションのコードを書く側はプラットフォームが何であるかは意識する必要はありませんでした。そのかわり、APIやランタイム実装の仕様は最大公約数的な設計なので、OS固有の機能はあまり提供されていませんでした。(マルチプラットフォームの対応をするということの宿命ですね…)

Construct SDK(s)

MAUI の思想としては各プラットフォームのネイティブコード を出力する形をとっています。

今回の記事を書くにあたりMacではこのような構成を構築しました。(macOS, iOS, Androidビルドを想定)

  • macOS Montery
    • .NET 7.0
    • Visual Studio for Mac 17.4.1
    • Xcode 14.1
      • iOS Simulator
    • Android SDK
      • Android Simulator

.NET SDK に加えてビルド対象のプラットフォームのネイティブなSDKの構築も必要になります。

この考え方はある種の正解なんだろうなー、と思っています。 .NET Runtime (JITコンパイル)といった実装だとiOSではレギュレーション的に不可能であったり、PCほどスペックが高くないバッテリー駆動のモバイル端末ではより軽量に動かしたいという側面もあるのだろうと思います。

マルチプラットフォーム対応のフレームワーク開発、というのは大変

短時間ながらではありますが、MAUI は色々考えられているなー、と改めて思い知らされました。 とある環境向けでフルスクラッチ(提供されている標準APIのみ)でマルチプラットフォームのライブラリやフレームワークを作った経験があるので、何となくでもその大変さが伝わってくる気がしています。

おわりに

冒頭で書いた通り、ほぼ個人的なポエムでした。最後まで読んでいただきありがとうございます。 アドベントカレンダーで2年連続でMAUIを書いているので、1年後にまた記事を書くかもしれません。

趣味のコード書きなどGUI系はMAUIをメインにしていきたいのでスキマ時間で色々触っていければと思います。

宣伝

弊社ではUnityエンジニアを募集しています。興味がある方は是非以下のページを覗いてみてください

twitter.com

herp.careers

Rails上で利用できるKarafkaを使ってKafkaを利用する

はじめに

エンジニアの松原です。前回の記事では、簡単にKafkaをローカルでサーバーを建ててHello Worldをする内容を紹介しました。

synamon.hatenablog.com

前回はConsumer側にKafkaJSを利用していましたが、今回は、Karafka を利用して、Rails上からConsumerとして振舞わせることを記事にしました。今回の記事で登場したコードはGitHubにリンクを置いていますので、参考になれば幸いです。
※今回使用しているKarafkaはLGPL v3.0のライセンスのものを利用しているため、利用の際はご注意ください。

github.com

Karafkaをインストールする

Karafka自体はRails上で動いていることを想定しているため、Railsのアプリケーションを作成していない場合はbundlerを通してrailsをインストールしておく必要があります。

Railsが入っていない場合はインストールする

既にRailsアプリケーションがある場合はこの作業は不要です。以下はRubyが入っていることを前提に書いています。

# Gemfileを追加
bundle init
# Railsをインストール
bundle add rails
# Railsアプリケーションを追加(kafka-consumer-railsディレクトリ以下にソースが追加される)
bundle exec rails new -G -M --skip-active-storage -C -A -J --api kafka-consumer-rails
# カレントディレクトリをkafka-consumer-railsに移動
cd kafka-consumer-rails

Karafkaをインストールする

railsが追加されたら、次にKarafkaをインストールします。

bundle add karafka
bundle exec karafka install

karafkaをインストールすると、以下の3つファイルが追加されます。

karafka.rb
app/consumers/application_consumer.rb
app/consumers/example_consumer.rb

設定を書き換える(karafka.rb)

karafka.rb の中身は以下のようになっています。(途中省略しています)

# frozen_string_literal: true

class KarafkaApp < Karafka::App
  setup do |config|
    config.kafka = { 'bootstrap.servers': '127.0.0.1:9092' } # <-- ブートストラップサーバーの設定
    config.client_id = 'example_app' # <-- KafkaのクライアントアプリケーションのID設定
    # ...
    config.consumer_persistence = !Rails.env.development?
  end
  
  (--中略--)

  routes.draw do
    # ...
    topic :example do  # <-- トピック名
      consumer ExampleConsumer
    end
  end
end

GitHubのサンプルではDocker内で動かしているため、設定を以下のように変更していますが、他の環境ではその都度変わると思いますので、環境に合わせて設定してください。

    config.kafka = { 'bootstrap.servers': 'host.docker.internal:29092' }
    config.client_id = 'my-app'

GitHubのサンプルではトピック名は sample のままにしています。

コードを一部書き換える(example_consumer.rb)

トピックを受け取って処理する内容を記述しているのは example_consumer.rb になります。

# frozen_string_literal: true

# Example consumer that prints messages payloads
class ExampleConsumer < ApplicationConsumer
  def consume
    messages.each { |message| puts message.payload }
  end

  # ...
end

このままだとうまくメッセージが受け取れなかったので、GitHubのサンプルでは以下のように変更しました。ここのmessageはKarafkaで定義されているオブジェクト型なので、このページからKarafkaの設計を詳しく調べておく必要がありそうです。

  def consume
    messages.each { |message| puts message.raw_payload }
  end

Karafkaを直接実行する

Karafkaはbundlerに依存関係の解決を任せているため、必ず bundle exec のコマンドを使って実行する必要があります。

bundle exec karafka server

仮想環境から実行する

GitHubのサンプルを実行する場合は、プロジェクト直下のディレクトリから、docker-composeを利用して以下のコマンドでビルド、実行します。

# ビルド
docker-compose -f zk-single-kafka-single.yml -f docker-compose-rails.yml build
# 起動(grep使って標準入出力の表示をフィルタする)
docker-compose -f zk-single-kafka-single.yml -f docker-compose-rails.yml up | grep kafka-consumer-rails

実行結果

前回の記事のProducerを別に動作させています。(トピック名は example に変更しています)

とりあえずは動いたというイメージです。
仮想環境上だと詰まったような動きをするのですが、どこかで設定間違っている可能性があるので、Karafkaの設定を見直す必要ありそうです。

おわりに

前回に引き続きKafkaを取り上げました。今回はRails上で利用できるKarafkaを利用してConsumerとして動かしてみました。
ProducerやConsumerに関する記事はJavaを利用したものが多いので、今後Javaを触ってみるのもよさそうです。

Rustで始めるOpenGLを読んでRustに入門した

はじめに

こんにちは、エンジニアのうぃすきー(@whisky_shusuky)です。

本記事は Synamon Advent Calendar 2022 の14日目の記事になります。

adventar.org

RustはWeb関連でもそこそこ使われており興味があったのですが難しそうな印象が有ったため手をつけられていませんでした。 そんな中Rustで始めるOpenGLという本に出会いました。 個人的に趣味でWebGLでGLSLを書いたことがあったため、Rustで動かしてみれば理解がとっつきやすいのではないかと思い読んで動かしてみました。

成果物

先に成果物を見せるとこのようなものができました。 本書の内容を実行すれば立方体をOpenGLで表示することができます。 そこに理解のために少し手を加えて画面上でx,y,z軸方向に立方体を回転できるようにしました。

コードはこちらに置いておきました。 github.com

Rust,sdl2をインストールしてcargo run で動作します。(macでのみ動作確認しています)

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
brew install sdl2
cargo run

手を加えた回転部分はRust側で回転行列を定義してシェーダーに渡しています。 rotation_x,rotation_y,rotation_zがそれぞれウィンドウ上で操作できる値になっておりそれらを増減することで回転します。

            let mut model_matrix = Matrix4::from_angle_x(cgmath::Rad(rotation_x));
            model_matrix = model_matrix * Matrix4::from_angle_y(cgmath::Rad(rotation_y));
            model_matrix = model_matrix * Matrix4::from_angle_z(cgmath::Rad(rotation_z));

Rust独特だと思ったのは単純にletだけで変数を定義しただけでは変数の再代入ができないことでした(それは定数では...?)

初期化の際にlet mutと宣言することで再代入できるようになります。

読んだ感想

個人でGLSLを書いていたときはシェーダー上で回転行列を直接定義していました。Rustで定義した値をシェーダー側に渡せるので色々面白いことができそうな気がしています。シェーダーエディタのデスクトップアプリとか作れそうです。

恐る恐る触ってみたRustですがなんとか動くものができました。Webに関わっていないので今度はAPIでも試しに作ってみたいです。

UnityのUI Elements (UI Toolkit)を触ってみた

概要

UnityでEditor WindowをUI Elementsで試してみました。 従来のIMGUIだとUIの配置などもスクリプトでやっていると地味に時間が取られてしまうものでした。 EditorのUIはある程度楽に実装したいですよね。

EditorWindowのUI Elementsのセットアップ

  • EditorWindowの場合、一式をメニューから生成します
  • 名称を設定しConfirm
  • 生成されるファイルは以下の通り
  • 「Window」→「UI Toolkit」→「名称」でサンプルが表示されます。

    • 楽です。

UI Elements一連ファイルについて

スクリプト(cs)

uxmlのロードと、各VisualElement(要素)のイベント処理を実装します。
最低限以下を記述すれば画面が生成されます。

    public void CreateGUI()
    {
        // Each editor window contains a root VisualElement object
        VisualElement root = rootVisualElement;

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/UIElements/Blog/Blog.uxml");
        root.Add(visualTree.Instantiate());
    }

対象の要素をQuery(Q)で名前を指定して取得。各プロパティ・イベントを設定する流れが基本となりそうです。

var textField = root.Q<TextField>("name"); // 要素取得
textField.value = "value"; // 値変更
textField.AddToClassList("class"); // スタイルのクラス追加
textField.RegisterValueChangedCallback(evt => {~~~}); // value change イベントアクション
textField.RegisterCallback<MouseOverEvent>(evt => {~~~}); // mouse over イベントアクション

UI Document(uxml)

xmlで要素を定義していきます。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi=~~~ >
    <Style src="project://database/Assets/UIElements/Blog/Blog.uss?fileID=7433441132597879392&amp;guid=20c827d3be187c146b576d843f7d1a60&amp;type=3#Blog" />
    <ui:GroupBox name="Toolbar" ~~~>
        <uie:ToolbarMenu display-tooltip-when-elided="true" text="メニューアクション" name="ToolbarMenu" />
        <uie:ToolbarToggle focusable="false" label="トグル" name="ToolbarToggle" text="無効" />
    </ui:GroupBox></ui:UXML>

がGUIのデザインツールのUI Builderがあるのでこちらが楽なので基本となりそうです。

スタイルシート(uss)

webのcssとほぼほぼ同じように記述できます。
セレクタに対してプロパティを定義していきます。

Label.pagetitle {
   color: red;
}

セレクタや、プロパティについては公式参照です docs.unity3d.com
また、ussより直接設定したプロパティ値の方が優先されます。これもhtmlと同様ですね。
楽ですね。

各種Component

LabelやTextField等シンプルなものはスキップして個人的に使いそうなものをピックアップしました。

ToolbarMenu


こちらはEditor専用のコンポーネントでUI Builderで有効にすることで利用可能になります。
HierarchyのTopのinspecterから設定します。

スクリプトでアイテムを追加し、クリックイベントを実装します

        // Menu
        var menu = root.Q<ToolbarMenu>("ToolbarMenu");
        menu.menu.AppendAction("アクション", action =>
        {
            if (EditorUtility.DisplayDialog("メニューアクション", action.name, "OK"))
            {
                Debug.Log("Click");
            }
        });
        menu.menu.AppendSeparator("------------");
        menu.menu.AppendAction("アクション2", action => {});

ToolbarToggle


こちらもEditor専用でメニューのトグルになります。

        var toggle = root.Q<ToolbarToggle>("ToolbarToggle");
        toggle.RegisterValueChangedCallback(evt =>
        {
            toggle.text = (evt.newValue) ? "有効" : "無効";
        });

ScrollView


こちらはスクロール付きのリストビューとなります。
シーンのオブジェクトを取得しLabelとして追加、表示してみました。
また、列をマウスオーバー時にクラスを切り替えることで背景色を変化させたりします。

        var scene = EditorSceneManager.GetActiveScene();
        var objects = scene.GetRootGameObjects();
        var menu = root.Q<ScrollView>("SceneList");

        foreach (var obj in objects)
        {
            var label = new Label(obj.name);
            label.RegisterCallback<MouseOverEvent>(evt =>
            {
                label.AddToClassList("MouseOver");
            });
            label.RegisterCallback<MouseLeaveEvent>(evt =>
            {
                label.RemoveFromClassList("MouseOver");
            });
            menu.Add(label);
        }

css

.MouseOver {
    background-color: darkblue;
}

Foldout


TreeViewを実現したく、Foldoutというコンポーネントが近かったのでこれで再現してみました。
合わせてDragAndDropも試してみました。

        // left
        var leftTree = root.Q<Foldout>("LeftFoldout");
        leftTree.text = "root";

        var child = new Foldout();
        child.AddToClassList("TreeFirst");
        child.text = "child";
        leftTree.Add(child);

        var child2 = new Foldout();
        child2.AddToClassList("Tree");
        child2.text = "child2";
        child.Add(child2);

        var child3 = new Foldout();
        child3.AddToClassList("Tree");
        child3.text = "child3";
        child.Add(child3);

        var child3a = new Label();
        child3a.AddToClassList("Tree");
        child3a.text = "childA";
        child3.Add(child3a);

        child3.RegisterCallback<MouseDownEvent>(evt =>
        {
            DragAndDrop.PrepareStartDrag();
            DragAndDrop.StartDrag("Dragging");
            DragAndDrop.SetGenericData("treedata", child3);
        });

        // right
        var rightTree = root.Q<Foldout>("RightFoldout");
        rightTree.text = "root";

        var rightChild = new Foldout();
        rightChild.AddToClassList("TreeFirst");
        rightChild.text = "ここにドロップ";
        rightTree.Add(rightChild);
        rightChild.RegisterCallback<DragUpdatedEvent>(evt =>
        {
            object draggedLabel = DragAndDrop.GetGenericData("treedata");
            if (draggedLabel != null)
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Move;
            }

        });
        rightChild.RegisterCallback<DragExitedEvent>(evt =>
        {
            var draggedLabel = (Label) DragAndDrop.GetGenericData("treedata");
            if (draggedLabel == null)
            {
                return;
            }

            var newchild = new Label();
            newchild.AddToClassList("Tree");
            newchild.text = draggedLabel.text;
            rightChild.Add(newchild);
        });

css

.Tree {
    padding-left: 0px;
}
.TreeFirst {
    padding-left: 15px;
}

Foldout要素に追加していくことで階層構造が構築できます。
見栄え的には縦に並んでいくだけだったので、スタイルシートでpaddingでそれっぽく調整しています。

ドラッグアンドドロップですが、Editorの場合UnityEditor.DragAndDropが利用できるので簡単なものならこれで実現できそうです。
ただ凝ったものや、ランタイムの場合は別の方法で実現するようです。
docs.unity.cn

また、Foldout要素自体にMouseDownEventイベントを登録してもうまく動作しませんでした。ツリーごとドラッグしたい場合は、別の手段で実現する必要がありそうです。

ちなみに要素の「userData」プロパティに任意のobjectを入れておくことでDrop時のカスタマイズも色々行えそうです。
また、DragAndDrop.objectReferencesにオブジェクトをセットしてHierarchyにドラッグすると、オブジェクトを配置
DragAndDrop.pathsにパスを入れてProjectにドラッグするとpathsのアセットをコピーしたりできます。


こちらはシンプルにDropdownメニューです。
スクリプトからアイテムを追加して選択時のアクションを設定します。

        var dropdown = root.Q<DropdownField>("Dropdown");
        dropdown.choices.Clear();
        dropdown.choices.Add("アイテム1");
        dropdown.choices.Add("アイテム2");
        dropdown.choices.Add("アイテム3");
        dropdown.choices.Add("アイテム4");

        dropdown.index = 0;

        dropdown.RegisterValueChangedCallback(evt =>
        {
            if (EditorUtility.DisplayDialog("ドロップダウン", evt.newValue, "OK"))
            {
                Debug.Log("Click");
            }
        });

RadioButtonGroup


こちらもシンプルにRadioボタンです。
スクリプトから追加します。

        List<string> radioButtons = new List<string>();
        radioButtons.Add("radio1");
        radioButtons.Add("radio2");
        var radiogroup = root.Q<RadioButtonGroup>("RadioGroup");
        radiogroup.choices = radioButtons;
        radiogroup.RegisterValueChangedCallback(evt =>
        {
            Debug.Log(evt.newValue);
        });

なおアイテムはstringだけ指定可能なようなので、文字以外で表現する場合は通常のRadio Button等でカスタマイズが必要そうです。

Progress bar


プログレスバー(とついでにスライダ)です。
値を設定するだけです。シンプルです。
なお下限値・上限値はプロパティから設定することができます

        var progress = root.Q<ProgressBar>("Progress");
        var slider = root.Q<Slider>("ProgressSlider");

        progress.value = slider.value;
        progress.title = $"{progress.value}%";
        slider.RegisterValueChangedCallback(evt =>
        {
            progress.value = evt.newValue;
            progress.title = $"{progress.value}%";
        });

レイアウトについて少し

基本的にはwindowのサイズに応じてフレキシブルに配置したく試していたのですが、
特に横並びの均等配置がcssになれてないとわかりづらかったので残しておきます。

GroupBox


要素をグループ化します。VisualElement等でも良いのですが、これを例に横並び配置します。
Positionはデフォルトのまま相対配置の自動サイズ

FlexのDirectionを横並びを指定。

alignでクロス軸方向の揃え位置を指定します。(MDN クロス軸)


次に並べる子要素側のFlexのShink/Growに1を指定します。
これで要素の表示領域の割合を決めます。

flex shink/growについてはcssの説明がわかりやすいです。MDN flex-grow
また要素が増減しない単純な場合はwidthで「%」指定でも良いです。
こういう設定をスタイルシートにまとめておくと良さそうですね。

最後に

やはりViewの分離とスタイルシートでの制御は簡潔で楽になったと思います。
ツール作成する際はわかりやすさも大事かと思うので、積極的に利用していきたいです。

またScreenSpaceのみですが、ランタイムでも利用できるそうです。
ただレスポンシブやゲームUI等細かい表現を実現しようとすると標準機能だけだとまだまだ難しそうかな(?)という印象です。
今回はEditorWindowを試しましたがもちろんカスタムインスペクターもUI Elementで対応できます。
カスタムインスペクターの作成