【お知らせ】
SwiftUIで作った macOS Todo アプリ
ToDone
を100ダウンロードまで無料にしました。マニュアルページは、ToDone サポートページ です。
【本文】
今までは、ビュー(View、UI部品)をレイアウトするのに、VStack や HStack
使ってきました。このように View をレイアウトするものをコンテナ(container)と言います。この章では Table(テーブル)いうコンテナを説明します。なおコンテナも View の一つです。
このコーナーでは、任意のテキストエディタでコードを記述し、 ターミナルを使ってビルドする方法で作業を進めています。Xcode をお使いの場合は、Xcodeで作業する場合 をご一読ください。
なお、ターミナルを使う場合も、Swift コンパイラや SwiftUI フレームワークなどを Mac にインストールするために Xcode をインストールして、一度起動させなければなりません。インストールが終われば Xcode は終了しても大丈夫です。
また、作成したアプリケーションを App Storeに提出するためのファイルにするには、 Xcode を使わなければならなかったかもしれません。 どこかで Xcode を使わずに作る方法を見たような気もするのですが...
私の開発環境は次のとおりです。
SwiftUI の Table は、macOS 12.0 以降で動作します。
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 になります。
import SwiftUI
struct ContentView: View {
struct Student: Identifiable {
let name: String
let grade: String
let score: Int
let id = UUID()
}
private var students = [
Student(name: "Bob", grade: "B", score: 75),
Student(name: "Dave", grade: "D", score: 55),
Student(name: "Andy", grade: "A", score: 90),
Student(name: "Cathy", grade: "C", score: 60)
]
var body: some View {
Table(students) {
TableColumn("Name", value:\.name)
TableColumn("Grade", value:\.grade)
TableColumn("Score") { item in Text(String(item.score))}
}
// ウィンドウの大きさを設定しています
.frame(minWidth: 500, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
// ウィンドウを閉じたらアプリケーションも終了するようにしています
.onDisappear(){ NSApplication.shared.terminate(self) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Xcode で作業する場合は、この部分は無視してください。 ターミナルで作業する場合は「アイコン」を参考にしてアプリケーションバンドルを作ってください。 アプリケーションの名前を変えるために、Info.plist の CFBundleName を書き換えます。ここでは「Table」という名前にしました。
アイコンは次のサイトから .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>Table</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)は、前述と同じです。
import SwiftUI
struct ContentView: View {
struct Student: Identifiable {
let name: String
let grade: String
let score: Int
let id = UUID()
}
private var students = [
Student(name: "Bob", grade: "B", score: 75),
Student(name: "Dave", grade: "D", score: 55),
Student(name: "Andy", grade: "A", score: 90),
Student(name: "Cathy", grade: "C", score: 60)
]
// 単一行を選択できるようにするには次のプロパティを宣言します
@State private var selectedStudent:Student.ID? = nil
// 複数行を選択できるようにするには次のプロパティを宣言します
//@State private var selectedStudent = Set<Student.ID>()
var body: some View {
// Table の引数に selection: を追加します
Table(students, selection: $selectedStudent) {
TableColumn("Name", value:\.name)
TableColumn("Grade", value:\.grade)
TableColumn("Score") { item in Text(String(item.score))}
}
.frame(minWidth: 500, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.onDisappear(){ NSApplication.shared.terminate(self) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Xcode では、ContentView.swift になります。App.swift(プロダクト名App.swift)は、前述と同じです。
/*
struct Student と private var students を
struct ContentView の外(グローバルの位置)に出しています。
*/
import SwiftUI
struct Student: Identifiable {
let name: String
let grade: String
let score: Int
let id = UUID()
}
private var students = [
Student(name: "Bob", grade: "B", score: 75),
Student(name: "Dave", grade: "D", score: 55),
Student(name: "Andy", grade: "A", score: 90),
Student(name: "Cathy", grade: "C", score: 60)
]
struct ContentView: View {
// 単一行を選択できるようにするには、次のプロパティを宣言します
@State private var selectedStudent:Student.ID? = nil
// 複数行を選択できるようにするには、次のプロパティを宣言します
//@State private var selectedStudent = Set<Student.ID>()
// ソートできるようにするには、次のプロパティを宣言します
// 末尾の score は、name でも、grade でも良いです、動作は同じです
@State private var sortOrder = [KeyPathComparator(\Student.score)]
var body: some View {
// Table の引数に sortOrder: を追加します
Table(students, selection: $selectedStudent, sortOrder: $sortOrder) {
TableColumn("Name", value:\.name)
TableColumn("Grade", value:\.grade)
TableColumn("Score") { item in Text(String(item.score))}
}
// 次の .onChange モディファイアがソートを実行しています
.onChange(of: sortOrder) { students.sort(using: $0) }
.frame(minWidth: 500, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.onDisappear(){ NSApplication.shared.terminate(self) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Name と Grade をクリックすると、項目が昇順、降順にソートされます。score のところは、残念ながらクリックに反応しません。
Xcode では、ContentView.swift になります。App.swift(プロダクト名App.swift)は、前述と同じです。
/*
struct Student と private var students を
struct ContentView の外(グローバルの位置)に出しています。
*/
import SwiftUI
struct Student: Identifiable {
let name: String
let grade: String
let score: Int
let id = UUID()
}
private var students = [
Student(name: "Bob", grade: "B", score: 75),
Student(name: "Dave", grade: "D", score: 55),
Student(name: "Andy", grade: "A", score: 90),
Student(name: "Cathy", grade: "C", score: 60)
]
struct ContentView: View {
// 単一行を選択できるようにするには、次のプロパティを宣言します
@State private var selectedStudent:Student.ID? = nil
// 複数行を選択できるようにするには、次のプロパティを宣言します
//@State private var selectedStudent = Set()
// ソートできるようにするには、次のプロパティを宣言します
// 末尾の score は、name でも、grade でも良いです、動作は同じです
@State private var sortOrder = [KeyPathComparator(\Student.score)]
var body: some View {
// Table の引数に sortOrder: を追加します
Table(students, selection: $selectedStudent, sortOrder: $sortOrder) {
// Name と Score 項目をボールドに変えています
TableColumn("Name" ) { item in Text(item.name).bold() }
TableColumn("Grade", value:\.grade)
TableColumn("Score") { item in Text(String(item.score)).bold() }
}
// 次の .onChange モディファイアがソートを実行しています
.onChange(of: sortOrder) { students.sort(using: $0) }
.frame(minWidth: 500, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.onDisappear(){ NSApplication.shared.terminate(self) }
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
項目の表示をボールドしたカラム(列)は、ヘッダをクリックしてもソートできません。表示内容をクロージャーで呼び出すとソートできなくなるみたいです。