なるようになるかも

力は多くの場合、その人の思いを超えない。

Android Kotlin のアサーションは Truth が良いのでは?

Android テスト全書を読んでたら、「Kotlin でアサーションを書くなら AssertJ」って感じだったので。

確かに AssertJ は堅実で、現実的な選択肢だと思うんですけれど、ちょっと気になったきっかけが P42 の AssertJ Android のコラムで、「サポートライブラリの進化への追従が難しかったから廃止された」とだけ書いてあって、もうひとつの理由については触れていないこと。

Additionally, we no longer think AssertJ's model for supporting alternate assertions is a good practice.

We recommend using Truth which has vastly superior extensibility model which, when coupled with things like Kotlin's apply method create a really nice assertion experience.

引用元: square/assertj-android

超ざっくりですが、「もはや AssertJ が代替アサーションを提供する良い手段だとは考えていない」「より優れた拡張を持ち、Kotlin との親和性が優れたアサーション体験をもたらす Truth を推奨する」みたいなことが書いてあります。

これを書いたのは Google 入社直後の Jake 神なので、Google 製の Truth を推しただけかもという可能性はありますが、かなりベタ褒めされてます。

で、Truth って何?

google truth」という単語の組み合わせでググっても、「グーグルの検閲によって真実が歪められている!」みたいなのが引っかかって、全然ライブラリの感想が出てこなくてぐったりしますが、Google の Guava 開発チームが作ってるアサーションライブラリです。

google.github.io

Android テストを書く上で Truth を採用するメリットは?

AndroidX Test Library が Truth 向けの拡張を用意している、というのが大きなメリットかなと思います。

AssertJ Android が突然の死を迎えたように、サードパーティの便利なライブラリは、依存しすぎると将来的にどうしようもない負債になるリスクがあるので、公式でサポートされている、というのは安心要素です。

とはいえ、現時点では対応しているコンポーネントはあまりないのですが、AndroidX Test Library はオープンソースなので、そのうちコントリビュートが殺到して充実するかもしれないし、そうでないかもしれないです。

どんな感じの書き方なの?

AssertJ と大体同じです。

大きな違いとしては、AssertJ だと method chain で複数の検証を連結させることができましたが、Truth ではこの方法を避けています。

Java だとやや冗長になるのですが、Kotlin には apply があるのですっきり書けます。

