macOS   SwiftUIプログラミング   ドラッグで図形を描画する

ホーム

【お知らせ】
SwiftUIで作った macOS Todo アプリ ToDone を100ダウンロードまで無料にしました。マニュアルページは、ToDone サポートページ です。

【本文】
将来的には、ペイントを簡単にしたようなグラフィックエディターを作りたい」の第2段です。今回はドラッグで図形を描いてみたいと思います。かなり難しかったです。

このコーナーでは、任意のテキストエディタでコードを記述し、 ターミナルを使ってビルドする方法で作業を進めています。Xcode をお使いの場合は、Xcodeで作業する場合 をご一読ください。

なお、ターミナルを使う場合も、Swift コンパイラや SwiftUI フレームワークなどを Mac にインストールするために Xcode をインストールして、一度起動させなければなりません。インストールが終われば Xcode は終了しても大丈夫です。

また、作成したアプリケーションを App Storeに提出するためのファイルにするには、 Xcode を使わなければならなかったかもしれません。 どこかで Xcode を使わずに作る方法を見たような気もするのですが...

私の開発環境は次のとおりです。

  • MacBook Air 2018年モデル、メモリ8G
  • macOS Monterey 12.4
  • Xcode 13.4.1
  • Swift 5.6.1

Drag Gesture

今回も DragGesture を使います。


App.swift

Xcode では、プロダクト名App.swift になります。


import SwiftUI

@main

struct FooApp: App {
    init() {
        // Tab(タブ)に関するメニューを削除するために次のコードを追加しました。
        NSWindow.allowsAutomaticWindowTabbing = false
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .commands(content: {
            // New Window メニューを削除するために次のコードを追加しました。
            CommandGroup(replacing: .newItem) {}
        })
    }
}
	


View.swift

Xcode では、ContentView.swift になります。


/************************************
    View.swift
    copyright    :   vivacocoa.jp
    last modified:   Aug. 01, 2022
************************************/

import SwiftUI

struct ContentView: View {
    @State var isDrag = false
    @State var location = CGPoint(x: 0.0, y: 0.0)
    @State var startLocation = CGPoint(x: 0.0, y: 0.0)
    @State var w = 0.0
    @State var h = 0.0
    
    var drag: some Gesture {
        DragGesture()
            .onChanged({
                value in
                isDrag = true
                startLocation = value.startLocation
                location = value.location
                w = location.x - startLocation.x
                h = location.y - startLocation.y
            })
            .onEnded({_ in
                isDrag = false
            })
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(.white)
                .frame( minWidth: 300, maxWidth: .infinity,
                        minHeight: 300, maxHeight: .infinity,
                        alignment: .center)
                .gesture(drag)
            Rectangle()
                //.fill( isDrag ? .red : .blue )
                .fill( .blue )
                .frame(width: abs(w), height: abs(h))
                .position( CGPoint( x: w / 2 + startLocation.x,
                                    y: h / 2 + startLocation.y ))
        }
        .onDisappear() { NSApplication.shared.terminate(self)}
        
    }
}
	


アプリケーションバンドル

Xcode で作業する場合は、この部分は無視してください。 ターミナルで作業する場合は「アイコン」を参考にしてアプリケーションバンドルを作ってください。 アプリケーションの名前を変えるために、Info.plist の CFBundleName を書き換えます。ここでは「Draw」という名前にしました。

アイコンは次のサイトから .icns ファイルをダウンロードしました。
フリーアイコンSVG、PNG、ICO、ICNS

Info.plist


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleExecutable</key>
	<string>foo</string>
	<key>CFBundleIconFile</key>
	<string>icon-macbook.icns</string>
	<key>CFBundleName</key>
	<string>Draw</string>
</dict>
</plist>
    


ビルド

Xcode で作業している場合は、▶︎(ビルドボタン)をクリックするか、⌘Rを押してください。ターミナルでビルドする場合は次のようにします。


swiftc App.swift View.swift -o foo
mv foo Foo.app/Contents/MacOS
    

実行

ウィンドウの中をドラッグすると青い四角が描けます。

図形を重ねて複数描けるようにする

View.swift

Xcode では、ContentView.swift になります。App.swift(プロダクト名App.swift)は、前述と同じです。


/************************************
    View.swift
    copyright    :   vivacocoa.jp
    last modified:   Aug. 03, 2022
************************************/
import SwiftUI

struct Shape: Hashable {
    var sx = 0.0
    var sy = 0.0
    var sw = 0.0
    var sh = 0.0
}

struct ContentView: View {
    @State var shapes:[Shape] = []
    @State var isDrag = false
    @State var location: CGPoint = .zero
    @State var startLocation: CGPoint = .zero
    @State var w = 0.0
    @State var h = 0.0
    
    var drag: some Gesture {
        DragGesture()
            .onChanged({
                value in
                isDrag = true
                startLocation = value.startLocation
                location = value.location
                w = location.x - startLocation.x
                h = location.y - startLocation.y
                //shapes.append(Shape(sx: 0.0, sy: 0.0, sw: w, sh: h))
            })
            .onEnded({value in
                isDrag = false
                shapes.append(Shape(sx: w / 2 + value.startLocation.x,
                                    sy: h / 2 + value.startLocation.y,
                                    sw: abs(w),
                                    sh: abs(h)))
            })
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(.white)
                .frame( minWidth: 300, maxWidth: .infinity,
                        minHeight: 300, maxHeight: .infinity,
                        alignment: .center)
            
            ForEach(shapes, id:\.self) { shape in
                Rectangle()
                    .stroke( .black, lineWidth: 1)
                    .background(.blue)
                    .frame(width: abs(shape.sw), height: abs(shape.sh))
                    .position( CGPoint( x: shape.sx,
                                        y: shape.sy))
            }
            
            Rectangle()
                .stroke( isDrag ? .black : .clear)
                .background( isDrag ? .blue : .clear)
                .frame(width: abs(w), height: abs(h))
                .position( CGPoint( x: w / 2 + startLocation.x,
                                    y: h / 2 + startLocation.y ))
        }
        .gesture(drag)
        .onDisappear() { NSApplication.shared.terminate(self)}
    }
}
    

