Swift Playgroundsで快適なSwiftUIコーディング環境を #SwiftAdventCalendar

これは Swift Advent Calendar 2019 24日目のエントリーです。
昨日23日は Moto0124 さんの CGAffineTransformを知る でした。

はてなエンジニアAdvent Calendar 2019での Deletable Table with TextField on SwiftUI に続いて、今回も SwiftUI ネタです。

そもそもSwift Playgroundsって?

ここでいう Swift PlaygroundsXcodeに付属のPlaygroundではなく、iPadアプリとしての Swift Playgroundsを指しています。
以降、 Swift Playgrounds は 「iPadアプリのSwift Playground」、 Xcode PlaygroundXcodeに付属のPlaygroundと使い分けます。

ドキュメント: https://developer.apple.com/documentation/swift_playgrounds

SwiftUIをSwift Playgroundsで扱う

SwiftUIのアナウンス直後から、Swift Playground 3.1以降でSwiftUIが実行できることは話題になっていました。

Swift Playgrounds 3.1のVersion History

us: • Build with the SwiftUI framework in new playgrounds you create
jp: • 新しく作成するプレイグラウンドでは、SwiftUIフレームワークを使用してビルドできます

SwiftUIで表示してみる

すでに広く知られている話だとは思うけれど、まずは単純に表示してみましょう。

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
                .font(.largeTitle)
                .foregroundColor(.primary)
            Text("world")
                .font(.title)
                .foregroundColor(.secondary)
        }
    }
}

let host = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = host

f:id:yutailang0119:20191223171953j:plain
Swift Playgroundsで実行

Xcode Playground用ファイルとしてExportする

便利な使い方として、Xcode Playground用の拡張子 .playground として作成することもできます。
ファイル作成時に Starting Points > Xcode Playground を選択して、始める必要があります。

f:id:yutailang0119:20191223172525j:plain
Starting PointsからXcode Playgroundを選択

ここから作成したファイルをiCloudAirdropmacOSに送り、Xcodeで開くと、実行できます。

f:id:yutailang0119:20191223161553p:plain
Xcode Playgroundで実行

再利用できるViewの作り方

簡単なレイアウトのViewを作ってみるにはこれだけでも十分便利ですが、複雑なレイアウトを作ったり、再利用しやすいViewを作ったりには不向きに思えます。
また、Playground上での作成と言っても、最終的にはアプリ開発にViewを組み込みたいでしょう。

Swift Playgroundsでも、複数のSwiftファイルを扱うことができるので、各パーツのViewを別々のSwiftファイルに分割してみます。
参考: https://developer.apple.com/documentation/swift_playgrounds/structuring_content_for_swift_playgrounds/using_modules_to_share_code_in_a_playground_book

ファイルを追加

左上のファイルアイコンから、ファイルの追加ができます。
今回は3ファイル作成しました。

f:id:yutailang0119:20191223183537j:plain
左上のアイコンから、ファイルを追加

LargeTitleView.swift

import SwiftUI

struct LargeTitleView: View {
    var body: some View {
        Text("Hello")
            .font(.largeTitle)
            .foregroundColor(.primary)
    }
}

TitleView.swift

import SwiftUI

struct TitleView: View {
    var body: some View {
        Text("World")
            .font(.title)
            .foregroundColor(.secondary)
    }
}

Preview.swift

Preview のみ public 修飾子でアクセスできるようにしておきます。

import SwiftUI

public struct Preview: View {
    
    public init() {}
    
    public var body: some View {
        VStack {
            LargeTitleView()
            TitleView()
        }
    }
}

main

Swift Playgroundのmainファイルでは、PreviewUIHostingController で描画するようにします。
Swift Playgroundsでのmainと、その他のファイルとでは、別Module扱いになるため、 public 修飾子ないものにはアクセスできません。
import はいらない。

import SwiftUI
import PlaygroundSupport

let preview = Preview()
let host = UIHostingController(rootView: preview)
PlaygroundPage.current.liveView = host

f:id:yutailang0119:20191223185219j:plain
実行結果

