Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

Swift's Strings

ガラパゴスのコードヒーヨアン(twitter: @luinily)です。

社内iOS勉強会の発表準備のため、Swift 4でStringの変更の説明を調べているうちに、そもそも現在のSwiftのStringはどうなっているのか、正確に把握していなかったことに気づいて、発表内容を現在のSwiftのStringの説明の方に変えました。

今回はその発表の内容を説明しようと思います。

Swiftの文字列はUnicodeの考え方に従っていますので、Swiftの文字列を理解するには、Unicodeを理解せねばなりません。

Unicode

アメリカで最初文字をパソコンで処理をしようとしたときは、まだメモリーも少なかったし、英語の使う文字だけで十分だったので1文字は1バイトで表現していました。そのあと、表現したい文字数が増えたり、国際化が進んで、各言語の文字を扱うためのエンコードができたりしていました。大学生の時、フランス語のWindows XPで日本語表示のインストールなどをした覚えがあります。ネットになると、複数のエンコードがありうるので、サイトによって全ての文字が化けて、表示に使うエンコードを変えたりしていました。

こういった問題を解決するため、すべての文字を表現できるエンコードUnicode、を作成することになりました。なぜUnicodeができるまでの歴史を簡単に説明したかというと、その歴史がUnicodeに影響を与えているからです。

Unicodeはどういうふうに文字をエンコードしているのか

Unicodeの文字はビットから文字まで複数の様子で定義されています:

  • Character

  • Unicode Scalar

  • Code Unit

一つの文字は一つか複数のUnicode Scalarで定義されています。一つのUnicode Scalarは一つか複数のCode Unitで定義されています。

f:id:glpgsinc:20170417115610p:plain

Code Unit

Code Unitはファイルなどに文字列を書き出すときに書かれる値です。複数のサイズが可能:8ビット(UTF-8)、16ビット(UTF-16)、32ビット(UTF-32)など。 複数のサイズが存在するのは、いろんな構築をしたシステムに対応できるためのと、英文など最初の8ビットで表現できていた文字列を使うと、1個の8ビットのCode Unitでかける文字がほとんど、メモリーの節約にもなります。

Unicode Scalar

Unicode Scalarは「文字」と「文字の変化様子」の2種類に分けられて、16進で表現される番号をふられている。例えば、日本語だと「す」という文字のUnicode Scalarは「3059」、「゙」でのUnicode Scalarは「3099」。二つ合わせれば、「ず」ができます。 つのUnicode ScalarをCode Unitで出力する場合、UTF-8では文字によって1つから4つのCode Unitが必要になります。

文字

一つの文字は一つか複数のUnicode Scalarの組み合わせでできています。Unicode Scalarの数には特に上限がありません。先の例で「す」と「゙」を組み合わせると「ず」になります。

同じ文字で複数の書き方が可能

一つの文字をUnicodeエンコードする場合、複数のやり方が存在することがあります。 例えば、さっきの「ず」は二つの Unicode Scalarを使ってできましたが、「ず」という文字にも一つのUnicode Scalarが定義されていますので、 Unicodeでは二つのUnicode Scalarの組み合わせで書けます:

  • 305A

  • 3059 + 3099

Swift

SwistのStringはこの考え方を反映しています。 Stringのプロパティーを見ると四つのビューが存在しています: String.characters: CharacterView String.unicodeScalars: UnicodeScalarView String.utf16: UTF16View String.utf8: UTF8View

見覚えありますよね?Unicodeの文字の定義を見たときにでた様子です!

  • CharacterViewはUnicodeの文字のコレクション

  • unicodeScalarsはUnicode Scalarのコレクション

  • utf16とutf8はCode Unitの16ビットのコレクションと8ビットのコレクション

Unicode Scalarの入出力

Stringの文字をUnicode Scalarから定義したいときは文字列の中に「\u{番号}」というふうに書きます。 例として「Pokémon」の「é」を使いましょう。「é」は「ず」と同じく、Unicode Scalarで二つの定義方法があります:「00E9」 (é)と「0065+0301」(e + ́)。Swiftで書くと下記のようになります。

