macOS   SwiftUIプログラミング   Doument App 1 (TextEditor)

ホーム

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

【本文】
Xcode で macOS の Document App テンプレートを使ってプロジェクトを作ったらテキストエディターを作るのに適しているんだろうなあ、と思っていました。実際にやってみたら、プロジェクトを作った段階で、もうテキストエディタが出来上がっていました。ただし少し書き換えなければ、便利には使えません。

今回は、Xcode を使って作業を進めます。しかしターミナルでビルドしても問題なく動作しました。

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

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

更新履歴
2022/07/25 「ターミナルでビルドする」にコード説明をつけました。コード内容は Xcode 用とまったく同じです。まだまだ雑ですが、徐々に改訂していこうと思っています。
2022/07/26 コード説明を少し改訂しました。

Document App

Document App テンプレートは、ドキュメント・ベースド・アプリケーション(Document-Based Application)を作るテンプレートです。

ドキュメント・ベースド・アプリケーションとは、 内容の違う複数のデータ(ドキュメント)を同時に開くことのできるアプリケーションです。それぞれぞれのデータは編集することもでき、保存することもできます。また新規にデータ(ドキュメント)を作ることもできます。開くデータの場所や、新規データの保存場所は SandBox にとらわれることがなく、お使いのマックの自由な場所に保存できます。

プロジェクトの作成

Xdoeを起動して表示される Welcome to Xcode 画面で Create a new Xcode Project をクリックするか、File メニュー → New → Project... を選びます。

次の画面で macOS の Document App テンプレートを選び Next をクリックします。

次の画面で次のように設定して Next をクリックします。

  1. Product Name は任意の名前にします。
  2. Team は Apple Developer に登録している名前にしますが、 登録していない場合は、None を選びます。
  3. Organization Identifier は、お持ちの URL を逆にしたものを設定しますが、 URL をお持ちでない場合は、com.yourname などにしておきます。
  4. Interface に SwiftUI を選びます。
  5. Language に Swift を選びます。
  6. Use Core Data のチェックを外します(Core Data はもともとチェックできない状態になっています)。
  7. Include Tests のチェックを外します。

次の画面で保存場所を決めて、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 を次のように書き換えます。コメントの指示のとおり変更してください。

プロダクト名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)
    }
}
	

書式の設定

デフォルト(初期設定)のままでは、文字の大きさがやや小さく、また等幅フォントでもないので、コードファイルが読みにくいです。ここでは文字の大きさとスタイルを変えたいと思います。ContentView.swift を次のように書き換えてください。

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()))
    }
}
	

等幅フォントにしたので、インデント(字下げ)がはっきりとしました。


// フォントの大きだけを変える場合は次のコードになります。
.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 みたいです。

BarApp.swift

Xcode では、プロダクト名App.swift です。


import SwiftUI

@main
struct BarApp: App {
    var body: some Scene {
        DocumentGroup(newDocument: BarDocument()) { file in
            ContentView(document: file.$document)
        }
    }
}
	

コード説明

  1. DocumentGroup(newDocument: BarDocument())
  2. Scene に DocumentGroup を宣言することにより、macOS では複数のドキュメントを開く機能と、それに合わせたメニューが表示されるようになります。引数には newDocument と viewing があり、前者はドキュメントの閲覧と編集(read, write)が可能で、後者はドキュメントの閲覧(read)だけが可能です。閲覧のみにする場合は次のように書き換えます。
    
    DocumentGroup(viewing: BarDocument.self) {
                ContentView(document: $0.$document)
    }
    // もしくは、
    DocumentGroup(viewing: BarDocument.self) { file in
                ContentView(document: file.$document)
    }
        
    閲覧のみにするとオープンダイアログから「New Document」ボタンがなくなり、「File」メニューの「New」や「Save」などは使えなくなります。


BarDoc.swift

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)
    }
}
	