これ以降はPreviewと各Viewを書き換えるだけで、実行ができるようになりました。
こうすることで、各View毎のstructをそれぞれファイルとして分割できたり、 public 修飾子を書き忘れたり、 init() 実装し忘れで実行できなかったりがなくなります。
iCloud経由で、Xcodeのアプリプロジェクトに追加もできるので、非常に便利ですね。
もちろん、copy&pasteするだけでも、動きます。

Xcode Playgroundで実行する時には注意点があって、Xcode Playground上でのファイルリンクは特殊で、struct定義から直接ファイルジャンプはできません。
上部のペインから、辿ってください。

f:id:yutailang0119:20191223184833p:plain
Jump to Definitionでは、ファイルジャンプできない

まとめ

iPad Proを買ってからというもの、Swift Playgroundsを使いたいと思うことが幾度とありましたが、やっと活用方法を見つけた気がします。
当たり前ですが、iCloudでファイルを同期する場合は、macOSで共有ディレクトリを開いていたりすると、最新の変更が更新されず、消えてしますこともあるので、ご注意を!
また、まだまだ不安定なのか、たまに .playground/Contents.swft が破損して、開けなくなることもありました。 *1

おまけ

このエントリー用の検証していて、以下のコードを書けることに気づいた。
なにか使い道ないかな。

import SwiftUI

public struct Preview<Body: View>: View {
    
    private let _body: Body
    
    public init(body: Body) {
        self._body = body
    }
    
    public var body: some View {
        _body
    }
}

let preview = Preview(body: ContentView())

*1:復帰方法は、macOSでパッケージの中のファイルをサルベージして、新しいPlaygroundとして作成し直す他ないようです...

Deletable Table with TextField on SwiftUI #はてなエンジニアAdventCalendar

これは はてなエンジニアAdvent Calendar 2019 1日目のエントリーです。
今年のAdvent Calendarでは初日を担当します id:yutailang0119 です!
去年はツール作りの話を書きましたが、今年は先日のアンケート記事でも言及していたSwiftUIの話です。

お題

  1. SwiftUIで
  2. 編集可能なテキストの
  3. リストを作り
  4. Rowごとに削除が可能

これが実は難しいお題だったので、解説していきます。

環境

Xcode 11.2.1 (11B500)

2024/02/19追記

Xcode 15.2 (15C500b) 現在 (おそらくこれ以前に) 、ForEachにBindingなinit(_:content:) が追加*1されたため、単純に実装が可能になりました。
ただし、1入力ごとにstateが確定し、キーボードが閉じてしまうため、この条件ではうまく機能しない気がします。

struct ContentView: View {
    @State private var animals: [String] = ["🐶", "🐱"]

    var body: some View {
        VStack {
            EditButton()
            List {
                ForEach($animals, id: \.self) { $animal in
                    TextField("", text: $animal)
                }
                .onDelete { indexSet in
                    self.animals.remove(atOffsets: indexSet)
                }
            }
        }
    }
}

解答

最初に解答なんですが、こちらが自分がstack overflowに投稿した質問。

stackoverflow.com

解説

リストを作る

まずは、SwiftUIの初歩とも言えるリストを作ってみます。

import SwiftUI

struct ContentView: View {
    private var animals: [String] = ["🐶", "🐱"]
    
    var body: some View {
        List {
            ForEach(animals, id: \.self) { animal in
                Text(animal)
            }
        }
    }
}

この後の形式と揃えるために ListForEach の両方を使用していますが、単一なViewのリストであれば、 List のみでも実現できます。

こんな感じ

import SwiftUI

struct ContentView: View {
    private var animals: [String] = ["🐶", "🐱"]

    var body: some View {
        List(animals, id: \.self) { animal in
            Text(animal)
        }
    }
}

テキストを編集可能にする

前述のリストの各項目を編集可能にしてみます。
Textで文字列を表示していた箇所をTextFieldに変更すればよさそうです。
しかし、 TextField のinitializerは、 Binding<String> を引数として要求して、更新を反映していきます。
ForEach のmapでは Binding<T> を取得することはできないので、配列のカウントをRangeでループさせ、indexを使って Binding<String> を扱えるようにします。 *2
合わせて、DataSourceである animals@State を付与しておきます。

