Sponsor Link
環境&対象
- macOS Big Sur 11.1
- Xcode 12.3
本シリーズ内容
SwiftUI を使って、イメージ処理するアプリを作ります。
以下の理解が進むことがゴールです。
- SwiftUI を使ったアプリ開発
- NSImage を使った画像処理全般
- Photos 拡張編集機能 と SwiftUI app の組み合わせ方
- イメージ処理アプリも TDD で進めることが可能かどうか
- その他 macOS app 開発 Tips
完成イメージ
写真に、メガネのイメージを重ねた写真を作るアプリにしてみます。
以下のような見た目になるイメージ
この記事で作る範囲
まずは、以下の実装をしていきます。
- 写真を表示する
- 写真に重ねて、眼鏡を表示する
- 眼鏡をドラッグできるようにする (-> 次回以降に)
写真を表示する
やること
将来的には、Drag&Drop できるようにしたいですが、まずは、リソースとして保持している写真を表示することにします。
テスト
この状態では、モデルもビューモデルもないので、シンプルに、「起動直後に表示されているイメージの名前が適切か」をテストすることにします。
テストコードとしては、以下のようになるはずです。
func test_appLaunch_initial_imageShouldBeInitialImage() throws {
let app = XCUIApplication()
app.launch()
// (1)
let mainImage = app.images["mainImage"].firstMatch
XCTAssertNotNil(mainImage)
// (2)
XCTAssertEqual(mainImage.label, "initialPhoto")
}
- ID: mainImage を持つイメージを取得
- イメージが表示しているものが initialPhoto であることをテスト
まだ、アプリのコードは書いていないので、当然エラーとなります。(mainImage というイメージが取得できないというエラーになるはずです)
アプリ実装
イメージの表示は、SwiftUI では、Image を使います。初期イメージは、適当(?)なイメージをリソース(Assets.xcassets)に、”initialPhoto” という名前で追加します。
struct ContentView: View {
// (1)
@State private var image:NSImage = NSImage(named: "initialPhoto")!
var body: some View {
// (2)
Image(nsImage: image).resizable().scaledToFit().frame(width: 500, height: 500)
// (3)
.accessibility(identifier: "mainImage")
.padding()
}
}
- 今後、別の写真も表示する予定なので、@State で NSImage を保持するようにします。
- 写真を表示するために、Image を使っています。フレームの大きさは適当に決めました。
- テストで取得できるように AccessibilityID を付与しておきます。
こうすることで、先ほどのテストをパスすることができるようになりました。
メガネの表示
やること
先ほどの写真に重ねて表示するので、ZStack を使います。
メガネの表示は、先ほどの写真と同じです。リソースに追加しておいて、Image を使って表示します。
テスト
写真とメガネの両方が表示されていることをテストします。
起動時のチェックなので、先ほどのテストを拡張します。
func test_appLaunch_initial_imageShouldBeInitialImage() throws {
let app = XCUIApplication()
app.launch()
let mainImage = app.images["mainImage"].firstMatch
XCTAssertNotNil(mainImage)
XCTAssertEqual(mainImage.label, "initialPhoto")
// (1)
let glassImage = app.images["glass"].firstMatch
XCTAssertNotNil(glassImage)
// (2)
XCTAssertEqual(glassImage.label, "glass")
}
- 写真と同じように、ID: glass を持つイメージを取得
- イメージが “glass” を表示していることをテスト
アプリ実装
initialPhoto と同様に、メガネ用のイメージをリソースに追加して表示します。
ZStack を使って initialPhoto に重ねて表示するようにします。
なお、メガネのイメージは、背景を透明にした png ファイルを用意しました。
struct ContentView: View {
@State private var image:NSImage = NSImage(named: "initialPhoto")!
@State private var pos: NSSize = .zero
@State var isDragging = false
@State var dragStartLoc: NSSize = .zero
@State private var grassScale: CGFloat = 0.5 /// 100
var body: some View {
// (1)
ZStack {
Image(nsImage: image).resizable().scaledToFit().frame(width: 500, height: 500, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
.accessibility(identifier: "mainImage")
// (2)
Image(nsImage: NSImage(named: "glass")!)
.resizable().scaledToFit()
// (3)
.accessibility(identifier: "glass")
}
.padding()
}
}
- 重ねて表示するので ZStack を使います
- glass という名前でリソースに保存したイメージを表示します
- glass という ID を付与します
この実装で先ほどのテストをパスします。
リファクタリング
ContentView を使い続けるのは良くない気がするので、MainView に リネームしておきます。
# XCode のリネーム機能を使うと、Struct 名だけでなく、ファイル名や、呼び出し側(App) も修正してくれて便利です。
以下が、ここまでに作ってきたコードです。
//
// PhotoGlassesApp.swift
//
// Created by : Tomoaki Yagishita on 2021/01/19
// © 2021 SmallDeskSoftware
//
import SwiftUI
@main
struct PhotoGlassesApp: App {
var body: some Scene {
WindowGroup {
MainView()
}
}
}
//
// ContentView.swift
//
// Created by : Tomoaki Yagishita on 2021/01/19
// © 2021 SmallDeskSoftware
//
import SwiftUI
import SDSCGExtension
struct MainView: View {
@State private var image:NSImage = NSImage(named: "initialPhoto")!
var body: some View {
ZStack {
Image(nsImage: image).resizable().scaledToFit().frame(width: 500, height: 500)
.accessibility(identifier: "mainImage")
Image(nsImage: NSImage(named: "glass")!)
.resizable().scaledToFit()
.accessibility(identifier: "glass")
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
この記事でできたこと
- JPG/PNG を Image を使って重ねて表示
- Accessibility ID を使って、テストコードから要素を取得し属性をテスト
次回
メガネを Drag で動かせるようにします。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link