【お知らせ】
SwiftUIで作った macOS Todo アプリ
ToDone
を100ダウンロードまで無料にしました。マニュアルページは、ToDone サポートページ です。
【本文】
「将来的には、ペイントを簡単にしたようなグラフィックエディターを作りたい」の第2段です。今回はドラッグで図形を描いてみたいと思います。かなり難しかったです。
このコーナーでは、任意のテキストエディタでコードを記述し、 ターミナルを使ってビルドする方法で作業を進めています。Xcode をお使いの場合は、Xcodeで作業する場合 をご一読ください。
なお、ターミナルを使う場合も、Swift コンパイラや SwiftUI フレームワークなどを Mac にインストールするために Xcode をインストールして、一度起動させなければなりません。インストールが終われば Xcode は終了しても大丈夫です。
また、作成したアプリケーションを App Storeに提出するためのファイルにするには、 Xcode を使わなければならなかったかもしれません。 どこかで Xcode を使わずに作る方法を見たような気もするのですが...
私の開発環境は次のとおりです。
今回も DragGesture を使います。
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) {}
})
}
}
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
<?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
ウィンドウの中をドラッグすると青い四角が描けます。
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)}
}
}
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)}
}
}