f:id:glpgsinc:20170418190846p:plain

文字列から、Unicode Scalarを出力したいときは、String.unicodeScalarsにあるアイテムのvalueを取ります:

f:id:glpgsinc:20170418192346p:plain

char.valueをそのまま使うと、10進で表示されますので、16進でStringとして表します。

UTF-8UTF-16の出力

UTF-8UTF-16の出力はString.utf8、String.utf16プロパティーを使います。

f:id:glpgsinc:20170418193339p:plain

f:id:glpgsinc:20170418193557p:plain

お分かりになったと思いますが、8ビットで表現できるUnicode ScalarはUTF-8UTF-16で一つのCode Unitになります、16ビット表現されるUnicode ScalarはUTF-8で二つのCode Unitになり、UTF-16で一つのCode Unitになります。

従来のStringとの違い

Stringという型は、従来の言語でただの文字の配列です。Androidでよく使われているJavaのStringと比べて見ましょう。

JavaのStringはどういうものかというとStringのUTF-16のCode Unitの配列になります。 SwiftとAndroidのStringの文字数を比べて見ましょう。 例として、先ほどのPokémonを使います。まずは「é」が一つのUnicode Scalarで定義されている場合:

Swift:

f:id:glpgsinc:20170421190308p:plain

Java:

f:id:glpgsinc:20170421190727p:plain

この場合は同じですね。面白くなるのは「é」を二つのUnicode Scalarで定義した時:

Swift:

f:id:glpgsinc:20170421191856p:plain

Java:

f:id:glpgsinc:20170421191917p:plain

Swiftは7つのままですが、Javaの方は8つになりました!

今回Unicode Scalarが8つあって、Javaのcountが8つになりました。JavaUnicode Scalarの配列なのか、確認したいですね。そのために32ビットのUnicode Scalarを使って見ます。

どういうことかというと、JavaのStringは文字の配列ではなく、Unicode Scalarの配列ですので、今回は「e」と「 ́」が一つずつ数えられる。Swiftの方はUnicode Scalarではなく、文字を数えてるので、「é」の両方の定義でも数が変わりません。 SwiftでUnicode Scalarを数えたいときは、unicodeSclarsを使います:

f:id:glpgsinc:20170421201415p:plain

SwiftでUnicode Scalarが一つなのに、utf16の方で二つになります。Javaの方はどう?

f:id:glpgsinc:20170421201450p:plain

Javaも2になりますが、もうちょっと詳しく見ると「\u10000」の最後の「0」の色が違う、Unicode Scalarのコードの一部として認識されるのではなく、「0」という文字として認識されて、「𐀀」が「က0」として認識されてしまった。

結論として、SwiftのStringはUnicodeの文字の配列で、JavaのStringはStringのUTF-16のCode Unitの配列。 ただ、SwiftのStringは本当に配列なのか?配列だったら、Intをインデックスとして、文字にアクセスできますね?テストして見ると、コンパイルが通らない、SwiftのStringはIntをインデックスとして使える配列ではない。

なぜそうなったかというと、もともと配列は中に入れるアイテムのサイズが全部同じという前提で定義されていて、Intを使ってアクセスすとき、メモリーアドレスは単純に「配列のアドレス+インデックスxエレメントのサイズ」で計算されています。ただし、SwiftのStringが使っているUnicodeの文字のサイズはバラバラです。Unicode Scalarによってサイズも違いますし、複数のUnicode Scalarを使った文字もあります。Stringのいくつ目の文字のアドレスを計算するには、単純な掛け算ではなく、一文字目から一個ずと文字のサイズを足して計算します。

その原因でSwiftのStringはただの配列ではないし、Intのインデックスから文字の場所の計算のコストが高いので、Collection扱いもSwift 2でなくなりました。