クリックした図形を一番手前にする

View.swift

Xcode では、ContentView.swift になります。App.swift(プロダクト名App.swift)は、前述と同じです。


/************************************
    View.swift
    copyright    :   vivacocoa.jp
    last modified:   Aug. 03, 2022
************************************/
import SwiftUI

struct Shape: Hashable {
    var sx = 0.0
    var sy = 0.0
    var sw = 0.0
    var sh = 0.0
}

struct ContentView: View {
    @State var shapes:[Shape] = []
    @State var isDrag = false
    @State var location: CGPoint = .zero
    @State var startLocation: CGPoint = .zero
    @State var w = 0.0
    @State var h = 0.0
    
    var drag: some Gesture {
        DragGesture()
            .onChanged({
                value in
                isDrag = true
                startLocation = value.startLocation
                location = value.location
                w = location.x - startLocation.x
                h = location.y - startLocation.y
                //shapes.append(Shape(sx: 0.0, sy: 0.0, sw: w, sh: h))
            })
            .onEnded({value in
                isDrag = false
                shapes.append(Shape(sx: w / 2 + value.startLocation.x,
                                    sy: h / 2 + value.startLocation.y,
                                    sw: abs(w),
                                    sh: abs(h)))
            })
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(.white)
                .frame( minWidth: 300, maxWidth: .infinity,
                        minHeight: 300, maxHeight: .infinity,
                        alignment: .center)
            
            ForEach(Array(shapes.enumerated()), id: \.offset) { i, shape in
                Rectangle()
                    .stroke( .black, lineWidth: 1)
                    .background(.blue)
                    .frame(width: abs(shape.sw), height: abs(shape.sh))
                    .position( CGPoint( x: shape.sx, y: shape.sy))
                    .onTapGesture(perform: {
                        let elm = shapes.remove(at: i)
                        shapes.append(elm)
                    })
            }
            
            Rectangle()
                .stroke( isDrag ? .black : .clear)
                .background( isDrag ? .blue : .clear)
                .frame(width: abs(w), height: abs(h))
                .position( CGPoint( x: w / 2 + startLocation.x,
                                    y: h / 2 + startLocation.y ))
        }
        .gesture(drag)
        .onDisappear() { NSApplication.shared.terminate(self)}
    }
}
    

コンテキストメニューで操作する

Xcode では、ContentView.swift になります。App.swift(プロダクト名App.swift)は、前述と同じです。


/************************************
    View.swift
    copyright    :   vivacocoa.jp
    last modified:   Aug. 03, 2022
************************************/
import SwiftUI

struct Shape: Hashable {
    var sx = 0.0
    var sy = 0.0
    var sw = 0.0
    var sh = 0.0
}

struct ContentView: View {
    @State var shapes:[Shape] = []
    @State var isDrag = false
    @State var location: CGPoint = .zero
    @State var startLocation: CGPoint = .zero
    @State var w = 0.0
    @State var h = 0.0
    
    var drag: some Gesture {
        DragGesture()
            .onChanged({
                value in
                isDrag = true
                startLocation = value.startLocation
                location = value.location
                w = location.x - startLocation.x
                h = location.y - startLocation.y
            })
            .onEnded({value in
                isDrag = false
                shapes.append(Shape(sx: w / 2 + value.startLocation.x,
                                    sy: h / 2 + value.startLocation.y,
                                    sw: abs(w), sh: abs(h)))
            })
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(.white)
                .frame( minWidth: 300, maxWidth: .infinity,
                        minHeight: 300, maxHeight: .infinity,
                        alignment: .center)
            
            ForEach(Array(shapes.enumerated()), id: \.offset) { i, shape in
                Rectangle()
                    .stroke( .black, lineWidth: 1)
                    .background(.blue)
                    .frame(width: abs(shape.sw), height: abs(shape.sh))
                    .position( CGPoint( x: shape.sx, y: shape.sy))
                    /*
                    .onTapGesture(perform: {
                        let elm = shapes.remove(at: i)
                        shapes.append(elm)
                    })
                     */
                    .contextMenu(menuItems:{
                                // メニューアイテムは Button で設定します。
                                Button(action:{
                                    if i < shapes.count - 1 {
                                        let elm = shapes.remove(at: i)
                                        shapes.insert(elm, at: i + 1)
                                    }
                                    }){Text("Bring one forward")}
                                Button(action:{
                                    if i > 0 {
                                        let elm = shapes.remove(at: i)
                                        shapes.insert(elm, at: i - 1)
                                    }
                                }){Text("Put one behind")}
                                /*
                                Button(action:{
                                }){Text("Move")}
                                 */
                                Button(action:{
                                    shapes.remove(at: i)
                                }){Text("Delete")}
                    })
            }
            
            Rectangle()
                .stroke( isDrag ? .black : .clear)
                .background( isDrag ? .blue : .clear)
                .frame(width: abs(w), height: abs(h))
                .position( CGPoint( x: w / 2 + startLocation.x,
                                    y: h / 2 + startLocation.y ))
        }
        .gesture(drag)
        .onDisappear() { NSApplication.shared.terminate(self)}
    }
}
    




5977 visits
Posted: Aug. 01, 2022
Update: Aug. 03, 2022

ホーム   目次