assertThat(actualIntent).apply {
    hasComponentClass(UserPageActivity::class.java)
    hasFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
assertThat(actualIntent.extras).apply {
    isNotNull()
    string("KEY_URL").matches("https://example.com/user/[a-z]{3,12}/")
}

拡張性は良いの?

AssertJ Android と AndroidX Test Library の実装を比較してみた感じ、より簡単に作れそうな感じはします。

IntentFlag 周りの実装が端折ってるので、出力されるエラーメッセージ自体は、AssertJ Android の方が親切ですね。

まだ安定版がないので導入は早いのでは?

現在のバージョンは 0.42 で、確かに安定版ではありません。

1.0 のリリースはいまは 2019 年の秋を予定しているらしいです。

こう聞くと AssertJ の後継として作られた、開発が活発な新しいライブラリなのかな? と思うところですが、そうではありません。

AssertJ とよく似た API をしているのは、AssertJ の fork 元である FEST に触発されて開発されたものだから……という理由らしく、2011 年から細々と存在しているみたいです。(AssertJ は 1.0 が 2013 年にリリースされたのですが、それより前からあるらしい)

なお、散発的にアップデートしていますが、お世辞にも活発とは言いがたく、1.0 が本当に 2019 年に出るのか怪しいと思ってます。

もっとも、1.0 に向けた開発のフォーカスは、主に拡張を作る人向けの部分らしいので、アサーション部分に破壊的変更が入る可能性はそれほど高くないのでは、と思います。

正直どっちでも大して変わらないのでは?

それはそうなんだけど人柱を増やしていきたい。

RxSwift の実装例っていろいろありすぎて困るよねって話。

元ネタ

感想

他人のコードを読むとき、どういうフレームワークや言語でも、「この機能をこんな使い方するのか……マジかよ……」というのはいい意味でも悪い意味でもあると思うのですが(悪い意味の方が7割くらい)、Rx 関連になるとみんな実装方法がまちまちで、もはや何を参考にしていいのか分からない状態になりますよね。

RxSwift 界隈と RxJava 界隈とでも空気感が全然違ったりしますし。

個人的な考えとして、Rx + MVVM を独学で学ぶならば、習熟段階に応じて、

  1. BehaviorRelay を入れ物にして、PublishSubject でイベントを発火する似非 Rx で考え方に慣れる
  2. 副作用(状態)を減らすことを腐心し、関数指向を目指しつつ、いろいろ妥協もして動くものを作る
  3. 理想的な Rx + MVVM アーキテクチャに到達する(自分はしてない)

と進んでいくのが無難だと思っていて、元記事はサンプルコードの一部の「そもそも Swift としてどーなの?」っていう突っ込みポイントを除けば、「入門者への記事」としては、そこまで悪いとも思わなかったり。

Rx 全然分からない人に向けて、挫折しにくい、自分の書いたコードと動作がわかりやすく対応付くような、「最初の一歩を踏み出させるサンプルコード」を作成するのって、なかなかハードルの高いタスクだと思うんですよね。

私的な実装例

なんとなく自分も実装してみたり。とはいえ、おおよその実装は前述した記事のコードをパクった真似た上に、テストコードはないのですが……。(あとで書いた)

github.com

ViewModel の作り方だけは、わりと変えてます。

ViewModel を作るときに、「依存モジュール」「入力」「出力」がごっちゃにならないように、こういうプロトコルに準拠させてます。

protocol ViewModelType {
    associatedtype Dependency
    associatedtype Input
    associatedtype Output
    
    init(_ dependency: Dependency)
    
    func transform(_ input: Input) -> Output
}

イニシャライザで受け取るのは依存するモジュールだけにして、入力の Observable<T> を受け取り、出力の Driver<T> を返す transform() というメソッドを用意してます。ViewModel のコード全体はこちら

この手法、すっきりしていてわたしが私的にコードを書くときには好んで使うのですが、ものすごい複雑なケースだと破綻するっぽい?

記事のレベルと共感

以下、あんまり関係ない余談。

某ポエムサイトでは「エンジニアは全員記事を書くべき」「ゴミ記事で汚染するべきではない」みたいな論争をやってるっぽいですね。

わたしがプログラミングの記事を書いているのは、自身が初学者だったときにいろいろな記事にお世話になった恩返しをしたい……というのが動機としてあったりしました。

そうした頃に読んだ記事は、片っ端からブックマークに入れていて、今でもたまに読み返しているのですが、

  • この実装よりももっといい方法があるなー
  • なんでこんなことで悩んでいたんだろう?
  • 当時は理解できなかったけれど、こういうことだったのかー
  • まだ難しくて読めない……

みたいに、自身に知識に応じて内容の理解度が変わって、新しい発見があって面白いです。

私見として、高度で正しい技術記事だけがこの世にあるよりも、間違ったり迷走したりしてるような記事も含めて、玉石混合の記事がある方が良いと思っています。

というのも、記事への共感というか、理解しやすさというのは、読み手と書き手のレベルが最接近しているときが最大になるという体感があって、中級者以上の人にとってはどうでもいいような悩みやトラブルの記事もきっとどこかの誰かの役に立つし、逆に「いいね」がまったく付かない内部実装について詳述したようなマニアックな記事もどこかの誰かにとっては最高の読み物になると信仰してます。

なので、明確な誤りに対する指摘は有益だと思いますが、「このやり方が正しい」という押し付けについてはあまりしたくはないんだけど、コードレビューをお仕事でしてるとそれに近いことをやってしまうことがあって、なかなか難しい……。

最近のアプリ界隈での「設計」の違和感

アプリ界隈で「設計」の話をするときに MVC / MVP / MVVM のような「設計パターン」だけが語られるようになった気がする。

往々にして、「アプリの規模によってどれを採択すべきかは変わる」みたいなお茶を濁すような結論で終わることが多い。

私的な結論

  • 「設計」と、「設計パターン」は別物だと思う。
  • 「設計」のレベルを上げたい。
  • アーキテクチャシンドロームから抜け出して、価値のあるものを作りたい。

以下、思うところのメモ。

MVC は古い / 劣ったやり方か?

MVC は Model をどう構築するかについてとくに規定していない。

MVC への批判をするときに、FatVC が持ち出されることが多いのですが、FatVC を実装してしまうのは単に実装者の能力不足だと考えていて、MVVM を採用しても FatVM を作るだけだと思っている。

また、比較的新しめの Flux アーキテクチャは、良く言えば MVC を詳細化したもの、悪く言えば MVC の再発見とみなすことができる。

VC / Activity が GOD クラスであることから目を背けるのはよくない

VC や Activity をクリーンにすることは重要なものの、そこに熱中しすぎるのはよくないと考えている。

VC や Activity は単一の View における状態の管理と、それらの View 同士の遷移の状態の管理を担っているため非常にやれることが多く、また、管理している状態も極めて複雑である。

これを上手く抽象化してパターンに当てはめるという試みが多数行われているものの、基本的にはトレードオフであり、正解はない。

もともと単体の VC や Activity で簡単にできたことがとてもとても複雑になってしまうというのはまだマシなほうで、インタラクティブ画面遷移のような「View の状態」と「遷移の状態」の両方を扱う操作は欠落してしまうようなケースもある。

GOD クラスの影響を低減させる試み自体は概ね好ましいものの、そこで消耗してもアプリの価値が劇的に向上することはないので、ほどほどに割り切って上手く付き合っていくのが良いと思っている。

DDD や Clean Architecture を設計パターンと同列に語る

MV* は実装フェーズ、それも GUI 実装のみに限定されたパターンの話でしかないはず。

対して、DDD はソフトウェア製造全体に対する巨視的な考え方をしているものだと思うし、Clean Architecture はアーキテクチャを構築する上での指針というか、設計パターンに対するメタな話だと解釈してるのですけれど、なんか同列で語られることが多いのが個人的にはもにょい。

あと DDD や Clean Architecture を独自解釈(というか、ネットの情報ベースで語っている?)ことについて思うところがないわけではないのですが、まだ原著完全に読めてないのでとりあえずノーコメント。

MVVM without DataBinding

まともなデータバインディング機構がないにも関わらず、MVVM が採用されるパターンが多い。自分もやる。

必ずしも悪いわけではないものの、以下のような副作用は散見される。

  • 双方向バインディングの記述がひどくぎこちない書き方になる
  • 一行のデータ更新によってリストを全て書き換えるような、大富豪な処理が平然と実行される
  • ひとつのボタン UI に対して、「状態に応じたボタンのラベル」「ボタンを実行するかどうか」「ボタンで実行する処理」のような複数のバインディングが必要になる
  • コンパイル・実行時エラーが不親切でデバッグ作業が非効率化する
  • データバインディングの作法が統一されていないので、オレオレ VM が横行する

個人的に「設計」をやる人に望むこと

要件に対する興味と理解

「設計」をやるときに GUI をどういうパターンで組むのか?から入るのは、ぶっちゃけよほど簡単で先の見通しがはっきりしている状況でもない限りは、悪手だと思う。

どういう要件があり、どういった機能を実現する必要があるのか?にまず興味を向けるべきで、ビジネスロジックドメイン層と言われる領域をうまく抽象化し、UI や DB などと隔離されたかたちで実装できるか?というところがもっとも肝要で、それこそが本来やるべき「設計」じゃないのかなぁと思っている。

プログラミングの原則の理解

設計パターンやアンチパターンの根底には大体プログラミングの原則の考え方が根付いている。

「BaseVC はアンチパターンだからダメ」みたいなのは単なる思考停止なのであまり好きではない。

たとえば「開放閉鎖原則の観点から、この実装では変更に対する柔軟性を失っており好ましくない BaseVC である」という指摘が入ると、具体的な問題箇所が明らかになり、どうやって修正するか?という建設的な議論に入ることができる。

設計パターンにはとりあえず手を出す

最終的に GUI を作る以上は MVC や MVVM や MVP などの GUI の設計パターンを採用することになる。

このとき、人や本を権威にして「MVC はダメ」「MVVM は実はこんなデメリットが」みたいに流されるだけではなく、自分で手を動かして実装するなり、コードリーディングはしなければならないと考えている。

というのも、作ったことも触ったこともないものに対して、どれが最適かなどという判断を下すことなど不可能なので。

ひとつの設計パターンに成功体験を持ってしまうと、その設計パターンがあらゆる状況で通用するものだと勘違いしてしまいがちだったり、その逆にも注意をする必要があると思う。

UISearchController を Rx で書けるように

iOS11 だと UISearchControllerUINavigationBar に入れられて楽しいので、入力した文字列をうまい感じで Observable<String> にしてくれる DelegateProxy を作ってみた。

UISearchResultUpdating+Rx の私的実装

なんで RxCocoa にないの?

PR 自体は何度か出てたけど蹴られてたっぽい。

./scripts/all-tests.sh 通してないなこの PR …。

ちゃんと RxSwift の作法っぽい感じで書き直したらこんな感じです。

これはちゃんと ./scripts/all-tests.sh も通ります。

よほどメジャーな delegate じゃない限り、この手のラッパーは各自で作ってねってスタンスだと思うので本家に PR は出さないですが。

UISearchController の使い方が分からない場合

ググってコードを探すよりも、Table Search with UISearchController というサンプルコードを見ると早いです。

なんと、Swift 版があるだけでなく、iOS11 対応済。

hidesSearchBarWhenScrolling が動作しない

UIScrollView と連動して動作してくれる条件は、以下の通り。

  • UIViewController の view が UIScrollView
  • UIViewController の view の subviews[0] が UIScrollView

Shrink large title when scrolling (not UITableViewController) iOS 11 に図解入りの超分かりやすい説明があります。

作った経緯とか

最初、UISearchResultsUpdating に準拠しなくても、searchController.searchBar.rx.value を subscribe すればいいんじゃ?と思ってたんですけれど、これだとうまくいかなかった。

  • キャンセル時に next が流れて来ない
  • 検索中に別の VC に push すると、ナビゲーションバーが消え去る

後者について、UISearchController が内部的に modal を出してるっぽいので、これを消すワークグラウンドとかを思いついたのですけれど、正規の方法で実装した方がよさそうだなぁということで素直に実装しましたとさ。

ついでだったので、SearchBar に入力したテキストを取れる Observable を追加したのですけれど、ちゃんと動いてるっぽい。

var queryText: Observable<String> {
    return Observable.merge(Observable.just(""),
                            RxSearchResultUpdatingProxy.proxy(for: base).didUpdateSearchResultSubject.map { $0.searchBar.text ?? "" })
        .distinctUntilChanged()
}

空文字と merge しているのは、combineLatest でフィルタリングしている実装の都合上です。

Activity Tracing どう使うのか謎だったので調べたメモ。

static void hoge() {
    static os_log_t log;
    static os_activity_t activity;
    static os_activity_t activity2;
    static os_activity_t activity3;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        log = os_log_create("activity", "a butterfly's dream");
        activity = os_activity_create("dream", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT);
        activity2 = os_activity_create("real", activity, OS_ACTIVITY_FLAG_DEFAULT);
        activity3 = os_activity_create("dream", activity2, OS_ACTIVITY_FLAG_DEFAULT);
    });
    
    struct os_activity_scope_state_s state = {};
    os_log_info(log, "Graveyard Memory");
    os_activity_scope_enter(activity, &state);
    os_log_info(log, "Nightmare Counselor");
    struct os_activity_scope_state_s state2 = {};
    os_activity_scope_enter(activity2, &state2);
    os_log_info(log, "Historical Vacation");
    struct os_activity_scope_state_s state3 = {};
    os_activity_scope_enter(activity3, &state3);
    os_log_error(log, "Cosmic Horoscope");
    os_activity_scope_leave(&state3);
    os_activity_scope_leave(&state2);
    os_activity_scope_leave(&state);
}