Pokémonの場合、「é」以外の文字は1バイトですが、「é」は書き方のよって2バイトか3バイトが必要です。 f:id:glpgsinc:20170424180202p:plain

インデックスの扱い

SwiftでStringの文字にアクセスするには他の言語と違ってIntのインデックスが使えないことがわかりましたので、Swiftでどういうインデックスが使えるか紹介します。

Swiftではビューによって専用のインデックスタイプが用意されています:

  • String.characters: String.CharacterView.Index

  • String.unicodeScalars: String.UnicodeScalarView.Index

  • String.utf16: String.Utf16View.Index

  • String.utf8: String.Utf8View.Index

各インデックスの使い方は同じ:

  • startIndex:一つ目の文字のインデックス 例:myString.characters.startIndex

  • endIndex:最後の文字のインデックス+1 例:myString.unicodeScalars.endIndex

  • index(i: Index, offsetBy: Int):一つのインデックスから別のインデックスの計算。 例:myString.utf16.index(myString.utf16.startIndex, offsetBy: 2)

  • index(after: Index):次のインデックス

  • index(of: T):文字、UnicodeScalarなどのインデックス

ビューの間のインデックスの変換が可能です:String.CharacterView.Index(String.UnicodeScalarView.Index, in: String)、String.Utf16View.Index(String.Utf8View.Index, in: String)

Stringに直接インデックスも使えますが、String.CharacterViewのインデックスです。

StringのビューはCollectionになっているため、ループができますが、パーフォマンスの問題がそのまま残っているため、使うときに注意したほうがいいです。

サブストリング

Stringからサブストリングを取るときにSwiftは単純にその文字をメモリーの別のところにコピーしているわけではなく、新しいStringは元のStringへのポインターとサブストリングのインデックスを持っています。そうすることによって、サブストリングで処理を行うときに、Stringはコピーされないので、処理が速い。ただし、サブストリングが残ってる限り、元のStringを消しても、メモリーに残ってしまいます。長い文字列から一つの単語をサブストリングにしてから、元の長いStringを消してもメモリー上、全部残ってしまいます。Swiftのサブストリングの処理はメモリーより速さを優先している。

SwiftのStringの不思議なところ

文字列がUnicode Scalarの変化様子字で始めれば、たのStringと合わせる場合、文字がまーじされることがあります。

f:id:glpgsinc:20170424192657p:plain

絵文字の場合、想定外の文字数になることがあります:

複数の国旗絵文字を並べても文字数が1になります

f:id:glpgsinc:20170424193538p:plain

四人の絵文字の文字数は・・4

f:id:glpgsinc:20170424193609p:plain

これでSwiftのStringはちょっとわかりやすくなりましたかな?中を見てみると意外と面白いですね。

SwiftのStringについて調べたときに役に立った記事:

テスト設計コンテスト'17 決勝戦聴講レポート

こんにちは!テストチームとのの(TW:@tono2587)です。
今回は2/23に参加した「テスト設計コンテスト」のことを書きます。
f:id:glpgsinc:20170419180938p:plain

時間は経ってしまいましたが、初めてのテスコンだったので張り切ってレポートします!

・テスト設計コンテストとは?
・決勝戦概要
・U-30クラス
・OPENクラス
・全体講評メモ
・感想など

テスト設計コンテストとは?

名前だけは聞いたことがあったのですが、いったいどんなコンテストなのかは知りませんでした。一緒におさらいしましょう!
簡単に言うと、「テスト設計のスキルを競い合うことで、さらに高めていきましょう!という会」な認識です。
公式サイト:http://aster.or.jp/business/contest.html
「OPENクラス(だれでも)」と「U-30クラス(30歳以下のみ)」の2つのクラスにわかれており、それぞれで予選がおこなわれた上での決勝戦となります。
わたしはチュートリアルは参加せず(知らなかった…)、この決勝戦のみ聴講で参加しました。

今回のテストベース

U-30クラス:「話題沸騰ポット」
OPENクラス:カラオケシステム