import SwiftUI

struct ContentView: View {
    @State private var animals: [String] = ["🐶", "🐱"]

    var body: some View {
        List {
            ForEach(0..<animals.count) { i in
                TextField("", text: self.$animals[i])
            }
        }
    }

これで、各項目のテキストを編集できるようになりました。
ここまでのリストとTextFieldの組み合わせだけでも、ちょっとトリッキーなことをしている印象はありますね。

Rowを削除可能にする

最後の条件です。
ForEach には .onDeleteが実装されていて、これを使うと EditMode.active の時には、UITableView.isEditingと近い振る舞いをできるようになります。 *3
EditMode == .active である必要があるので、 EditButton も追加しておきましょう。

以上を満たした実装をします。

import SwiftUI

struct ContentView: View {
    @State private var animals: [String] = ["🐶", "🐱"]

    var body: some View {
        VStack {
            EditButton()
            List {
                ForEach(0..<animals.count) { i in
                    TextField("", text: self.$animals[i])
                }
                .onDelete { indexSet in
                    self.animals.remove(atOffsets: indexSet)
                }
            }
        }
    }
}

Previewで確認してみましょう。

Preview実行し、EditButtonを押したところ

できていそう!!!

これではRuntime Errorが起きる

PreviewでRowを削除してみると、Previewの端末画面が真っ白になってしまいます。
これはRuntimeでエラーが起きたということです。 *4

シミュレーターで実行し、再度エラーを発生させ、ログを見てみます。

Thread 1: Fatal error: Index out of range

Index out of Range が発生しているということがわかりました。

このログを考察すると、🐶を削除したにも関わらず、 ForEach のループは2回起きているようです。
念の為に、animals から🐶が削除できているのか確認してみます。

(lldb) po animals.count
1

(lldb) po animals
▿ 1 element
  - 0 : "🐱"

🤔🤔🤔

ここからいろいろ試してみますが、うまくいかず、冒頭のstack overflowの質問をしました。

例えば、最初の Text のリストの削除はうまくいく

import SwiftUI

struct ContentView: View {
    private var animals: [String] = ["🐶", "🐱"]
    
    var body: some View {
        List {
            ForEach(animals, id: \.self) { animal in
                Text(animal)
            }
            .onDelete { indexSet in
                self.animals.remove(atOffsets: indexSet)
            }
        }
    }
}

stack overflowでの解答

https://stackoverflow.com/a/58911168/11264346

解答にあるように、ForeEach.init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content) のDocument Commentを読んでみます。

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ForEach where Data == Range<Int>, ID == Int, Content : View {

    /// Creates an instance that computes views on demand over a *constant*
    /// range.
    ///
    /// This instance only reads the initial value of `data` and so it does not
    /// need to identify views across updates.
    ///
    /// To compute views on demand over a dynamic range use
    /// `ForEach(_:id:content:)`.
    public init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)
}

なるほど

  • data の初期値のみを読み取るので、更新時のView識別が必要ない
    • -> 要素数に更新があっても、初期値のみを読み取るので、それに追従されない。
  • ダイナミックに計算するには、 ForEach(_:id:content:)を使用する。

なるほど
それだと、 Binding<T> が扱えないんだけど!

答え

解答してくれた Asperiさんが、ちゃんと実装も答えてくれていました。
以下が解です。

import SwiftUI

struct ContentView: View {
    @State private var animals: [String] = ["🐶", "🐱"]

    var body: some View {
        VStack {
            EditButton()
            List {
                ForEach(animals, id: \.self) { animal in
                    EditorView(container: self.$animals, index: self.animals.firstIndex(of: animal)!, text: animal)
                }
                .onDelete { indexSet in
                    self.animals.remove(atOffsets: indexSet)
                }
            }
        }
    }
}

struct EditorView : View {
    var container: Binding<[String]>
    var index: Int

    @State var text: String

    var body: some View {
        TextField("", text: self.$text, onCommit: {
            self.container.wrappedValue[self.index] = self.text
        })
    }
}

ForEach では、要素数の変更を反映できるように .init(_:id:content:) を使用し、EditorViewの中で、TextFieldでの変更を元の animals に更新できるように作ると解決できました!