os_activity_scope_state_s は空の構造体を作って os_activity_scope_enter にポインタを渡せばいいだけなのね。

f:id:quesera2:20180429190527p:plain

こんな感じで階層表示される。

block でネストしても良いのであれば、os_activity_initiate マクロだと、Activity の作成+スコープ開始を同時にやってくれるので便利なのだけれど、Activity Tracing のリファレンス には載っていない…。

os_activity_initiate("dream", OS_ACTIVITY_FLAG_DEFAULT, ^{
    os_log_info(log, "Northeast Nostalgia");
    os_activity_initiate("real", OS_ACTIVITY_FLAG_DEFAULT, ^{
        os_log_info(log, "Forgotten Paradise");
        os_activity_initiate("dream", OS_ACTIVITY_FLAG_DEFAULT, ^{
            os_log_error(log, "Shining Future");
        });
    });
});

OSLog が記録しているファイル名や行数の情報を知りたい

Don’t include symbolication information or source file line numbers in messages. The system automatically captures this information.

「OSLog は自動的にソースファイル名と行数を収集するから、そういった情報をメッセージに含めるべきではない」というリファレンスの記述に対して、じゃあどうやって見ればいいの?っていう疑問が当然あると思うんですけれど、何も書いてないのですよね…。

Developer Forum を調べたところ、os_log source file and line numbers に記載を発見。