テストベース仕様、採点基準についてはコチラから↓
http://aster.or.jp/business/contest/rulebooku30.html http://aster.or.jp/business/contest/rulebook.html

勝戦概要

◇日時
2017年2月23日(木)
10:00 U-30クラス決勝戦開始
12:30~13:30 昼休み
14:40 OPENクラス決勝戦開始
18:10 終了

◇場所:日本大学理工学部 駿河台校舎 7号館

◇参加チーム(発表順)

U-30クラス OPENクラス
OPTiM てすにゃんRev2
いんプレオ 紙印テスト倶楽部
ヤングレッジ STUDIO IBURI
でこパン462 モモテツ
チームT研 わんだーズ
SHINNOSUKE
TBD

☆持ち時間は発表15分→質疑応答5分の計20分

U-30クラス

優勝 でこパン462 準優勝 SHINNOSUKE

総評メモ
井芹洋輝さん(審査委員長)より

入賞チームについて

  • でこぱん462
     テスト要求分析の発散と収束が上手だった(まとめが上手かった)
     3つの目的でビューを分けてやっていたところがよかった
     テストの手法を理解してつかっていた

  • SHINNOSUKE
     わかりやすかった
     よく内容をみるとあれ?ってところがある
     アーキテクチャを2つつくって片方を採用したところがおもしろいが、根拠が少ない

  • TBD(準優勝惜しかった)
     テスト要求分析でいろんな分析をやっている
     新しい技術にチャレンジしているのがよいが、なぜその手法なのか、どうつながっているのか、それがみえず説得力に欠ける
     技法、手法をつかうなら理解していることも証明できるとよい

U-30クラス全体について

  • テスト開発プロセスの基礎ができていない

  • 技術力不足
    チュートリアルで設計について説明があったけど、それを満たしていない
    手法の使い所がおかしい、正しくない

  • 竜頭蛇尾
    分析がテストケースにまで落ちていない
    テスト設計は、「テスト」が成果物であり、本質。
    「テスト」をよくすることが一番で、そのためにやっている。

  • テストの厚みの分析が足りない
    根拠の分析が弱い。
    「なにを」テストするかはできているが「どこまで」テストするかが足りない

上記の改善について

  • 品質 についてよく考えて下さい。理解してください。
    ポットに対してテストが仕様書どおりではそもそもテストが足りない。
    (倒れる危険があったり、子どもがつかう、という前提がある。)

  • 「よいテスト」について考えてください
    保守性の高いテスト、マネジメントが高いテスト、等
    テストの内部品質を工夫しましょう

  • 各「技」はしっかり身につけてください
    技ばかりにこだわってはいけない。それをつかってこそ

OPENクラス

優勝 STUDIO IBURI 準優勝 わんだーズ

総評メモ
鈴木三紀夫さん、湯本剛さんより

  • 成果物をみて
    とんでもない成果物の量で圧倒しているチームもあったが、それは本当によいかどうか
  • アーキテクチャはいろんなビューでみなければいけない。
    どう組み合わせたら品質がよくなるのか
    いくつかのアーキテクチャのビューのなかで、順番のやつが簡単です 
    アーキテクチャ設計」とは…をよく考える
  • アジャイル開発にチャレンジするなら中途半端ではなく、それについてもしっかり勉強しておかないといけない
  • 「用語」をしっかりつかうこと。
    一般的な使い方をしたほうがよい。発表にそれぞれ違う定義でつかわれている。この発表ではこうです、という定義がされていてもいつもと違う使い方だと誤解しそうになる

全体講評メモ