まとめ

SwiftUIはまだまだ足りないパーツも多いし、ちょっと凝ったことをしようとすると、ドキュメントが少なく苦労することが多いので、挫折しがちです。
特にEditModeが絡む実装をしようとすると、途端にハイレベルになる印象があります。
SwiftUIがよりよいものになるように、集合知を集めて行きましょう!!!

明日は id:masawada です!

はてなエンジニアAdvent Calendar 2019 をお楽しみに!

*1:iOS 13にBackport済み

*2:ちょっと綺麗な形ではないですね

*3:実際にはスワイプ削除等が効かないので、近い振る舞い

*4:Previewでのエラーはログが出ない

Kotlin LiveDataにReactiveXっぽいOperatorを追加する

こんにちは、Reactive人間こと id:yutailang0119 です。

世間にはiOSアプリケーションエンジニアとして認識されていると思いますが、仕事ではAndroidアプリも作っています。
最近は公私で、ReactiveXReactiveCocoaCombine、そしてLiveDataと広義のFunctional Reactive Programming (以下、FRP) と触れ合って生活しています。

そんな中で、今回はLiveDataネタです。

LiveDataとは

一言で表すなら、「Androidのライフサイクルに寄り添ったFRP Framework」といったところでしょうか。
詳細な説明は、LiveData Overview やその他の詳細な解説ブログたち任せます。

LiveDataの欠点

ReactiveX/RxJavaReactiveX/RxSwiftを使ってきた人が、LiveDataに触れた時の戸惑いは、Operatorの少なさだと思います。
ReactiveXにはこんなにも豊富なOperatorが用意されています。

一方、素のLiveDataでは、 map 一つで苦労します。

LiveDataの map

val user: MutableLiveData<User?> = MutableLiveData() // RXSwiftでいうとBehaviorRelayだと考えてもらえば、ここではよい
val name: LiveData<String?> = Transformations.map(user) { it?.name }

ただ、 nameNon-Null な値だけを流すのは、 map だけでは一筋縄にいかなそうです。

Transformations

突然出てきた Transformations ですが、android.arch.lifecycle.Transformationsに実装されているOperatorまとめクラスです。
ドキュメントには mapswitchMapのみしか記載がありませんが、Version 2.1.0distinctUntilChanged も実装されました。

ただ、やはりReactiveXに慣れてしまった我々には、Operatorが足りない...

ということで、LiveDataを拡張して、Operatorを追加しましょう。

LiveDataにOperatorを追加する

前提

ここでは拡張したLiveDataをKotlinからのみ使用する場合を想定しています。
Javaの世界は考慮できていませんので、ご了承を。

Transformationsの実装を参考にする

Transformations.distinctUntilChangedの挙動を確かめる - QiitaAndroid Open Source Projectの実装が載っているので、参考にしてみます。

https://android-review.googlesource.com/c/platform/frameworks/support/+/763608/5/lifecycle/livedata/src/main/java/androidx/lifecycle/Transformations.java

    /**
     * Creates a new {@link LiveData} object does not emit a value until the source LiveData value
     * has been changed.  The value is considered changed if {@code equals()} yields {@code false}.
     *
     * @param source the input {@link LiveData}
     * @param <X>    the generic type parameter of {@code source}
     * @return       a new {@link LiveData} of type {@code X}
     */
    @MainThread
    @NonNull
    public static <X> LiveData<X> distinctUntilChanged(@NonNull LiveData<X> source) {
        final MediatorLiveData<X> outputLiveData = new MediatorLiveData<>();
        outputLiveData.addSource(source, new Observer<X>() {

            boolean mFirstTime = true;

            @Override
            public void onChanged(X currentValue) {
                final X previousValue = outputLiveData.getValue();
                if (mFirstTime
                        || (previousValue == null && currentValue != null)
                        || (previousValue != null && !previousValue.equals(currentValue))) {
                    mFirstTime = false;
                    outputLiveData.setValue(currentValue);
                }
            }
        });
        return outputLiveData;
    }

