feed

2021年08月23日, 編集履歴

SwiftUIでDocument-Based Appな画像閲覧アプリを作る その1 プロジェクト作成から画像の表示まで

SwiftUIでDocument-Based Appな画像閲覧アプリを習作した。PNG/JPEG画像を開いて閲覧、スクロール、ピンチジェスチャで拡大縮小、ツールバーにボタン配置等を実装した。その覚書その1。

このシリーズの他のブログは、

プロジェクトはGitHubで公開している。

ビルド環境は、

である。

Document Appテンプレートでプロジェクト作成

SwiftUIでDocument-Based Appなプロジェクトを作成するには「Document App」テンプレートを使用する。今回はmacOS/iOS両対応で作りたいので、「Multiplatform」の「Document App」テンプレートを選択してプロジェクトを作成する。

Info.plistの設定

Info.plistファイルで、アプリが読み書きできるファイル形式を以下の項目で指定する。

Document Typesは必須、既存のファイル形式を扱う場合は「Imported 〜」を、アプリ独自の形式を定義する場合は「Exported 〜」を使用する。

今回は既存のPNG/JPEG画像を扱いたいので、「Exported 〜」は不使用。

Document Typesでは、PNG/JPEGそれぞれに形式の名称やUTIを指定する。また、アプリは画像の閲覧専用とするので、「Role」は「Viewer」を、「Handler Rank」は「Alternate」を選択した。

Imported Type Identifiersでは、それぞれの形式のUTIや拡張子、MIME Type等を指定する。

macOS/iOSのInfo.plistは独立しているので、両対応させる場合は両方で設定を行う必要がある。

FileDocument/ReferenceFileDocumentで対応形式を宣言

SwiftUI Document-Based Appでは、対応するファイル形式のモデルをFileDocumentあるいはReferenceFileDocumentに適合したオブジェクトで表現する。FileDocumentあるいはReferenceFileDocumentが持つstatic var readableContentTypes: [UTType] { get }プロパティで、対応する形式のUTIを[UTType]で返すようにする。

  static var readableContentTypes: [UTType] {
    [
      UTType(importedAs: "public.png"),
      UTType(importedAs: "public.jpeg")
    ]
  }

今回はPNG/JPEG画像を同様に扱いたいので、ひとつのドキュメントモデルで一緒に宣言した。形式によって読み書きの処理等を変えたい場合は、複数のドキュメントモデルを作成する。

ファイルの書き込みにも対応する場合は、static var writableContentTypes: [UTType] { get }を実装する。

NSImageUIImageを一律に扱う

今回のアプリはmacOSとiOSの両対応にしたい。それぞれの環境で画像を表すオブジェクトであるNSImageUIImageを一律に扱うため、typealiasを使ってIVImageなる型を定義する。

#if os(macOS)
typealias IVImage = NSImage
#elseif os(iOS)
typealias IVImage = UIImage
#endif

また、後の工程で画像を表示するビューとしてSwiftUIのImageを使うが、IVImageを扱えるようにextensionで拡張する。

extension Image {
  init(ivImage: IVImage) {
    #if os(macOS)
    self.init(nsImage: ivImage)
    #elseif os(iOS)
    self.init(uiImage: ivImage)
    #endif
  }
}

ドキュメントモデルで読み書き処理を実装

FileDocument/ReferenceFileDocumentなドキュメントのモデルで画像の読み書きの処理を実装する。次回の工程でモデルのプロパティを@Publishedにしたかったので、今回のアプリではReferenceFileDocumentで実装する。この場合、

が必要である。

class ImageDocument: ReferenceFileDocument {

  typealias Snapshot = IVImage

  static var readableContentTypes: [UTType] {
    [
      UTType(importedAs: "public.png"),
      UTType(importedAs: "public.jpeg")
    ]
  }

  var image: IVImage

  init(image: IVImage = IVImage()) {
    self.image = image
  }

  required init(configuration: ReadConfiguration) throws {
    guard let data = configuration.file.regularFileContents,
          let image = IVImage(data: data)
    else {
      throw CocoaError(.fileReadCorruptFile)
    }
    self.image = image
  }

  func fileWrapper(snapshot: IVImage, configuration: WriteConfiguration) throws -> FileWrapper {
    throw CocoaError(.fileWriteUnknown)
  }

  func snapshot(contentType: UTType) throws -> IVImage {
    return self.image
  }

}

読み込み処理は、単純に画像を読み込んでIVImageNSImageUIImage)型のimageプロパティに格納する。func fileWrapper(snapshot:configuration:)はファイルの書き込みに用いられるが、今回のアプリは閲覧専用なのでエラーを吐かせておく。

init(image:)は後の工程でビューのプレビューを表示させるときに空のドキュメントを供給するときに必要なので実装しておいた。

DocumentGroupの指定

Appプロトコルに適合した構造体の中、DocumentGroupシーンでドキュメントモデルの指定を行う。

struct ImageViewerSwiftUIApp: App {
  var body: some Scene {
    DocumentGroup(viewing: ImageDocument.self) { file in
      ContentView(document: file.document)
    }
  }
}

今回は閲覧専用なので、init(viewing:viewer:)を使った。第1引数にはドキュメントモデルの型を指定する。複数のドキュメントモデルを用いる場合は、DocumentGroupを複数宣言する。