西康晴さん(審査委員長)より

  • テスコンはじめの4年は主に「テスト観点」で勝負がついていた。
    そこからテスト要求分析の技術がついてきて、2年くらい前からテストアーキテクチャ設計の質で勝負が決まるようになってきた。
  • 設計方針とはなんなのか をしっかり考える
    ものの設計とは何か?
    車輪で走るような自転車の設計、とかはわかりやすいが、じゃあ「テスト」の設計は???
    テストの設計は何が動くのかよくわからないからソフトウェア設計よりも難しい。  
    参加者みんなで技術力をアップさせていっている。

  • テストレベル、テストタイプ などの定義は世界にもある。
    それをどういう順番で並べるかを検討する方法はまだ世界にはない。
    UMLテスト 2.0とかに入ってくるかもしれない
    世界最先端です!

  • 今回はテストベースが「派生製品」だった
    じゃあ次回は?自動化を考えたら設計の中身も変わってきたりするでしょう

  • 要求分析→アーキテクチャ設計
    この流れが、過程が踏まれてない

  • 成果物がただの日記で、「理由」「意図」がない
    やることが決まっていて、何も考えずにやったことを出せばOKがもらえる仕事になっている

  • つかう用語、技術は理解してつかわなければならない
    自分たちの技術にする、アイデアにする、現場の工夫をコンテストの機会で整理してください
    それをフィードバックにして、自分たちの強みにする

感想など

  • 同じテストベースを与えられているのに、成果物も発表も同じものはひとつもない
    逆に、自分たちの立ち位置の説明→設計の流れ説明→要求分析→テスト設計の流れはだいたい共通していたので、基本的なテスト設計の流れを知ることができました。
  • プレゼン(発表)も得点に入っている
    設計した成果物だけではなくて、プレゼン内容が得点に影響するところがおもしろかったです。自分たちの設計したことを相手にわかるように説明することも、ときには必要なスキルなのだ思いました。
  • すぐできそうなこと
    「用語」や「手法」を正しく理解すること。スキルアップには基本知識を増やすことが一番はやそう
  • 「世界の最先端です」
    総評のこの話が一番わくわくしました!最先端技術のすぐそばにいる!

学んだこと

  • テスト設計の基本の流れを理解しました
  • テストの成果物も設計により様々なのだと知りました
  • 十分な設計をするためにテスト設計の基本知識を身につけることが近道だと教えてもらいました
  • 技術を高める機会が社外にもあることを知りました

さいごに

参加者には発表者や関係者が多い印象でしたが、聴講のみでも参加できてよかったです。
参加前にもう少しテストベースについて読んでいったり、予習していけばよかったなと思いました。(そのほうがもっと話が入りやすかったかも)
総評で「世界の最先端にいるから、本とかに自分の名前のついた手法が載ったりするかもよ(意訳)」と言われたときはすごくテンションがあがりました!
勉強のし甲斐がありますよね。これからもがんばりましょうー!!!

「最先端」が大好きなぼくのはたらく会社がこちら
www.glpgs.com

RxSwiftでMVVMの簡単なサンプルコード

こんにちは、iOS/Androidエンジニアのイバンです。

今回はアプリのアーキテクチャについての記事になります。

ソフトウエアのアーキテクチャというと様々な提案があります。iOSアプリ開発になると、アップルが推奨するMVCが一番よく使われているでしょう。

MVCと課題

MVCとはモデル(データ)・ビュー(ユーザインターフェース)・コントローラー(ロジック)という3つの責任にコンポーネントに分別するソフトウエアデザインパターンの一つです。

ビューはモデルのデータを表示して、コントローラーはビューとモデルを結びつくロジックを持っているコンポーネントです。

コントローラーは多くのUIのロジックとデータの処理のロジック両方を持ってコードの行数が簡単に何千行に上ります。iOSMVCは実際にM + VC、つまり、ビューとコントローラーは一つになっているわけですね。

ビジネスロジックとUIロジックが混合しているとユニットテストも書きにくくなります。どちらかというと書かないようになってしまうことが多いですね。

そうなるとコードのメンテナンス性とテスト性が低下してしまい技術的負債が増え続けるパターンになりがちです。

太くなってしまうコントローラーが業界中にファットコントローラー、MVCはMassive View Controllerと呼ばれるようになってますね。

MVVMでコントローラーをダイエットさせましょう

