feed
2021年08月29日, 編集履歴
ウィンドウ下部にマウスを持っていくと、メインの領域の上に重なるように表示される操作UIを実装する。こんな感じ。
動画プレイヤアプリの再生ボタン等がこんな感じで実装されていることがよくある。本項ではこれを「オーバレイ操作UI」と呼ぶことにする。
成果物をGitHubに公開する。
ビルド環境は、
macOS 11.5.2
Xcode 12.5.1
である。
操作UIの重ね合わせ表示
一例として以下のようなビューがあるとする。
struct ContentView : View {
var body : some View {
Image ( systemName : "star" )
. font ( . system ( size : 500 ))
}
}
オーバレイ操作UIに入れるUI部品を今回はSlider
にするとして、そのスライダ値によって星の色を変える場合を考える。
struct ContentView : View {
@State private var hue = 0.5
var body : some View {
ZStack {
Image ( systemName : "star" )
. font ( . system ( size : 500 ))
. foregroundColor ( Color ( hue : self . hue , saturation : 1.0 , brightness : 1.0 ))
VStack {
Spacer ()
Slider ( value : self . $ hue , in : 0 ... 1 ) {
Text ( "Hue:" )
}
. frame ( width : 250 )
. padding ()
. background (
Capsule ()
. fill ( Color ( . windowBackgroundColor ))
. shadow ( radius : 5 )
)
}
. padding ( . bottom , 16 )
}
}
}
全体をZStack
に入れ、元のImage
と目的のUI部品を併置する。今回はオーバレイ操作UIをウィンドウ下部に配置したいので、VStack
とSpacer
を組み合わせた。また、UI部品の背景を描画するため、background
とCapsule
を使用した。メインの領域と境界をわかりやすくするため、shadow
も加えた。
onHover
で表示・非表示の切り替え
これではオーバレイ操作UIが表示しっぱなしなので、領域にマウスが入ったときにだけ表示されるようにする。
struct ContentView : View {
@State private var hue = 0.5
@State private var isHover = false
var body : some View {
ZStack {
Image ( systemName : "star" )
. font ( . system ( size : 500 ))
. foregroundColor ( Color ( hue : self . hue , saturation : 1.0 , brightness : 1.0 ))
VStack {
Spacer ()
Slider ( value : self . $ hue , in : 0 ... 1 ) {
Text ( "Hue:" )
}
. frame ( width : 250 )
. padding ()
. background (
Capsule ()
. fill ( Color ( . windowBackgroundColor ))
. shadow ( radius : 5 )
)
. onHover { hovering in
withAnimation {
self . isHover = hovering
}
}
}
. padding ( . bottom , 16 )
. opacity ( self . isHover ? 1.0 : 0.0 )
}
}
}
onHover
はマウスがその領域に出入りするときに呼ばれる。クロージャの引数hovering
に出入りの状態がBool
型で入っているので、@State
なプロパティisHover
に代入する。isHover
はオーバレイ操作UI全体を包むVStack
のopacity
の条件として用いる。isHover
代入時にwithAnimation
を用いることで、ふわっとした表示・非表示の切り替えがされるようになる。
これで、領域内へマウスが入ったときにだけオーバレイ操作UIが表示されるようになるが、スライダ操作中に領域外へ出てしまうと非表示になってしまう問題がある。
2023年3月18日追記
少なくともmacOS 13.2.1では、.opacity()
と.onHover()
の順番が重要で、.opacity()
を先に記述しないと、.onHover()
が反応しなかったり、反応する領域が狭く変化してしまうという問題がある(参照:SwiftUIの .opacity() と .onHover() は順番が重要 )。
したがって、前記コードの.opacity()
を.onHover()
の前に移動させなければならない(後記コードも同様)。
操作中の非表示を抑制
スライダ操作中は非表示にならないよう工夫をする。
struct ContentView : View {
@State private var hue = 0.5
@State private var isHover = false
@State private var isEditing = false
var body : some View {
ZStack {
Image ( systemName : "star" )
. font ( . system ( size : 500 ))
. foregroundColor ( Color ( hue : self . hue , saturation : 1.0 , brightness : 1.0 ))
VStack {
Spacer ()
Slider ( value : self . $ hue , in : 0 ... 1 , onEditingChanged : { editing in
withAnimation {
self . isEditing = editing
}
}, label : {
Text ( "Hue:" )
})
. frame ( width : 250 )
. padding ()
. background (
Capsule ()
. fill ( Color ( . windowBackgroundColor ))
. shadow ( radius : 5 )
)
. onHover { hovering in
withAnimation {
self . isHover = hovering
}
}
}
. padding ( . bottom , 16 )
. opacity ( self . isHover || self . isEditing ? 1.0 : 0.0 )
}
}
}
Slider
の作成をinit(value:in:onEditingChanged:label:)
を使うようにした。増えたonEditingChanged
の部分は、スライダ操作開始でクロージャ引数にtrue
が、操作終了でfalse
が入る。これを@State
なプロパティisEditing
に代入する。オーバレイ操作UI全体のVStack
に付けたopacity
の条件にisEditing
も加えることで、スライダ操作中は非表示にならなくなる。
最初の数秒間は表示させる
さらに一工夫。初見ではオーバレイ操作UI自体の存在が認知されないので、最初の数秒間はオーバレイ操作UIが表示されるようにする。
struct ContentView : View {
@State private var hue = 0.5
@State private var isHover = true
@State private var isEditing = false
@State private var timer : Timer ? = nil
var body : some View {
ZStack {
Image ( systemName : "star" )
. font ( . system ( size : 500 ))
. foregroundColor ( Color ( hue : self . hue , saturation : 1.0 , brightness : 1.0 ))
VStack {
Spacer ()
Slider ( value : self . $ hue , in : 0 ... 1 , onEditingChanged : { editing in
withAnimation {
self . isEditing = editing
}
}, label : {
Text ( "Hue:" )
})
. frame ( width : 250 )
. padding ()
. background (
Capsule ()
. fill ( Color ( . windowBackgroundColor ))
. shadow ( radius : 5 )
)
. onHover { hovering in
self . timer ? . invalidate ()
self . timer = nil
withAnimation {
self . isHover = hovering
}
}
}
. padding ( . bottom , 16 )
. opacity ( self . isHover || self . isEditing ? 1.0 : 0.0 )
}
. onAppear {
self . timer = Timer . scheduledTimer ( withTimeInterval : 2.0 , repeats : false ) { _ in
withAnimation {
self . isHover = false
}
self . timer ? . invalidate ()
self . timer = nil
}
}
}
}
isHover
プロパティの初期値をtrue
にし、ビュー表示時にはオーバレイ操作UIが出ている状態にする。それと同時に、ビュー全体を包むZStack
にonAppear
を付けて、その中で数秒後にisHover
をfalse
に切り替えるTimer.scheduledTimer
を作った。
また、タイマ発火前にオーバレイ操作UIの領域に入り、領域にいるままにタイマが発火すると、領域内にマウスがあるのに非表示なってしまう。タイマ発火前に領域に入った場合は、タイマを破棄する処理を入れる。
ビューの切り出し
最後に、使い回しができるようにコンテナビューとして切り出す。
struct ContentView : View {
@State private var hue = 0.5
@State private var isEditing = false
var body : some View {
VStack {
OverlayControlContainer ( isEditing : self . $ isEditing , content : {
Image ( systemName : "star" )
. font ( . system ( size : 500 ))
. foregroundColor ( Color ( hue : self . hue , saturation : 1.0 , brightness : 1.0 ))
}, overlayControl : {
Slider ( value : self . $ hue , in : 0 ... 1 , onEditingChanged : { editing in
withAnimation {
self . isEditing = editing
}
}, label : {
Text ( "Hue:" )
})
. frame ( width : 250 )
})
}
}
}
struct OverlayControlContainer < Content : View , OverlayControl : View > : View {
@Binding var isEditing : Bool
@ViewBuilder let content : Content
@ViewBuilder let overlayControl : OverlayControl
@State private var isHover = true
@State private var timer : Timer ? = nil
var body : some View {
ZStack {
self . content
VStack {
Spacer ()
self . overlayControl
. padding ()
. background (
Capsule ()
. fill ( Color ( . windowBackgroundColor ))
. shadow ( radius : 5 )
)
. onHover { hovering in
self . timer ? . invalidate ()
self . timer = nil
withAnimation {
self . isHover = hovering
}
}
}
. padding ( . bottom , 16 )
. opacity ( self . isHover || self . isEditing ? 1.0 : 0.0 )
}
. onAppear {
self . timer = Timer . scheduledTimer ( withTimeInterval : 2.0 , repeats : false ) { _ in
withAnimation {
self . isHover = false
}
self . timer ? . invalidate ()
self . timer = nil
}
}
}
}
完成!
2021年08月26日, 編集履歴
SwiftUIでDocument-Based Appな画像閲覧アプリ習作の覚書その3。PNG/JPEG画像を開いて閲覧、スクロール、ピンチジェスチャで拡大縮小、ツールバーにボタン配置等を実装した。
このシリーズの他のブログは、
作成したプロジェクトはGitHubで公開している。
ビルド環境は、
macOS 11.5.2
Xcode 12.5.1
である。
前回 、表示した画像をジェスチャで拡大縮小できるようにした。今回はツールバーを実装する。
ツールバーの実装
ビューに対してtoolbar(content:)
を付与すると、ツールバーをつけることができる。引数content
にはToolbarContent
に適合したオブジェクト、具体的にはToolbarItem
かToobarItemGroup
を与える。
var body : some View {
ScrollView ([ . horizontal , . vertical ]) {
...
}
. toolbar {
ToolbarItem {
Button ( "Button 1" ) { ... }
}
ToolbarItemGroup {
Button ( "Button 2" ) { ... }
Button ( "Button 3" ) { ... }
}
}
}
のような感じ。toolbar(content:)
の中に適当に要素を列挙していけばよいが、長くなると本来のビューの構造が解りづらくなるので、今回はサブビュー化する方針を採った。
struct ContentView : View {
...
var body : some View {
ScrollView ([ . horizontal , . vertical ]) {
...
}
. toolbar {
MagnifyToolbarButtons ( document : self . document )
}
}
}
struct MagnifyToolbarButtons : ToolbarContent {
let document : ImageDocument
var body : some ToolbarContent {
#if os(iOS)
let placement = ToolbarItemPlacement . bottomBar
#else
let placement = ToolbarItemPlacement . automatic
#endif
ToolbarItemGroup ( placement : placement ) {
Button ( action : {
self . document . scaleViewSize ( 0.5 , animate : true )
}) {
Image ( systemName : "minus.magnifyingglass" )
}
Button ( action : {
self . document . resetViewSize ( animate : true )
}) {
Image ( systemName : "equal.circle" )
}
Button ( action : {
self . document . scaleViewSize ( 2.0 , animate : true )
}) {
Image ( systemName : "plus.magnifyingglass" )
}
}
}
}
このとき、サブビューの構造体とそのbody
プロパティはToolbarContent
に適合させるようにする。
また、iPhoneの場合、画面上部のツールバーに複数のボタンを配置しても最初のひとつしか表示されないため、画面下部のツールバーに表示させるようOSによる場合分けを行なった。
2021年08月24日, 編集履歴
SwiftUIでDocument-Based Appな画像閲覧アプリ習作の覚書その2。PNG/JPEG画像を開いて閲覧、スクロール、ピンチジェスチャで拡大縮小、ツールバーにボタン配置等を実装した。
このシリーズの他のブログは、
作成したプロジェクトはGitHubで公開している。
ビルド環境は、
macOS 11.5.2
Xcode 12.5.1
である。
前回 までで、画像を開いて表示できるところまでを作成した。今回は開いた画像の拡大縮小表示の実装を行う。
MagnificationGesture
でピンチジェスチャの実装
ビューに対して.gesture(_:including:)
を付けるとジェスチャ操作を実装できる。第1引数はGesture
プロトコルに適合したオブジェクトを渡す。標準で、
TapGesture
LongPressGesture
DragGesture
MagnificationGesture
RotationGesture
が用意されているので、いずれかを使う。今回はピンチジェスチャによる画像の拡大縮小表示がしたいので、MagnificationGesture
を使う。
struct ContentView : View {
...
@GestureState private var scale : CGFloat = 1.0
var magnificationGesture : some Gesture {
MagnificationGesture ()
// gestureState に値を代入すると、それが @GestureState のプロパティに入る。
// @GestureState のプロパティはジェスチャ終了時に自動的に初期値にリセットされる。
// 急激な拡大・縮小を防ぐため、値の範囲に制限を加える。
. updating ( self . $ scale ) { currentValue , gestureState , _ in
if currentValue < 0.1 {
gestureState = 0.1
}
else if currentValue > 5 {
gestureState = 5
}
else {
gestureState = currentValue
}
}
// ジェスチャ完了時の最終的な値が finalValue に入っている。
// これを使って実際に表示サイズを変更する。
// .updating() で @GestureStateに加えた制限は finalValue には
// 適用されないので、改めて範囲制限を加える。
. onEnded { finalValue in
var scale = finalValue
if scale < 0.1 {
scale = 0.1
}
else if scale > 5 {
scale = 5
}
self . document . scaleViewSize ( scale )
}
}
var body : some View {
ScrollView ([ . horizontal , . vertical ]) {
Image ( ivImage : document . image )
. resizable ()
. aspectRatio ( contentMode : . fit )
// ジェスチャ中の見掛け上の表示サイズ変更をする。
// @GestureState なプロパティはジェスチャ終了時には初期値にリセットされる。
. scaleEffect ( self . scale )
// ジェスチャ完了後の実際の表示サイズは .frame(width:, height:) を使う。
// .scaleEffect() では ScrollView から見た表示サイズが変更されないので、
// スクロールが狂う。
. frame ( width : self . document . viewSize . width ,
height : self . document . viewSize . height )
. gesture ( magnificationGesture )
}
}
}
ここで、Gesture
プロトコルにはジェスチャ操作中の値変化ごとに発火するメソッドがふたつと、ジェスチャ操作終了後に発火するメソッドがひとつ存在する。
updating(_:body:)
@GestureState
なプロパティと組み合わせて、値変化ごとの一時的なビューの状態変化を実装するときに使用
onChanged(_:)
値変化ごとのビューの永続的な状態変化を実装するときに使用
onEnded(_:)
ジェスチャ操作完了後の最後の値が渡ってくる。
今回はジェスチャ操作中の一時的な見掛け上の拡大縮小表現を、updating(_:body:)
とImage
ビューにつけたscaleEffect(_:anchor:)
で実装した。実際の表示サイズ変更はジェスチャ操作完了後にonEnded(_:)
で実装した。
updating(_:body:)
の実装
updating(_:body:)
の第1引数に@GestureState
なプロパティを渡すと、ジェスチャ操作中の値をそのプロパティから見ることができる。ジェスチャの値は種類ごとに異なり、MagnificationGesture
の場合はtypealias Value = CGFloat
として実装されており、拡大縮小率として使用できる。
updating(_:body:)
の第2引数は@escaping (Self.Value, inout State, inout Transaction) -> Void)
なコールバックになっており、その第1引数は現在のジェスチャの値、第2引数はupdating(:body:)
の第1引数で与えた@GestureState
なプロパティのエイリアス的な変数でinout
指定になっている。この第2引数に値を代入することで、@GestureState
なプロパティに操作中のジェスチャの値が入る。
updating(:body:)
に関係するところを抜き出した実装が以下である。
struct ContentView : View {
@ObservedObject var document : ImageDocument
@GestureState private var scale : CGFloat = 1.0
var magnificationGesture : some Gesture {
MagnificationGesture ()
. updating ( self . $ scale ) { currentValue , gestureState , _ in
if currentValue < 0.1 {
gestureState = 0.1
}
else if currentValue > 5 {
gestureState = 5
}
else {
gestureState = currentValue
}
}
}
var body : some View {
ScrollView ([ . horizontal , . vertical ]) {
Image ( ivImage : document . image )
. scaleEffect ( self . scale )
. gesture ( self . magnificationGesture )
}
}
}
急激な拡大縮小を防ぐため、@GestureState
なプロパティ(のエイリアス的存在であるgestureState
)には値の範囲の制限を加えている。
これでジェスチャ操作中にImage
が拡大縮小表示されるようになるが、問題点がふたつある。
@GestureState
なプロパティはジェスチャ操作が終了すると初期値にリセットされる。したがって、上の実装だけでは、ジェスチャ操作を終えると元の大きさに戻る(.scaleEffect(1.0)
と同義となる)。
scaleEffect(_:anchor:)
は見掛け上の拡大縮小をするだけで、ScrollView
から見たImage
のサイズが変わるわけではない。したがって、最終的な表示サイズ変更をscaleEffect(:anchor:)
だけに任せるとスクロールが狂う。
viewSize
プロパティの実装とframe(width:height:alignment:)
の付与
前項の問題点解消のため、Image
の永続的な表示サイズの変更にはframe(width:height:alignment:)
を用いる。このとき、引数に与える幅と高さを保持するviewSize
プロパティをドキュメントモデルに実装する。
class ImageDocument : ReferenceFileDocument {
...
var image : IVImage
@Published var viewSize : CGSize
init ( image : IVImage = IVImage ()) {
self . image = image
self . viewSize = image . size
}
required init ( configuration : ReadConfiguration ) throws {
guard let data = configuration . file . regularFileContents ,
let image = IVImage ( data : data )
else {
throw CocoaError ( . fileReadCorruptFile )
}
self . image = image
self . viewSize = image . size
}
...
// MARK: -
func scaleViewSize ( _ scale : CGFloat ) {
self . scaleViewSize ( scale , animate : false )
}
func scaleViewSize ( _ scale : CGFloat , animate : Bool ) {
var newViewSize = CGSize ( width : self . viewSize . width * scale , height : self . viewSize . height * scale )
if newViewSize . width < self . image . size . width * 0.2 {
newViewSize . width = self . image . size . width * 0.2
newViewSize . height = self . image . size . height * 0.2
}
else if newViewSize . width > self . image . size . width * 5 {
newViewSize . width = self . image . size . width * 5
newViewSize . height = self . image . size . height * 5
}
if animate {
withAnimation {
self . viewSize = newViewSize
}
}
else {
self . viewSize = newViewSize
}
}
func resetViewSize () {
self . resetViewSize ( animate : false )
}
func resetViewSize ( animate : Bool ) {
if animate {
withAnimation {
self . viewSize = self . image . size
}
}
else {
self . viewSize = self . image . size
}
}
}
viewSize
プロパティの変化をビューに通知して再描画させるため、@Published
を付けた。image
プロパティのsize
で初期化する。また、拡大縮小率を与えてviewSize
プロパティを変化させるメソッドも実装した。拡大縮小されすぎないような制限も加えている。
このviewSize
プロパティを用いてImage
の表示サイズを変更する。
Image ( ivImage : document . image )
. resizable ()
. aspectRatio ( contentMode : . fit )
. scaleEffect ( self . scale )
. frame ( width : self . document . viewSize . width ,
height : self . document . viewSize . height )
. gesture ( magnificationGesture )
表示サイズの変更にframe(width:height:alignment:)
を用いた。またサイズ変更可能にするため、resizable(capInsets:resizingMode:)
を併せて付与する。今回のサイズ変更はかならず縦横同率で変更するので必要ないがaspectRatio(_:contentMode:)
も一応つけておく。
この段階では、まだ実際にviewSize
を変化させる処理の呼び出しがないので、永続的なサイズ変更はされない。
onEnded(_:)
の実装
実際の表示サイズ変更処理の呼び出しをonEnded(_:)
で実装する。
var magnificationGesture : some Gesture {
MagnificationGesture ()
. updating ( self . $ scale ) { currentValue , gestureState , _ in
...
}
. onEnded { finalValue in
var scale = finalValue
if scale < 0.1 {
scale = 0.1
}
else if scale > 5 {
scale = 5
}
self . document . scaleViewSize ( scale )
}
}
onEnded(_:)
の第1引数は@escaping (Self.Value) -> Void
なコールバックでジェスチャ操作完了時に呼ばれる。その第1引数にはジェスチャの最終的な値が入っている。この最終値はupdating(_:body:)
で加えた制限とは関係がないので、改めて値の制限を行う。
ここまでで、ジェスチャ操作の実装は完了である。
次回はツールバーを実装する。
2021年08月23日, 編集履歴
SwiftUIでDocument-Based Appな画像閲覧アプリを習作した。PNG/JPEG画像を開いて閲覧、スクロール、ピンチジェスチャで拡大縮小、ツールバーにボタン配置等を実装した。その覚書その1。
このシリーズの他のブログは、
プロジェクトはGitHubで公開している。
ビルド環境は、
macOS 11.5.2
Xcode 12.5.1
である。
Document Appテンプレートでプロジェクト作成
SwiftUIでDocument-Based Appなプロジェクトを作成するには「Document App」テンプレートを使用する。今回はmacOS/iOS両対応で作りたいので、「Multiplatform」の「Document App」テンプレートを選択してプロジェクトを作成する。
Info.plistの設定
Info.plistファイルで、アプリが読み書きできるファイル形式を以下の項目で指定する。
Document Types(CFBundleDocumentTypes
)
Imported Type Identifiers(UTImportedTypeDeclarations
)
Exported Type Identifiers(UTExportedTypeDeclarations
)
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 }
を実装する。
NSImage
、UIImage
を一律に扱う
今回のアプリはmacOSとiOSの両対応にしたい。それぞれの環境で画像を表すオブジェクトであるNSImage
、UIImage
を一律に扱うため、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
で実装する。この場合、
typealias Snapshot
の定義
init(configuration:)
の実装
func fileWrapper(snapshot:configuration:)
の実装
func snapshot(contentType:)
の実装
が必要である。
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
}
}
読み込み処理は、単純に画像を読み込んでIVImage
(NSImage
、UIImage
)型の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 ())
}
}
今回のドキュメントモデルで採用したReferenceFileDocument
はObservableObject
に適合しているので、プロパティとして保持するときは@ObservedObject
で受ける。大きな画像を開いたときにスクロールできるように、Image
をScrollView
に内包させた。
ここまでで、とりあえずPNG/JPEG画像を閲覧するアプリが動く。
Document-Based Appなので、複数のウィンドウで別々の画像を開いたり、複数開いたウィンドウをタブでひとつのウィンドウにまとめたりできる。
次回 はピンチイン/アウトジェスチャによる画像の拡大縮小の実装を行う。
2021年07月22日, 編集履歴
Murasaki ver. 2.4をリリースしました。macOS用のEPUBリーダアプリです。
前回からの主な変更点は、
サイドバー開閉に関する問題を修正
ツールバーボタンの位置に関する問題を修正
環境設定ウィンドウを修正
Spotlight/Quick Lookプラグインを廃止
です。
今回から対応OSはmac OS Big Sur以降 になります。また、Spotlight/Quick Lookプラグインは廃止 になります。