NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

【SwiftUI】Pixel Artアプリを作ってみる-その2

概要

【SwiftUI】ForEachでPixel Artアプリを作ってみるの続きとなります。
今回はColorPickerを使用して任意の色を選択して着色できる方法を紹介しつつ、サンプルアプリを昇華していく様子をご紹介したいと思います。

環境

この記事は以下のバージョン環境のもと作成されたものです。
【Xcode】14.0.1
【iOS】16.0.2
【macOS】Monterey バージョン 12.5

きっかけ

前回「 【SwiftUI】ForEachでPixel Artアプリを作ってみると言う記事を書かせていただきました!
その記事を見ていただいた弊社のSlackで"サムネイルのモナリザ作ったのかと思った"と言う反応がありました。
いや、自分も思いましたよ。「これは期待させてしまうのではw」と。
ただ「いやいや2日ぐらいあったらこんなの速攻作れちゃうよ?」
と言うことで有言実行することに。w

追加、修正機能

前回の記事で作ったのサンプルアプリは十字キーもどきで操作して、ボタンをタップすると赤色がつけれるレベルのサンプルでした。
ここから追加及び修正機能として以下行っていき、サムネイルのモナリザを作れるように仕上げていきたいと思います。

  • Pixelのコンポーネントをレゴ部品を上から見たようなものに変更
  • Pixel選択の操作は十字キーもどきからタップで操作できるように変更
  • 着色する色を選択できる機能を追加

その他細々した設定は後で実装するとして、とりあえずここまで作ってみることに!

Pixelのコンポーネント作成

とりあえず四角でもよかったのですが、サムネイルのモナリザはレゴの部品みたいなデザインになっているのでとりあえず四角の中に円を作成し、ちょっとシャドウなど着けてそれっぽく仕上げていくことに。

struct BlockView: View {
    @State var selectedColor: Color
    var body: some View {
        ZStack {
            Rectangle()
                .frame(width: 200, height: 200)
                .foregroundColor(selectedColor)
                .background(
                    Rectangle()
                        .stroke(lineWidth: 0.5)
                )
            Rectangle()
                .frame(width: 200, height: 200)
                .foregroundColor(.black.opacity(0.2))
            
            Circle()
                .frame(width: 180, height: 180)
                .foregroundColor(selectedColor)
                .shadow(color: .black.opacity(0.6), radius: 10, x: -10, y: 15)
                .shadow(color: .white.opacity(0.8), radius: 3, x: 3, y: -3 )
        }
    }
}

サムネイルのモナリザをよくみると円柱になっており、それぞれ角度も変わっています。
今回はそこまでこだわらなくても良いかなと思い一旦このコンポーネントを使用していきます!

タップ動作でPixelを選択する

前回の記事では十字キー操作でPixelを選択操作していました。

今回は十字キーを廃止してタップ操作でPixelの選択操作を行うよう修正していきます。
修正方法はBlockViewに対して.onTapGestureでタップした時の行数と列数を取得するだけです。

.onTapGesture {
    topViewModel.selectedRow = row
    topViewModel.selectedColumn = column
}

これでタップ動作でPixelの選択ができ、ボタンをタップで着色ができました!

任意の色を着色できる

SwiftUIではiOS14からColorPickerが使用できます。
ColorPickerを使用する事で簡単に様々な任意の色を取得することができます。

ColorPicker("色を選択", selection: $topViewModel.blockColor)

たった1行でここまでの機能を追加することができます。
またColorPickerで選択した色をPixelごとに保存できるようstructを作っておきます。

struct IndexData: Identifiable {
    var id: String
    var row: Int
    var column: Int
    var color: Color
}

これで任意の色を取得して、Pixel事に色も保存できる準備ができたので、ボタンをタップしたタイミングでPixelにデータがあれば更新、なければ追加する処理を行います。

Button {
    if topViewModel.saveIndex.contains(where: { indexData in
        indexData.id == topViewModel.rowAndColumnToStringConverter(
            row: topViewModel.selectedRow,
            column: topViewModel.selectedColumn
        )}) {
        topViewModel.saveIndex[
            topViewModel.saveIndex.firstIndex(where: { indexData in
                indexData.id == topViewModel.rowAndColumnToStringConverter(
                    row: topViewModel.selectedRow,
                    column: topViewModel.selectedColumn
                )
            }) ?? 0] = IndexData(
                id: topViewModel.rowAndColumnToStringConverter(
                    row: topViewModel.selectedRow,
                    column: topViewModel.selectedColumn),
                row: topViewModel.selectedRow,
                column: topViewModel.selectedColumn,
                color: topViewModel.blockColor
            )
    } else {
        topViewModel.saveIndex.append(
            IndexData(
                id:topViewModel.rowAndColumnToStringConverter(
                    row: topViewModel.selectedRow,
                    column: topViewModel.selectedColumn),
                row: topViewModel.selectedRow,
                column: topViewModel.selectedColumn,
                color: topViewModel.blockColor
            ))
    }
} label: {
    Text("tap!")
        .frame(width: 100, height: 100)
        .background(
            Circle()
                .stroke(lineWidth: 1)
        )
}

idについては任意のidを返すconverterメソッド作って使用しているので、適当に作っていただければと思います!
if文ではsaveIndexに配列としてIndexDataに準拠した値を保存しています。その際、配列に対して既に任意の色が保存されているのか新規で追加なのか判定して値を保存する処理を行なっています。
サンプルとして掲載しておきます。

func rowAndColumnToStringConverter(row: Int, column: Int) -> String {
    return "r" + String(row) + "c" + String(column)
}

これで任意の色で各Pixelを着色することができたので完成です!

おまけ

当初の目標としては果たしたのですが、どうせなら写真から色を抽出して、Pixel Artに変換するGeneratorのような機能も欲しいなという事で作ってみました!
(突貫で作ったので作り方に関しては後日整理して改めてご紹介したいと思います)

作り方の工程としては

  • 任意の写真から任意のPixel範囲の色を抽出
  • 抽出した色を保存
  • Pixel Art画面に色を渡して反映する

これだけです。
任意の写真から任意の範囲を切り抜きするところまではスムーズにできましたが、切り抜きしたImageのデータから平均色に変更する部分で少し手詰まり、こちらの記事を参考にさせていただきました!
これで写真からもPixel Artを生成することができ、更に編集もできるアプリになりました!

原画と変換後(解像度別)の様子

編集している様子

まとめ

とにかくiOS14から使用できるColorPickerが強力でした。
また当然ですが、ForEachでViewを生成するとその分動作に影響するので今回のようなマスを生成するのはあまり向いていないと感じました。
15×15ぐらいまではいいですが、それ以上になると重く、もっさりとした動作になります。
このあたり解消方法はまた探っていきたいと思います。
またもう少し機能追加してちゃんとしたアプリとしてリリースしてもよさそうなので気が向いたらします!
その時は是非使用していただけると嬉しいです!

執筆者岡優志

iOSエンジニア
iOSを専門とし、モバイルアプリの開発を行なっています。

Twitter