コード説明

  1. import UniformTypeIdentifiers
    Apple では、ファイルの形式(フォーマット)を UniformTypeIdentifiers(UTI)で設定します。ここでは、すでに定義されているファイル形式(フォーマット)や UTI に関するいろいろな設定を読み込んでいます。

  2. struct BarDocument: FileDocument {
    FileDocument プロトコルを採用した場合に実装しなければいけないのは init(configuration:) と readableContentTypes: と fileWrapper(configuration:) と writableContentTypes: です。init(configuration:)がファイルの読み込みを担当して、readableContentTypes: でドキュメントが開くことができるタイプを設定して、fileWrapper(configuration:)がファイルの保存を担当して、writableContentTypes: がドキュメンが保存できるタイプを設定します。ただし writableContentTypes: は、デフォルト(初期設定?)で実装され、実際に記述しなくても良いみたいです。表にすると次のようになります。

    FileDocument プロトコルの必須項目
    必須項目役割
    init(configuration:)ファイルの読み込みを担当
    readableContentTypes:ドキュメントが開くことができるタイプを設定
    fileWrapper(configuration:)ファイルの保存を担当
    writableContentTypes:ドキュメンが保存できるタイプを設定
    ※ただしデフォルトで実装されるので、実際の記述は不要

  3. var text: String
    ContentView にデータを渡すためのプロパティを宣言しています。

  4. init(text: String = "") { self.text = text }
    オープンパネルや「File」メニューからNew Document を選んだ場合に、New Document(新規ドキュメント)に表示される内容を定義します。編集と閲覧ができるアプリケーションの場合、この定義を省略することはできません。

  5. static var readableContentTypes: [UTType] { [.text] }
    扱えるファイル形式を UTType の配列として設定します。UTType は、ロード、送信、または受信するデータのタイプを表す構造体です。.text はすでに UTType として登録されています。

  6. init(configuration: ReadConfiguration) throws {
    ファイルの読み込みを定義します。

  7. guard let data = configuration.file.regularFileContents,
    data 定数にファイルの内容が入ります。

  8. let string = String(data: data, encoding: .utf8)
    data: 引数に前行で取得した data を渡して作った String を作成して string 定数に入れています。

  9. throw CocoaError(.fileReadCorruptFile)
    configuration に失敗した場合のエラー処理です。

  10. text = string
    text プロパティに、先ほど作った String を入れています。

  11. func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
    ドキュメントを保存するメソッドです。これはテンプレートで自動的に書かれたものを素まま使っています。

  12. let data = text.data(using: .utf8)!
    data 定数に text プロパティのデータを UTF-8 形式で代入しています。

  13. return .init(regularFileWithContents: data)
    詳しいことは分かりませんが data 定数の内容を FileWrapper 型で返しているでしょう。


BarView.swift

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()))
    }
}
	

コード説明

  1. @Binding var document: BarDocument
    プロダクト名Document とバインディングしています。

  2. TextEditor(text: $document.text)
    text: 引数にバインディングされたプロダクト名Document の text プロパティの値を設定した TextEditor View を作っています。

  3. .font(.system(.body, design: .monospaced))
    TextEditor のフォントの大きさに .body を、フォントのタイプに 等幅を設定しています。

  4. struct ContentView_Previews: PreviewProvider {・・・}
    この構造体は Xcode のプレビュー機能を有効にする構造体です。アプリケーションの機能にはまったく関係ありません。アプリケーションによっては、この構造体が原因でエラーが出る場合もありますので、プレビュー機能を使わないのであれば、削除しても良いでしょう。


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

macOS では、アプリケーションを、「アプリケーションバンドル」 とよばれる特殊なフォルダで管理します。アプリケーションバンドルを使うと ダブルクリックでターミナルを開かずにアプリケーションを起動できたり、 アプリケーションにアイコンや画像をつけることができます。

アプリケーションバンドルは次のようなディレクトリ構成になります。


Bar.app
-Contents
--Info.plist
--MacOS
---bar
--Resources
---アイコン.icns
    

Bar.app/Contents

  1. Bar.app は、通常のフォルダの末尾に .app という拡張子をつけたものです。 〜.app というフォルダは単一のアプリケーションのように表示されます。
  2. アプリケーションバンドルを右クリックして「パッケージの内容を表示」を選ぶと、 アプリケーションバンドルの中身にアクセスできます。
  3. Bar.app の直下に Contents というフォルダを作ります。
  4. Contens フォルダの下に Info.plist というテキストファイルと、MacOS というフォルダと、Resources というフォルダを作ります。
  5. MacOS フォルダの中に、これから作る bar 実行ファイルを入れます。
  6. Resources の中に、.icns という拡張子のついたアイコンファイルを入れます。

私は次のサイトから .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>TextEditor</string>
</dict>
</plist>
    

Info.plist には、いろいろな設定を書き込めますが、今回は三つだけ設定しました。

  1. CFBundleExecutable には、実行ファイルを指定します。
  2. CFBundleIconFile には、.icns 形式のアイコンファイルを指定します。
  3. CFBundleName には、アプリケーションの名前を指定します。 これを指定しないとアプリケーションバンドルの名前が アプリケーションの名前になります。

ビルド

先ほど作った三つの .swift ファイルと Bar アプリケーションバンドルを同じディレクトリに置きます。ターミナルを起動して、そのディレクトリに移動して次のようにコマンドしてください。その後アプリケーションバンドル(実際にはアプリケーションに見えます)をダブルクリックするとアプリケーションが起動します。


// ビルド
swiftc BarApp.swift BarDoc.swift BarView.swift -o bar
// ビルドされた実行ファイルをアプリケーションバンドルの MacOS ディレクトリに移動
mv bar Bar.app/Contents/MacOS
    

今のところ不具合は、変更したファイルを保存せずにドキュメントを閉じたり、アプリケーションを終了した場合に、確認ダイアログも出ずに保存してしまうという点だけです。しかしこれは Xcode でビルドした場合も同じでした。




5963 visits
Posted: Jul. 19, 2022
Update: Jul. 26, 2022

ホーム   目次