SwiftUI を使った イメージ処理アプリを作ってみます
Sponsor Link
環境&対象
以下の環境で動作確認を行なっています。
- macOS Big Sur 11.1
- Xcode 12.3
本シリーズ内容
SwiftUI を使って、イメージ処理するアプリを作ります。
以下の理解が進むことがゴールです。
- SwiftUI を使ったアプリ開発
- NSImage を使った画像処理全般
- Photos 拡張編集機能 と SwiftUI app の組み合わせ方
- イメージ処理アプリも TDD で進めることが可能かどうか
- その他 macOS app 開発 Tips
この記事で作る範囲
前回、写真とメガネを表示するところまでいきました。今回は、以下が目標です。
[SwiftUI][Image] イメージ処理アプリを作る(1)
- メガネをドラッグして、動かせるようにする
ドラッグで移動できるようにする
やること
Drag のイベントは、.gesture を使い、DragGesture で受けます。
テスト
テスト内容が難しいですが、シンプルに 「ドラッグしたら表示位置が変更されている」とします。
そろそろ、UITest が複雑化してきそうなので、PageObject を使ったテストに変更します。
PageObject は、以下の記事でも説明しています。
[SwiftUI][CoreData] SwiftUI と MVVM で始める CoreData 入門 (その6:View の改良)
用意した PageObject は、以下の通りです。
example
//
// PhotoGlassesPageObjects.swift
//
// Created by : Tomoaki Yagishita on 2021/01/19
// © 2021 SmallDeskSoftware
//
import Foundation
import XCTest
// (1)
import SDSCGExtension
// (2)
protocol PageObject {
var app: XCUIApplication { get }
}
// (3)
struct MainViewPageObject: PageObject {
var app: XCUIApplication
init(_ app: XCUIApplication) {
self.app = app
}
// (4)
var mainImage: XCUIElement {
app.images["mainImage"].firstMatch
}
// (5)
var glassImage: XCUIElement {
app.images["glass"].firstMatch
}
// (6)
var glassImageFrame: CGRect {
glassImage.frame
}
// (7)
func dragGlasses(_ from: CGPoint, _ diff:CGSize) {
let start = glassImage.coordinate(withNormalizedOffset: from.cgVector())
let end = start.withOffset(diff.cgVector())
start.press(forDuration: 0.01, thenDragTo: end)
}
}
コード解説
- CGPoint/CGSize/CGVector の相互変換用に作った関数群なので、無視してください
- PageObject を使って複数ページ作るかもしれないので、Protocol を定義してます
- 現在は、1画面しかありませんが、メイン画面を表す PageObject です。
- 写真の Image を返します(厳密には、Image ではなく、Image に該当する XCUIElement です)
- 眼鏡画像の Image を返します
- メガネ画像の frame を返します
- メガネをドラッグするメソッドを提供しています。移動開始位置と移動量を受け取ります
テストコードとしては、以下のようにしました。
test_dragGlass_dragAround_glassShouldBeMoved
func test_dragGlass_dragAround_glassShouldBeMoved() throws {
let app = XCUIApplication()
app.launch()
// (1)
let mainPage = MainViewPageObject(app)
// (2)
let initialFrame = mainPage.glassImageFrame
let moveSize = CGSize(width: 100, height: 200)
// (3)
mainPage.dragGlasses(CGPoint(x: 0, y: 0), moveSize)
// (4)
let movedFrame = mainPage.glassImageFrame
// (5)
XCTAssertEqual(initialFrame.origin.shift(moveSize), movedFrame.origin)
}
コード解説
- 初期ビューの PageObject を取得します
- 移動前の メガネビューの位置を取得します
- ドラッグします
- ドラッグ後の位置を取得します
- ドラッグ後の位置との差分をテストします
まだ、アプリのコードは書いていないので、当然エラーとなります。
アプリ実装
.gesture と DragGesture を使って実装していきます。
DragGesture については、以下の記事で説明しています。
[SwiftUI]Image上でTextをドラッグして動かす(DragGesture使用)
MainView
struct MainView: View {
@State private var image:NSImage = NSImage(named: "initialPhoto")!
// (1)
@State private var pos: NSSize = NSSize(width: 350, height: 200)
@State var isDragging = false
@State var dragStartLoc: NSSize = .zero
// (2)
@State private var grassScale: CGFloat = 0.5
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")
.scaleEffect(grassScale)
// (3)
.position(pos.cgPoint())
// (4)
.gesture(DragGesture()
.onChanged { (gesture) in
if !isDragging {
self.dragStartLoc = pos
isDragging = true
}
pos = self.dragStartLoc.move(gesture.translation)
}
.onEnded { gesture in
self.isDragging = false
})
}
.padding()
}
}
コード解説
- 画面の中央あたりを初期位置に指定しました
- 用意したイメージが大きすぎたので、小さくしました
- pos 変数で 配置する位置を指定します
- DragGesture の中では、移動開始時に 位置を記録しておき、その後移動されるたびに、position を変更します
こうすることで、先ほどのテストをパスすることができるようになりました。
この記事でできたこと
- ドラッグ操作で、イメージの位置を変更できるようにした
次回
メガネを、Drag で拡大縮小できるようにします。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link