[SwiftUI][Image] イメージ処理アプリを作る(2)

SwiftUI

SwiftUI を使った イメージ処理アプリを作ってみます

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Big Sur 11.1
  • Xcode 12.3

本シリーズ内容

SwiftUI を使って、イメージ処理するアプリを作ります。

以下の理解が進むことがゴールです。

  • SwiftUI を使ったアプリ開発
  • NSImage を使った画像処理全般
  • Photos 拡張編集機能 と SwiftUI app の組み合わせ方
  • イメージ処理アプリも TDD で進めることが可能かどうか
  • その他 macOS app 開発 Tips

この記事で作る範囲

前回、写真とメガネを表示するところまでいきました。今回は、以下が目標です。

SwiftUI[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)
    }
    
}
コード解説
  1. CGPoint/CGSize/CGVector の相互変換用に作った関数群なので、無視してください
  2. PageObject を使って複数ページ作るかもしれないので、Protocol を定義してます
  3. 現在は、1画面しかありませんが、メイン画面を表す PageObject です。
  4. 写真の Image を返します(厳密には、Image ではなく、Image に該当する XCUIElement です)
  5. 眼鏡画像の Image を返します
  6. メガネ画像の frame を返します
  7. メガネをドラッグするメソッドを提供しています。移動開始位置と移動量を受け取ります

テストコードとしては、以下のようにしました。

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)
    }
コード解説
  1. 初期ビューの PageObject を取得します
  2. 移動前の メガネビューの位置を取得します
  3. ドラッグします
  4. ドラッグ後の位置を取得します
  5. ドラッグ後の位置との差分をテストします

まだ、アプリのコードは書いていないので、当然エラーとなります。

アプリ実装

.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()
    }
}
コード解説
  1. 画面の中央あたりを初期位置に指定しました
  2. 用意したイメージが大きすぎたので、小さくしました
  3. pos 変数で 配置する位置を指定します
  4. DragGesture の中では、移動開始時に 位置を記録しておき、その後移動されるたびに、position を変更します

こうすることで、先ほどのテストをパスすることができるようになりました。

この記事でできたこと

  • ドラッグ操作で、イメージの位置を変更できるようにした

次回

メガネを、Drag で拡大縮小できるようにします。

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です