Sponsor Link
環境&対象
- macOS Big Sur 11.1
- Xcode 12.3
本シリーズ内容
SwiftUI を使って、イメージ処理するアプリを作ります。
以下の理解が進むことがゴールです。
- SwiftUI を使ったアプリ開発
- NSImage を使った画像処理全般
- Photos 拡張編集機能 と SwiftUI app の組み合わせ方
- イメージ処理アプリも TDD で進めることが可能かどうか
- その他 macOS app 開発 Tips
この記事で作る範囲
前々回、前回 で イメージを表示して、ドラッグにより移動させることをできるようにしました。
写真によっては、メガネの大きさを調整したくなると思うので、ドラッグにより大きさを調整できるようにしてみます。
- ドラッグ操作で、大きさを変更する
ドラッグ操作で大きさを変更する
やること
Drag での移動量に応じて、Image の大きさを調整することにします。
今の実装は、メガネのイメージのドラッグは、移動になっていますので、メガネのイメージ上に別のイメージを配置し、そのイメージをドラッグされたら、大きさを調整することにします。
実装終了時のイメージは、以下のような動作です。
テスト
以下をテスト項目としました。
「右側のレンズ部分を右にドラッグすることで、幅が広がること」
「左側のレンズ部分を上にドラッグすることで、高さが広がること」
まずは、PageObject にレンズ部分のドラッグ操作を追加しないといけません。
レンズ部分のイメージを取得できるように、glassRightLens と glassLeftLens を用意し、操作用のメソッドとして、dragRightLens, dragLeftLens を追加しました。
//
// PhotoGlassesPageObjects.swift
//
// Created by : Tomoaki Yagishita on 2021/01/19
// © 2021 SmallDeskSoftware
//
import Foundation
import XCTest
import SDSCGExtension
protocol PageObject {
var app: XCUIApplication { get }
}
struct MainViewPageObject: PageObject {
var app: XCUIApplication
init(_ app: XCUIApplication) {
self.app = app
}
var mainImage: XCUIElement {
app.images["mainImage"].firstMatch
}
var glassImage: XCUIElement {
app.images["glass"].firstMatch
}
var glassImageFrame: CGRect {
glassImage.frame
}
var glassRightLens: XCUIElement {
app.images["rightLens"].firstMatch
}
var glassLeftLens: XCUIElement {
app.images["leftLens"].firstMatch
}
func dragGlasses(_ from: CGVector, _ diff:CGSize) {
let start = glassImage.coordinate(withNormalizedOffset: from)
let end = start.withOffset(diff.cgVector())
start.press(forDuration: 0.01, thenDragTo: end)
}
func dragRightLens(_ offset: CGVector, _ diff:CGVector) {
let start = glassRightLens.coordinate(withNormalizedOffset: offset)
let end = start.withOffset(diff)
start.press(forDuration: 0.01, thenDragTo: end)
}
func dragLeftLens(_ offset: CGVector, _ diff:CGVector) {
let start = glassRightLens.coordinate(withNormalizedOffset: offset)
let end = start.withOffset(diff)
start.press(forDuration: 0.01, thenDragTo: end)
}
}
テストコードとしては、以下のようになるはずです。
func test_dragGlassForResize_makeGlassWider_shouldBecomeWider() throws {
let app = XCUIApplication()
app.launch()
let mainPage = MainViewPageObject(app)
let initialFrame = mainPage.glassImageFrame
let dragVector = CGVector(dx: 100, dy: 0)
mainPage.dragRightLens(CGVector(dx: 0.5, dy: 0.5), dragVector)
let resizedFrame = mainPage.glassImageFrame
XCTAssertTrue(initialFrame.width resizedFrame.width)
}
func test_dragGlassForResize_makeGlassTaller_shouldBecomeTaller() throws {
let app = XCUIApplication()
app.launch()
let mainPage = MainViewPageObject(app)
let initialFrame = mainPage.glassImageFrame
let dragVector = CGVector(dx: 0, dy: -50)
mainPage.dragLeftLens(CGVector(dx: 0.5, dy: 0.5), dragVector)
let resizedFrame = mainPage.glassImageFrame
XCTAssertTrue(initialFrame.height resizedFrame.height)
}
当初、厳密なサイズ調整結果をテストしようと考えていたのですが、XCUICoordinate の扱いが一部わからず、上記のように、幅が広がっていること、高さが高くなっていること をテスト項目にしました。
アプリ実装
レンズ部分に、丸を表示して、その丸がドラッグされたら、大きさを調整することにします。
上下方向には、上にドラッグしたら大きく、下にドラッグしたら小さくする としました。
左右方向には、メガネの中心から離れる方にドラッグしたら大きく、中心方向にドラッグしたら小さくする としました。
仕様がきまれば、メガネの移動と同じように実装するだけです。
メガネイメージと操作用のビューを配置することを想定しているため、GlassImage という別ビューを作成してその中に実装することとしました。
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")
GlassImage()
}
.padding()
}
}
GlassImage を以下のように実装します。
//
// GlassImage.swift
//
// Created by : Tomoaki Yagishita on 2021/01/19
// © 2021 SmallDeskSoftware
//
import SwiftUI
struct GlassImage: View {
@State private var pos: NSSize = .zero
@State private var glassSize: NSSize = NSSize(width: 300, height: 150)
@State private var isGlassDragging = false
@State private var glassDragStartLoc: NSSize = .zero
@State private var isEyeDragging = false
@State private var eyeDragStartSize:NSSize = .zero
@State private var eyeSignOpacity:Double = 0.01
var body: some View {
// (1)
ZStack {
Image(nsImage: NSImage(named: "glass")!)
.resizable()
.frame(width: glassSize.width, height: glassSize.height)
.accessibility(identifier: "glass")
.offset(pos)
.gesture(DragGesture()
.onChanged { (gesture) in
if !isGlassDragging {
self.glassDragStartLoc = pos
isGlassDragging = true
}
pos = self.glassDragStartLoc.move(gesture.translation)
}
.onEnded { gesture in
self.isGlassDragging = false
})
// (2)
Circle()
.foregroundColor(Color.red.opacity(eyeSignOpacity))
.frame(width: glassSize.width / 4, height: glassSize.height/2)
.offset(x: -1 * glassSize.width / 4 + pos.width, y: 0 + pos.height)
.accessibility(identifier: "leftLens")
.gesture(DragGesture()
.onChanged { gesture in
if !isEyeDragging {
self.isEyeDragging = true
// (3)
self.eyeSignOpacity = 0.8
self.eyeDragStartSize = self.glassSize
}
// (4)
guard (self.eyeDragStartSize.width - gesture.translation.width > 0) else { return }
guard (self.eyeDragStartSize.height - gesture.translation.height > 0) else { return }
// (5)
glassSize.width = self.eyeDragStartSize.width - gesture.translation.width
glassSize.height = self.eyeDragStartSize.height - gesture.translation.height
}
.onEnded { gesture in
self.isEyeDragging = false
// (6)
self.eyeSignOpacity = 0.01
})
// (7)
Circle()
.foregroundColor(Color.red.opacity(eyeSignOpacity))
.frame(width: glassSize.width / 4, height: glassSize.height/2)
.offset(x: +1 * glassSize.width / 4 + pos.width, y: 0 + pos.height)
.accessibility(identifier: "rightLens")
.gesture(DragGesture()
.onChanged { gesture in
if !isEyeDragging {
self.isEyeDragging = true
self.eyeSignOpacity = 0.8
self.eyeDragStartSize = self.glassSize
}
guard (self.eyeDragStartSize.width + gesture.translation.width > 0) else { return }
guard (self.eyeDragStartSize.height - gesture.translation.height > 0) else { return }
glassSize.width = self.eyeDragStartSize.width + gesture.translation.width
glassSize.height = self.eyeDragStartSize.height - gesture.translation.height
}
.onEnded { gesture in
self.isEyeDragging = false
self.eyeSignOpacity = 0.01
})
}
}
}
- ZStack を使って、メガネのイメージ上に重ねて表示します
- Circle を使って、ドラッグする対象を作成します(左側レンズ分)
- ドラッグするときだけ、Opacity を上げて 見易くします(ドラッグしていないときはほとんど透明にします)
- ドラッグ操作によって、小さくしすぎてしまうことを防ぎます
- ドラッグ操作の移動量に応じて、メガネのイメージサイズを変更します
- ドラッグ操作が終わった時に、Opacity を下げて、透明にします(0 に設定すると、ドラッグできなくなってしまうので、0.01 にしてます)
- 右側のレンズ部分も同じです
# 配置に使用している frame や offset の値は、調整した数値です
この実装で、先ほどのテストをパスすることができます。
この実装で、この記事冒頭のアプリケーションの動きとなります。
この記事でできたこと
- ドラッグ操作で、イメージのサイズを変更できるようにした
次回
別のイメージを Drag&Drop で表示できるようにします。
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link