ビューの作成

DocumentGroup内で指定したビューでドキュメントの内容を表示する(前項のContentView)。

struct ContentView: View {

  @ObservedObject var document: ImageDocument

  var body: some View {
    ScrollView([.horizontal, .vertical]) {
      Image(ivImage: document.image)
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView(document: ImageDocument())
  }
}

今回のドキュメントモデルで採用したReferenceFileDocumentObservableObjectに適合しているので、プロパティとして保持するときは@ObservedObjectで受ける。大きな画像を開いたときにスクロールできるように、ImageScrollViewに内包させた。

ここまでで、とりあえずPNG/JPEG画像を閲覧するアプリが動く。

Document-Based Appなので、複数のウィンドウで別々の画像を開いたり、複数開いたウィンドウをタブでひとつのウィンドウにまとめたりできる。

次回はピンチイン/アウトジェスチャによる画像の拡大縮小の実装を行う。

2021年07月22日, 編集履歴

Murasaki ver. 2.4をリリースしました

Murasaki ver. 2.4をリリースしました。macOS用のEPUBリーダアプリです。

前回からの主な変更点は、

です。

今回から対応OSはmac OS Big Sur以降になります。また、Spotlight/Quick Lookプラグインは廃止になります。

2021年07月19日, 編集履歴

Silver Baton ver. 1.4をリリースしました

Silver Baton ver. 1.4をリリースしました。手元のトラックパッド/マウス操作でMusic.appを操作できるパネルを表示するmacOS用アプリです。

前回からの主な変更点は、

です。

環境設定ウィンドウに、アプリが必要とする権限の確認、およびその取得を誘導するビューを追加しました。

また、アプリ起動時に環境設定ウィンドウを表示するようにし、その動作を抑制できる設定を追加しました。これは、権限の取得やパネル表示方法の選択など、Silver Batonの初期導入をスムーズに行えるようにすることを狙っています。

環境設定ウィンドウの改修にあたり一部SwiftUIを採用しました。環境設定ウィンドウにある3つのビューがそれぞれSwiftUIで作られています。ガワとなるウィンドウおよびNSTabViewControllerは既存のStoryboardを使い、各ビューだけをSwiftUIに置き換えた形となります。

今後はmacOSアプリもSwiftUIが主流になっていくと思われ、その練習がてらとして使ってみました。

2021年03月24日, 編集履歴

Silver Baton v1.3.1をリリース

Silver Baton v1.3.1をリリースしました。手元のトラックパッド/マウス操作でMusic.appを操作できるパネルを表示するmacOS用アプリです。

前回からの主な変更点は、

です。

2点目の音量調節機能は、手元のトラックパッドでSilver Batonのパネルを呼び出してすぐさま音量調節できるので、自分でもよく使う良いのができたと思います。

あと、v1.0からあったのに言ってなかった機能として、パネル表示中にキーボードショートカットが使えます。

こちらもあわせてご活用ください。

Mac App Storeへのレビュー投稿機能やアプリ内課金も追加したのでどんどんやっちゃってください(購入しなくても全機能使えます)。

2021年03月09日, 編集履歴

tccutilでmacOSアプリのプライバシー設定をリセットする

macOSアプリのプライバシー設定をリセットしたい時はtccutilコマンドを使う。

macOSアプリが連絡先やカレンダー情報等、カメラ等の使用、そしてアクセシビリティ機能の使用や他のアプリの操作をしようとする際には、対応するプライバシー設定で許諾を得る必要がある。たいていは、それらの機能の初回使用時に許諾を促すダイアログを出すようにしているはずである。

ここで問題になるのは、許諾を促すダイアログは通常一度しか表示されないということである。許諾を得られていれば問題ないが、一度拒否された場合は再度アプリ側が要求しても表示されない。以降はシステム環境設定の「セキュリティとプライバシー」設定から手動操作をする必要がある。したがって、一度拒否された後は自前でダイアログ等を表示して、手動での許諾操作を促してやらなければならない。

アプリの開発側としては、最初の許諾が得られた後の挙動はもちろん、拒否された場合の挙動も確認する必要がある。システム環境設定からGUI操作でプライバシー設定をリセットすることができるが、許諾後・拒否後の挙動を何度も確認しなければならない時は面倒である。また「オートメーション」のプライバシー設定に関しては、システム環境設定からでは完全なリセットができない(削除ボタンがない。なんで?)。

そこで「tccutil」コマンドの登場である。tccutilを使うことで、プライバシー設定のリセットができる。

基本的な使用方法は、

$ tccutil command service [bundle_id]

である。

commandは唯一のサブコマンドであるresetを指定する。

serviceはプライバシー設定の種類を指定する。種類は検索すれば出てくるが、service-list的なサブコマンドで一覧を出力して欲しい。Allを指定するとすべてのプライバシー設定が対象となる。開発中のアプリの挙動確認をする時は、Allと後述のbundle_idを組み合わせればよい。

bundle_idはリセットしたいアプリのバンドルIDを指定する。指定がない場合はその種類のプライバシー設定すべてをリセットする。

まとめると、

$ tccutil reset All com.genjiapp.Silver-Baton

のようになる。上記コマンドを実行すると、すべてのプライバシー設定からバンドルIDcom.genjiapp.Silver-Batonを持つアプリが削除される(リセットされる)。