log コマンドを --source オプションを付けて実行したときに見れるっぽい。DevForum だと Swift だと動作しないという話があったけれど、今はちゃんと Swift コードでも行数が出るっぽい。

関数でラップすると、出る行数はラッパーになるので、やはり os_log を直接呼ぶしかないっぽい。@inline(__always) を使ってもダメだった。

$ log stream --level debug --source --predicate 'subsystem == "activity"'
2018-04-29 21:34:40.264429+0900 0x28b38e   Info        0x0                  31286  <Activity`hoge (hogehoge.c:28)> [activity:a butterfly's dream] Graveyard Memory
2018-04-29 21:34:40.264499+0900 0x28b38e   Info        0x786a3              31286  <Activity`hoge (hogehoge.c:30)> [activity:a butterfly's dream] Nightmare Counselor
2018-04-29 21:34:40.264540+0900 0x28b38e   Info        0x786a4              31286  <Activity`hoge (hogehoge.c:33)> [activity:a butterfly's dream] Historical Vacation
2018-04-29 21:34:40.264599+0900 0x28b38e   Error       0x786a5              31286  <Activity`hoge (hogehoge.c:36)> [activity:a butterfly's dream] Cosmic Horoscope
2018-04-29 21:34:40.264848+0900 0x28b38e   Info        0x786a6              31286  <Activity`__hoge_block_invoke.3 (hogehoge.c:40)> [activity:a butterfly's dream] Northeast Nostalgia
2018-04-29 21:34:40.264918+0900 0x28b38e   Info        0x786a7              31286  <Activity`__hoge_block_invoke.6 (hogehoge.c:40)> [activity:a butterfly's dream] Forgotten Paradise
2018-04-29 21:34:40.264982+0900 0x28b38e   Error       0x786a8              31286  <Activity`__hoge_block_invoke.9 (hogehoge.c:40)> [activity:a butterfly's dream] Shining Future
2018-04-29 21:34:40.269216+0900 0x28b38e   Default     0x0                  31286  <Activity`main (main.swift:26)> [activity:a butterfly's dream] Dominated Realism

os_activity_initiate の方が圧倒的に簡潔に書けるのですけれど、block を開始した行数しか記録に残らないので、os_activity_scope_enter を使った方がいいのかもしれない。

OSLog にいい加減に移行するべきなの?

と思って調べた内容のメモ。

結論から言って、watchOS みたいなパフォーマンスにシビアなデバイスで、マルチスレッドが絡む部分をロギングしたいとかじゃない限り、普通のロガーでよさそう…。

OSLog の優位点

メモリ上の循環バッファにバイナリでログを保持しておき、何らかの障害が発生した時点で初めて外部に出力するので、観測者効果が最小限で、余計な I/O が発生せずログが容量を逼迫することがない。

Activity Tracking により、そのログがどういうコンテキストで出力されたのかを視覚的に表示することができる。

その他、機微情報の取り扱いなどが変わっているらしい。小容量な SSD がメインストレージで、紛失によるセキュリティリスクの高い、モバイル端末時代を考えたロガーとして設計されている感じがある。

OSLog の欠点

ログが単純なテキストファイルではないので、これを横断的に検索する手段が存在しない。閲覧に際しても、基本的には Sierra 以降のコンソール.app を使う必要がある。

肝心の Activity Tracking が長らく Swift に対応していない。

ログ収集時に文字列組み立てによる負荷を発生させないために、Obj-C ライクな書式文字列が存在して、それを覚える必要がある(その書式文字列の一覧がどこにあるのかは分からない…)。

OSLog の使い方でいまいち分かりにくかったところ

Xcode の Console に出る情報が少なすぎて使いにくい

コンソール.app を使うのがほぼ前提なところがある。

OSLog.default って使っていいの?

subsystem と category の情報をログに含める必要がないなら別に使っても問題ないらしい。

subsystem と category って何?

ログを識別するために設定できる任意の文字列。

  • subsystem は慣例的に Reverse domain name notation で実行しているアプリの識別子などを渡す。
  • category はその中でログメッセージを分類するのに使用する。

ビデオの中では、メッセージ本体とタイムスタンプに別の category を設定するという使い方が紹介されていた。

ログレベル良くわからない

ログレベル 意味
Default 循環バッファにログを出力し、メモリが溢れたとき圧縮して外部に保存する。追跡に使うデータは基本これ。
Info 循環バッファにログを出力する。エラーが発生しない限り、外部に書き出されず捨てられる。
Debug 設定を変えない限り、メモリには取り込まれない。開発用。
Error 常に外部に書き出される。プロセスレベルのエラーを報告するためのもの。
Fault 常に外部に書き出される。システムレベルおよびマルチプロセスのエラーを報告するためのもの。

とりあえず残しておいて損はない情報は Info にざくざく詰んでおくと、障害発生時にだけ書き出してくれる。

Error / Fault の違いは、Activity Tracking の振る舞いに起因しているっぽい?あんまり分かってない。

ラッパークラスやラッパー関数を作ってもいいの?

ラップするとどこでエラーが発生したのかという情報が失われるのでダメらしい。(参考にしたビデオの Transcript より)

Avoid wrapping os log APIs in other functions. If you wrap it in another function you then lose our ability to collect the file and line number for you.

これについて信憑性を調査しようと思ったものの、OSLog の実体のマクロを展開してもよくわからず。

参考にしたもの

Unified Logging and Activity Tracking

OSLog について知るならまずこのビデオを見ないと始まらない。(という風潮が最近多いけれど、ちゃんとリファレンスを充実させて欲しい…)

Logging: Using the os_log APIs

サンプルコード。コンソールでログを見る方法とかも書いてある。

Fix Bugs Faster using Activity Tracing

2014 年の WWDC の頃にあった Activity Tracing の話。

トレーシングは UIKit に統合されてて Target-Action で勝手に Activity を作るみたいな挙動だったはずなんだけど…。

Unified Logging and Activity Tracing

Unified Logging の話とか、Activity の Swift3 Wrapper の話とかがある。

iOS デザイン実装はつらい話のあとがきみたいなの。

Qiita に久々に記事を書いたのですけれど、なんか途中で力尽きていろいろツッコミどころが多い感じになってしまった。

qiita.com

わたしとしてはコメントがとても勉強になったので良かったのですけれど…。

以下、書ききれなかった内容の供養みたいなの。

画像リソース周りの話

  • ついにラスタライズされた偽ベクタ画像じゃなくて、本物のベクタ画像が使えるようになった
  • Slicing の機能はわりと便利だと思うのですけれど、あまり使われてるのを見ない…

みたいなことを書こうと思って忘れたらしい。

Named Color の話

iOS11 以降しか使えないので(R.swift とか SwiftGen とかでバックポートできると思いますが)、まだ本格普及しないと思うのですけれど、これの動作はわりと謎です。

単に Assets Catalog から UIColor を生成して KVC で設定してるだけかと思いきや、シミュレータで見ると View がロードされてから、実際に表示されるまでの間に色空間が別物になっていたりとか。iOS10 辺りで入ったカラーマネジメントとか Display P3 が関係しているのだと思いますが、あんま詳しくないので適当なことしか書けない…。

// viewDidLoad()
UIExtendedSRGBColorSpace 0.562 0.529 0.342 1
// viewDidAppear()
kCGColorSpaceModelRGB 0.562 0.529 0.342 1 

なので、Named Color で指定した場所は、viewDidLoad() で色を変えても反映されないですし、KVC で値を渡すだけの @IBInspectable で Named Color を指定しても (Missing) という表示になります。

Named Color の登場で割を食ったのが Color Literal で、実体が RGBA 値をコードにベタ書きしてるだけなので、Named Color としては使えません。(なのに、Media Library から Named Color をドラッグすると、Color Literal に化けてしまうのがよくない)

Attributed String の話

JLREQ の要求が高すぎて目眩がしつつ、せめてベタ組みと連続約物の処理くらいはちゃんとやりたいなと思いながら、どう実装すればいいのか悩んでいる今日この頃。

iOS でリッチテキストの描画をするときは、Attributed String を使うわけですが、System Font で日本語の1行だけの Attributed String を作ると、Paragraph Style に lineSpacing を指定したときに、高さ計算に失敗するというバグ?があります。

qiita.com

これはそもそも Attributed String に System Font を渡すのが、想定された使い方ではない…という感じなんでしょうか。実際、Storyboard 上では System Font の Attributed String は作れませんし。

普通のテキストには System Font を推奨しているわりに、Attributed な場合は System Font を使うとダメっぽいのが罠として優秀。

追記:もうちょっと調査した

UIFontDescriptorヒラギノを持つカスケードリストを作っても上記の問題が回避できないですね。

// これは擬似コードなんですけれど、こういう書き方をすると bese にない文字は .cascadeList で指定したものにフォールバックされる
let baseFontDescriptor = UIFontDescriptor(name: "HelveticaNeue-Medium", size: size)
let hiragino = UIFontDescriptor(name: "HiraginoSans-W3", size: size)
let font =  UIFont(descriptor: baseFontDescriptor.addingAttributes([.cascadeList: [hiragino]]), size: size)

System Font 固有の問題というわけではなく、iOS 内臓のヒラギノの抱える闇の一端っぽい感じ?UIFontDescriptor はめっちゃバギーな感じがするのでもっと使われてヤバさが広がってほしい…。

@IBDesignable の話

@IBDesignable って実際使われてるんでしょうか。分かってくると結構便利だと思うんですけれど、トラップがいろいろあってとっかかりが悪い感じ。

  • init(frame:) を実装する必要がある
    • UITextView の場合は、textContainer を引数に取るイニシャライザが必須
  • layer プロパティの操作や addSubLayer(_:)maskLayer など CALayer に対する各種操作はできる
    • class function の layerClass を変更しても反映されない
  • TARGET_INTERFACE_BUILDER で処理を分岐させると、その範囲はリファクタリングの対象外になったり、IB でのみビルドエラーになっていても気付かなかったりする
  • Storyboard 上で致命的なクラッシュが発生した(IUO の Outlet とかを触ったとか)は、~/library/Logs/DiagnosticReports/ にログが吐かれるので読めば分かる
  • Editor > Debug Selected Views を実行すると、当該のカスタム View のイニシャライザや draw(_:) が走って、breakpoint で止まる

アプリケーションを実行せずに View だけでデバッグできるのは良い機能だと思います。