ファットコントローラーを痩せたコントローラーにするには様々な提案がありますが、今回はMVVMを紹介したいと思います。

MVVMのパターンは下記のようにアプリのコンポーネントを分別します。

  • M(モデル):情報を持つエンティティ。
  • V(ビュー):ビューとビューコントローラを含めます。ビューを管理する以外のロジックは持ってない。
  • VM(ビューモデル):モデルを取得して表示用にデータを変換する

f:id:glpgsinc:20170406175204p:plain

ビュー(コントローラー)は情報の表示とインプットの受け付けしか行いません。表示のフォーマットとインプットのアクションとその他の情報処理はビューモデルに任せます。ビューはモデルについて何も知りません。つまり、モデルのインスタンスは持ちません。

ビューモデルでは表示のロジックを行いますがUIについて何も知りません。UIKitインポートしないし、ビューのインスタンスも持ちません。 ビューモデルがモデルから取得した情報を加工してビューに提供します。

モデルは情報しか持ってないです。Swiftだとstructで実装するのは一般的です。

ここは疑問一つ挙げられることがあると思います。もしビューモデルは表示用にデータを取得して処理するなら、ビューを参照しないでどうやって画面を更新するのか?

答えはデータバインディングが仕込まれているのです。

データバインディング

データバインディングの仕組みを使うと、データがビューと連動させられてデータが変わったら自動的にビューに反映されます。

iOSはデータバインディングを対応してないので外部ライブラリを導入する必要があります。ReactKit/Bond、ReactiveCocoa、RxSwiftなどありますが、今回はRxSwiftを使います。

RxSwiftはバインディングだけではなく非同期処理やイベント処理を宣言的に書けるライブラリです。サンプルコードで使う分の解説はしますが、RxSwiftについて詳しく知りたい方は初めてのRxSwiftの発表資料や参考文献の節にあるリンクをご確認いただければ幸いです。

実際にMVVMどうなる?

サンプルアプリは単純にAPIサービスを利用して画像とテキストを取得して表示するだけですが、簡単なソースコードになりますのでMVVMはどうなっているのかわかりやすいと思います。実際のアプリには足りないところが多いですが、MVVMの基本的な仕組みの理解には十分かと思います。要求あれば今後の記事で拡張していきたいと思います。

今回はXKCDというギークなコミックサイトのAPIを使ってコミックを表示します。サンプルコードはこちらからダウンロードできます: xkcd観覧アプリ

さて、コードを追って解説しましょう。

ビュー

ComicViewControllerviewDidLoadviewModelの初期化とデータバインディングを行います。ViewModelからViewModelへバインディングは2パターンがあります。ViewModelからはビューモデル側でデータが更新になったらビューを更新するようにバインディングします。

例えば

comicViewModel.title.asDriver().drive(titleLabel.rx.text).disposed(by: disposeBag)

ビューモデルがコミックのタイトルを取得できたらRxの力でタイトルラベルに自動的に反映されるように設定します。 DriverはRxSwiftの世界で監視可能なオブジェクトのことです。最後にあるディスポーザはメモリ管理の為に追加します。

ViewModelへのところではボタンの処理(前後のコミックを依頼する)の設定と最新のコミックを取得する依頼を行いす。

ボタンのハンドリングはIBActionでも良いですが、Rxを使うとコンパクトにviewDidLoad()の中に書けます。

nextButton.rx.tap.asDriver().drive(onNext: {
    self.comicViewModel.getNextComic()
}).disposed(by: disposeBag)

実際の処理はcomicViewModelに任せます。

ビューコントローラーは以上でビューのデータバインディングとボタンのタップイベントを受けるだけのコードになります。

ビューモデル

ビューモデルはデータを取得して表示用に処理します。まずはデータバインディングできるようにRxの監視型プロパティーを用意します。例えばタイトルは下記のように宣言します。

var title: Variable<String>