MediatorLiveData を内部で使って、変更をhookした時にobserve先に値を流すかを判断する処理を隠蔽したLiveDataを作るstaticメソッドとして実装されているようです。
これを参考に MediatorLiveData を利用すれば、内部に処理を隠蔽したOperatorを実装できそうです。

ということで、以下に答えです。

filter

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

fun <T> LiveData<T>.filter(predicate: (T?) -> Boolean): LiveData<T> {
    val mediatorLiveData: MediatorLiveData<T> = MediatorLiveData()
    mediatorLiveData.addSource(this) {
        if (predicate.invoke(it)) {
            mediatorLiveData.value = it
        }
    }
    return mediatorLiveData
}

notNull

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

fun <T> LiveData<T?>.notNull(): LiveData<T> {
    val mediatorLiveData: MediatorLiveData<T> = MediatorLiveData()
    mediatorLiveData.addSource(this) {
        if (it == null) return@addSource
        mediatorLiveData.value = it
    }
    return mediatorLiveData
}

使い方

val user: MutableLiveData<User?> = MutableLiveData()
val name: LiveData<String> = Transformations.map(user) { it?.name }
    .filter { !it.isNullOrEmpty() }
    .notNull()

filternotNull を実装したことで、 nameNon-Null な値だけを流せるようになりました。

解説

LiveDataの拡張関数として定義しています。
前述の通り、 TransformationsJavaで実装されているため、 TransformationCompanion objectの拡張関数としては実装できなそうです。
なので、LiveDataを直接拡張しました。

Transformations.map と書き方と違うので、ちょっともやっとしますね。
そこで androidx.lifecycle:lifecycle-livedata-ktx を使用しましょう。
Version 2.1.0 でTransformationsのOperatorの拡張関数が実装されています。

Added ktx extensions for LiveData.observe methods and Transformations.* methods.

使い方をさらに変更

これが顧客が本当に欲しかったものだ!!!

val user: MutableLiveData<User> = MutableLiveData(user)
val name: LiveData<String> = user
    .map  { it?.name }
    .filter { !it.isNullOrEmpty() }
    .notNull()

まとめ

今回の要領で、MediatorLiveData で拡張関数を実装すれば、ReactiveXの他のOperatorたちも実装できそうですね。
Transformations 自体に filternotNull くらいは実装してくれよと思うし、その内に追加されるのではとも思っています。
「LiveDataはそういうもんじゃないんだよ」と言われるかもしれませんが、直感的に書けることは真だと思っているので、ぜひお試しください。

「こういうのあるともっと便利だぜ」等ありましたら、 @yutailang0119までどうぞ!

Network Frameworkを使ったUDP通信チャットアプリのサンプルを作った

今年のGWは10連休だったわけだけど、あまりコード書く時間は取れなくて、終盤に何かやらないとやばいと思い立った。
そういえば、昨年のWWDC18で発表されたNetwork Frameworkに興味があったけど、結局使えてないなと思い出したので、ローカルネットワーク内でUDP通信するチャットアプリのサンプルを作ってみた。

github.com

これ自体はわざわざUDPを使う必要もない実装ではあるのだけど、これをベースに例えば動画や音声コミュニケーションを実装したいなと思っている。
これが本当のサーバーレスや!!! *1

Special Thanks

実際は @shu223さんのブログを、大筋真似ただけなんだけども、やってみると、大枠は掴めた。

shu223.hatenablog.com

締め

書き出しが遅かったので、GW後にもちょっと手直しする必要が出てきて、結局月末までかかってしまったけど、なんとかWWDC19が始まる前に触れてよかった。

*1:違う

fastlane deliverでiPad Pro 3rd generationのスクリーンショットアップロードができるようになった件

App Store ConnectではiPhone XS Maxおよび、iPad Pro 3rd Generationのストアスクリーンショットが必須になった。
(いわゆるベゼルレス筐体)

Upcoming App Store Submission Requirements - News - Apple Developer

Starting March 27, 2019, all new apps and app updates for iPhone or iPad, including universal apps, must be built with the iOS 12.1 SDK or later and support iPhone XS Max or the 12.9-inch iPad Pro (3rd generation). Screenshots for these devices will also be required.

