【お知らせ】
SwiftUIで作った macOS Todo アプリ
ToDone
を100ダウンロードまで無料にしました。マニュアルページは、ToDone サポートページ です。
【本文】
Xcode で macOS の Document App テンプレートを使ってプロジェクトを作ったらテキストエディターを作るのに適しているんだろうなあ、と思っていました。実際にやってみたら、プロジェクトを作った段階で、もうテキストエディタが出来上がっていました。ただし少し書き換えなければ、便利には使えません。
私の開発環境は次のとおりです。
更新履歴
2022/07/25 「ターミナルでビルドする」にコード説明をつけました。コード内容は Xcode 用とまったく同じです。まだまだ雑ですが、徐々に改訂していこうと思っています。
2022/07/26 コード説明を少し改訂しました。
Document App テンプレートは、ドキュメント・ベースド・アプリケーション(Document-Based Application)を作るテンプレートです。
ドキュメント・ベースド・アプリケーションとは、 内容の違う複数のデータ(ドキュメント)を同時に開くことのできるアプリケーションです。それぞれぞれのデータは編集することもでき、保存することもできます。また新規にデータ(ドキュメント)を作ることもできます。開くデータの場所や、新規データの保存場所は SandBox にとらわれることがなく、お使いのマックの自由な場所に保存できます。
Xdoeを起動して表示される Welcome to Xcode 画面で Create a new Xcode Project をクリックするか、File メニュー → New → Project... を選びます。
次の画面で macOS の Document App テンプレートを選び Next をクリックします。
次の画面で次のように設定して Next をクリックします。
次の画面で保存場所を決めて、Create をクリックします。 私の場合は、Create Git repository on may Mac のチェックは外しています。
この状態で(なにも変更せずに)Product メニューの Run をクリックするか、Xcode の左上の右三角 ▶︎ をクリックします。⌘ + R でも OK です。しばらく待つとアプリケーションが起動します。
実行するとまずオープンパネルが開きます。この段階で実際に開けるファイルはないみたいです。
前の画面で「New Document」をクリックするか、前の画面で「Cancel」をクリックしてオープンパネルを閉じてから、File メニュの New をクリックすると新しいドキュメントウィンドウが開きます。
「Hello, world!」の部分は自由に書き換えられます。複数行のテキストも書けます。テキストを書き換えると、書類のタイトル「Untitled」の右横に編集済みであることを表す -- Edited という文字列が表示されます。この「Untitled」を閉じると保存するかどうかを確認するダイアログが現れます。
あるいは、File メニューの Save ⌘S を選ぶと保存パネルが現れます。
ドキュメントを保存してから、アプリケーションメニューの Quit を選ぶか ⌘Q でアプリケーションを終了します。そして、保存先にできたファイルをダブルクリックすると、アプリケーションが起動して、該当書類が開きます。
新規ファイルを作ると「Hello, world!」と書かれています。これを何も書かれていないようにします。次のようにプロダクト名Document.swift のコードを一部変更してください。
init(text: String = "Hello, world!") {
// ▼
init(text: String = "") {
先ほど保存した Untitled を「情報を見る」で確認すると、拡張子は .exampletext となっていました。初期設定では、この拡張子(フォーマット)のファイルだけが読み込みと保存ができるみたいです。プロダクト名Document.swift の一部を次のように変更して対応できるフォーマットを増やします。。
// 次のコードの .exampleText と書かれた配列に他のフォーマットも追加します
static var readableContentTypes: [UTType] { [.exampleText] }
// ▼
static var readableContentTypes: [UTType] { [.exampleText, .text, .rtf] }
.text を追加すると、.txt、.swift、.jason、.js、.html、.c など、およそプレーンテキスト型式のファイルの読み込みができるようになります。
次のリッチテキストファイルを試す場合は、ファイルが壊れるかもしれませんので、必要なリッチテキストファイルでは試さないでください。テスト用のリッチテキストファイルを作って、それで試してください。
.rtf を追加するとMacに付属しているテキストエディットのファイルも読み込めます。 .text だけでも Mac に付属しているテキストエディットのリッチテキストは読み込めます。しかし、TextEditor View がリッチテキスト対応にしていないので .text でも .rtf でもファイルは正しく表示されません。
保存パネルにも File Format プルダウンメニューがつきます。
File Format プルダウンメニューの中身は次のようになります。ただし text や rtf を選んだ場合、ファイル名に自動的に拡張子が付くことはありません。ファイルに拡張子をつける場合は、ファイル名を拡張子付きで設定してください。Example Text の場合は自動で拡張子が付きます。なお、Untitled.txt を .exampletext フォーマットで保存しようとした場合、拡張子として、.exampletext を使うか両方を使うかを選択しなければなりません。両方使った場合は、Untitled.txt.exampletest になります。つまり.exampletext フォーマットでは汎用的なテキストエディタ(プログラムファイルを書くテキストエディタ)としては使えないことになります。
もともと、プレーンテキスト対応で、プログラムコードが書けるテキストエディターを作りたかったので、ここでは思い切って、.text だけに対応することにします。プロダクト名Document.swift を次のように書き換えます。コメントの指示のとおり変更してください。
import SwiftUI
import UniformTypeIdentifiers
/* exampletext フォーマットはもう不要になりますので削除します。
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
*/
struct BarDocument: FileDocument {
var text: String
init(text: String = "") {
self.text = text
}
// 扱えるフォーマットを .text だけにします。
static var readableContentTypes: [UTType] { [.text] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
text = string
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = text.data(using: .utf8)!
return .init(regularFileWithContents: data)
}
}
import SwiftUI
struct ContentView: View {
@Binding var document: BarDocument
var body: some View {
TextEditor(text: $document.text)
// 次のコードだけを追加しています。
.font(.system(.body, design: .monospaced))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: .constant(BarDocument()))
}
}
等幅フォントにしたので、インデント(字下げ)がはっきりとしました。
// フォントの大きだけを変える場合は次のコードになります。
.font(.body)
// フォントのタイプを変える場合は次のコードのように大きさも一緒に設定しなければなりません
.font(.system(.body, design: .monospaced))
次の表は、フォントの大きさと、フォントのタイプを設定するコードの一覧表です。
フォントの大きさ | コード | フォントのタイプ | コード |
---|---|---|---|
1番大きい | .largeTitle | 標準 | .default |
.title | 等幅 | .monospaced | |
.title2 | 丸ゴシック体 | .rounded | |
.title3 | 明朝体 | .serif | |
.headline | |||
.subheadline | |||
.body | |||
.callout | |||
.caption | |||
.caption2 | |||
1番小さい | .footnote |
わずか四箇所の変更だけでしたが、これで、自作テキストエディターの完成としたいと思います。もともと Document App テンプレートがテキストエディターとして、ほぼ完成していたので、やってみればすることがほとんどなかったです。
当然、ファイルをダブルクリックするとアプリが起動するとか、Dock に登録されたアプリにファイルをドラッグ&ドロップするとファイルが開くとか、フォントの大きさが変えられるなどの機能もつけたいですが、それよりも Document App を使ってイメージビューワーを作ることを先にしたいです。あるいは、今回のテキストエディターを Xcode を使わずにターミナルでビルドできるようにするとか(^^);
何も変更せずにターミナルでビルドできました。今のところ無事に動作しています。
次の三つのファイルを任意のテキストエディタで記述してください。ファイル名は一応それなりの名前にしていますが、大文字で始まっていればどんな名前でも OK みたいです。
Xcode では、プロダクト名App.swift です。
import SwiftUI
@main
struct BarApp: App {
var body: some Scene {
DocumentGroup(newDocument: BarDocument()) { file in
ContentView(document: file.$document)
}
}
}
DocumentGroup(viewing: BarDocument.self) {
ContentView(document: $0.$document)
}
// もしくは、
DocumentGroup(viewing: BarDocument.self) { file in
ContentView(document: file.$document)
}
閲覧のみにするとオープンダイアログから「New Document」ボタンがなくなり、「File」メニューの「New」や「Save」などは使えなくなります。
Xcode では、プロダクト名Document.swift です。
import SwiftUI
import UniformTypeIdentifiers
struct BarDocument: FileDocument {
var text: String
init(text: String = "") {
self.text = text
}
static var readableContentTypes: [UTType] { [.text] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
text = string
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = text.data(using: .utf8)!
return .init(regularFileWithContents: data)
}
}
必須項目 | 役割 |
---|---|
init(configuration:) | ファイルの読み込みを担当 |
readableContentTypes: | ドキュメントが開くことができるタイプを設定 |
fileWrapper(configuration:) | ファイルの保存を担当 |
writableContentTypes: | ドキュメンが保存できるタイプを設定 ※ただしデフォルトで実装されるので、実際の記述は不要 |
Xcode では、ContentView.swift です。
import SwiftUI
struct ContentView: View {
@Binding var document: BarDocument
var body: some View {
TextEditor(text: $document.text)
.font(.system(.body, design: .monospaced))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(document: .constant(BarDocument()))
}
}
macOS では、アプリケーションを、「アプリケーションバンドル」 とよばれる特殊なフォルダで管理します。アプリケーションバンドルを使うと ダブルクリックでターミナルを開かずにアプリケーションを起動できたり、 アプリケーションにアイコンや画像をつけることができます。
アプリケーションバンドルは次のようなディレクトリ構成になります。
Bar.app
-Contents
--Info.plist
--MacOS
---bar
--Resources
---アイコン.icns
Bar.app/Contents
私は次のサイトから .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>TextEditor</string>
</dict>
</plist>
Info.plist には、いろいろな設定を書き込めますが、今回は三つだけ設定しました。
先ほど作った三つの .swift ファイルと Bar アプリケーションバンドルを同じディレクトリに置きます。ターミナルを起動して、そのディレクトリに移動して次のようにコマンドしてください。その後アプリケーションバンドル(実際にはアプリケーションに見えます)をダブルクリックするとアプリケーションが起動します。
// ビルド
swiftc BarApp.swift BarDoc.swift BarView.swift -o bar
// ビルドされた実行ファイルをアプリケーションバンドルの MacOS ディレクトリに移動
mv bar Bar.app/Contents/MacOS
今のところ不具合は、変更したファイルを保存せずにドキュメントを閉じたり、アプリケーションを終了した場合に、確認ダイアログも出ずに保存してしまうという点だけです。しかしこれは Xcode でビルドした場合も同じでした。