ビューコントローラ側にデータバインディングdrive(..)のところ)を設定してますのでtitle.value = "タイトルです"だけで自動的にtitleLabelに反映されます。

文字列はそんな感じですが、次へ、前へのボタンは無効になったり有効になったりしますね。それはisNextEnabled/isPreviousEnabledのドライバーで実現します。監視型のVariableで最新号と観覧中のコミックを持っていて、isNextEnabledcombineLatestでその両方が同じの場合(見ているコミックは最新)はfalseを返します。isNextEnablednextButtonenabledプロパティーにバインドされてますので自動的に反映されます。

isNextEnabled = Driver.combineLatest(self.latestComicNum.asDriver(), self.currentComic.asDriver(), resultSelector: { (latestNum, current) -> Bool in
    guard let latestNum = latestNum, let currentNum = current?.num else { return false }
    return  latestNum != currentNum
}).distinctUntilChanged()

isPreviousEnabledはコミック番号が1かどうかを確認するだけですのでmapで適切なブールを返します。

isPreviousEnabled = currentComic.asDriver().map({ (comic) -> Bool in
    guard let num = comic?.num else {
        return false
    }
    return num > 1
}).distinctUntilChanged()

distinctUntilChangedは回覧中が変わる度にブールの値が変わらなければ無駄にenabledバインディング処理が起こさないようにあります。

ビューモデルの初期化は以上で終わります。他の関数はapiサービスの呼び出しとビューモデルの情報の更新です。

ApiサービスもRx化になっているのでsubscribe(onNext:)でコミックの情報を取得します。情報はComicモデルクラスの形で来ますが、モデルから表示用に変えるのがupdateViewModelというヘルパー関数で行います。日付のフォーマットを変えたり画像のurlのstringをURLオブジェクトに変換します。

func getLatestComic() {
    service.getLatestComic().subscribe(onNext: { (comic) in
        guard let comic = comic else {
            return
        }
        self.latestComicNum.value = comic.num
        self.updateViewModel(comic: comic)
    }).disposed(by: disposeBag)
}

Variablevalueをアサインすると、Rxのバインディングで画面が更新されます。

private func updateViewModel(comic: Comic) {
    self.currentComic.value = comic
    self.title.value = comic.title ?? ""

    if let urlString = comic.img, let url = URL(string: urlString) {
        self.imageUrl.value = url
    }

    if let date = comic.date {
        self.date.value = formatter.string(from: date)
    } else {
        self.date.value = ""
    }
}

モデル

モデルは生のデータしか持ってないstructです。引数としてDictionaryを渡せるコンストラクタもありますが、基本的にはロジックが持ちません。ビジネスロジックはビューモデルか別のクラスに委任すると良いです。例えばコミックを取得するのはComicServiceの役割で、それを利用するのはモデルではなくビューモデルですね。

何かのロジックを入れるとしたら、内部のデータの形式の変更やデータのバリデーションぐらいなら良いかと思います。

TODO

エラーハンドリング

このサンプルコードは未完成なプロジェクトになっています。エラーハンドリングは行っていません。実装のアプローチとしてビューモデルからアラートを出すのは一番簡単ですが、エラーの場合にビューを更新したい場合はエラーの状態を持つ監視型のプロパティーをビューモデルに追加してビューコントローラーでバインディングしてビューを更新されるのが考えられますね。

テスト

テストコードも一切書いてないですね。書くとしたら主にビューモデルのテストを書いたらカバレッジの高いテストになります。ビューコントローラーにはロジックがなくて、ビューモデルにはテストの難しいビューもありませんのでテストが書きやすくなっています。

テストに関するもう一つ改善できるところはビューモデルが持つComicServiceインスタンスです。現状のコードだとビューモデルでインスタンスを生成してますが、DI(依存性注入)を使ってインスタンスをビューモデルに渡すとテストのターゲットの際、テスト用のモックサービスに入れ替えて通信なし気楽にテストできるようになります。

それらの課題は今後、の記事の続きにしたいと思います。

参考