ところで、fastlane deliver では、アップロードするストアスクリーンショットの種別判別に、画像の解像度 (resolution) に基づいて、振り分けを行っている。
そして、12.9-inch iPad Pro (2nd generation)と12.9-inch iPad Pro (3rd generation)は、同じ 2,732 x 2,048ピクセル解像度、264ppi ...
勘のいい皆さんはお気づきでしょう、この状態でアップロードすると、2nd generationのトラックに3rdの分で用意したスクリーンショット画像もアップロードされていた。
-> [deliver] Cannot upload iPad 3rd generation screenshots · Issue #14116 · fastlane/fastlane · GitHub

3rd分だけ手でアップロードするしかなかったし、Appleのレビューはユーザーが使用している端末と違う筐体型のデバイスが写っているスクリーンショットを当て込んでいると、metadata rejectしてくる。

本日、12.9-inch iPad Pro (3rd generation)も区別してアップロードできるようになった fastlane 2.121.0 がリリースされた🎉
https://github.com/fastlane/fastlane/releases/tag/2.121.0

さて、どのように実現されているかを見てみる。
https://github.com/fastlane/fastlane/commit/15c80c227bf249e913a8429520496d3e5ad95a69

To solve this issue 3rd generation screenshots must contain (3rd generation) part in the name of screenshot. *1

...
......
.........
現場からは以上です。

追記

追記2

2.121.1 を使いましょう。

*1: supplyのようにディレクトリを分ける案もあるけど、App Store Connectの場合、スクリーンショットを別端末サイズに流用する機能もあって、大変なんだと思う

キングダム ハーツとyutailang0119

これは何

会社の朝会で話したやつ2
前回

今回はキングダム ハーツの話をした。

本題

ちょっと旬は過ぎましたが、キングダム ハーツⅢ 発売されましたね、めでたいですね。

キングダム ハーツとは

スクエア・エニックスがディスニー協力の元、制作/販売しているRPGゲーム作品、およびそのシリーズ。
ミッキー、ドナルド、グーフィーが武器を振り回したりするゲーム。
KHオリジナルのキャラクターの版権も、全てディズニーが持っている。

ナンバリングタイトル (KH1, KH2, KH3) がソラが主人公の正史、それ以外は外伝という扱い (だったはず)
KH2から3の間に5作の外伝をはさんだもの、正史を13年待った。
ナンバリングタイトル以外もやらないと、普通に話がわからない。

2002年3月28日発売のKHが発売されてから17年、ダークシーカー編という大きな1章が完結したが、まだまだ終わりが見えない。
KH10周年の時に、20周年には完全完結するだろうと言われてたけど、絶対3年後までに次作が出ているのかさえ怪しい

発売順 ≠ 時系列ではなく、さらには同じ名前の人物が複数人いて、輪をかけて混乱する。
でも、KH3までやると、いろいろと繋がるので、ぜひやってほしい。

id:yutailang0119 の時系列順キングダム ハーツ

キングダム ハーツ ファイナル ミックス (KHFM)

2002年12月26日発売
初めてプレイしたキングダム ハーツ
好きなワールドは ハロウィンタウン (ナイトメアー・ビフォア・クリスマス)
発売は2002年12月26日だけど、この頃はPS2は持ってなく、春に新しくクラスが一緒になった友達の家でちょっとずつやって進めた記憶があるので、2003年の4月頃にやったんだと思う。
当時、小学5年生。
ファイナル ミックス (以下、FM) は北米版ベースに追加要素が加わった英語版で、英語のゲームをしているだけでもかっこいい感じがしてた。
ちなみにここまでスクエア発売らしい。 *1
記念すべきKHシリーズ始まりの物語なので、ぜひやってほしい。

キングダム ハーツ チェイン オブ メモリーズ (COM)

2004年11月11日発売
ナンバリングタイトルじゃないけど、これは正史。
好きなワールドはやっぱり ハロウィンタウン (KH1とワールド構成がほぼ一緒) 当時、小学6年生で、ゲームボーイアドバンスは持っていたので、クリスマスに買ってもらった。
KHシリーズとゲームシステムが全然違って、ひどく落胆した覚えがある。
今考えると、ソーシャルゲーム感ある。

自分はKHキャラクターの中で、リクが好きだったので、シナリオはおもしろかったので、ぜひやってほしい。

キングダム ハーツⅡ (KH2)

2005年12月22日発売
好きなワールドは タイムレス・リバー (蒸気船ウィリー)
中学生になっていたので、かなり自由にゲームを買えるようになっていたので、自分で買った。
ゲームシステムがかなり改善されているし、シナリオが本当に泣けます。
裏ボスまで辿りつく気力がなくて、後述のFMでやって驚いた。

ここからKH3の発売を13年待つこととなる。
今なら待たずにKH3までできるので、ぜひやってほしい。

キングダム ハーツ 358/2 Days (Days)

2009年5月30日発売
好きなワールドは トワイライトタウン (KHオリジナル)
ⅩⅢ機関と呼ばれる、前作までの敵視点で進めていくので、ちょっとディズニー感が薄くて、物足りないかも。
DSだったので、高校の休み時間にやってた。
今後のKHシリーズで重要な情報が紛れている可能性があるので、ぜひやってほしい。

キングダム ハーツⅡ ファイナル ミックス+ (KH2FM)

2007年3月29日発売
コンテンツがKH2とCOMのリミックスだったので、2010年くらいに後からやった。
KH2でまた泣いた。
2回KH2で泣きたい人は、ぜひやってほしい。

キングダム ハーツ Re:コーデッド (Re:Coded)

2010年10月7日発売
好きなワールドは 忘却の城 (KHオリジナル) 元々携帯用ゲームで展開していたものシナリオを、DSに移植したもの。
Daysの続きでやってた。
今考えると、受験期によくやれてたと思う。
手軽にできるので、ぜひやってほしい。

キングダム ハーツ バース バイ スリープ ファイナル ミックス (BbS)

2011年1月20日発売
好きなワールドは レイディアントガーデン (KHオリジナル) BbS無印は2010年1月9日に発売されてたんだけど、英語版がやりたくて待ってた。
結果、受験期に直撃して、受験が終わるまで我慢した。
受験が終わってぶっ通しでやって、1日ちょっとで終わらせた。
これも本当におすすめできるので、ぜひやってほしい。

キングダム ハーツ ドリーム ドロップ ディスタンス (DDD)

2012年3月29日発売
好きなワールドは シンフォニー・オブ・ソーサリー (ファンタジ)
ソフトだけ友達に借りてやった。
約10周年のタイトルで、操作性をかなり刷新して賛否両論あるけど、自分はおもしろかった。
シナリオの印象は薄めだったけど、KH3の直前の物語で、やっていてよかったので、ぜひやってほしい。

キングダム ハーツ ユニオン クロス (Uχ)

iOS/Androidソーシャルゲーム
惰性でやってたけど、KH3の腹落ち感のためには必須だったので、ぜひやってほしい。

キングダム ハーツⅢ (KH3)

2019年1月25日発売
全部のワールドがよくできてて好きだけど、1つだけ上げるなら トイボックス (トイ・ストーリー)
最高、待ってた。
本当にダークシーカー編の完結って感じで、とてもよかった。
エピローグとシークレットムービーを見ると、まだまだまだまだ物語は続きそうだなぁとなった。

これのためにPS4 Proと42inch 4Kディスプレイを買った。
買うだけの価値があったので、みんな買って、ぜひやってほしい。

クリアResult

キングダム ハーツ キー バックカバー (χ BC)

実はKH3が終わってから、キングダム ハーツ HD 2.8 ファイナル チャプター プロローグを買って、やっと新規映像特典のχ BCを見た。
KH3やる前に見ておくべきだったので、ぜひ先に見てほしい。

今やるなら

これで全部できる

store.playstation.com

全部はやっている暇はない人向け

www.jp.square-enix.com

で気持ちを高めて、 KINGDOM HEARTS INTEGRUM MASTERPIECE | 公式PlayStation™Store 日本 を気づいたら買ってほしい。

最後に

可愛いドナルドの姿をお楽しみください。 (⚠️KH3の画像を含みます)

挟まるドナルド

*1:この後からはスクエア・エニックス