[SwiftUI]Resizableな画像の特定の位置に表示する

SwiftUI

SwiftUIでImageに.resizableをつけると、画像のサイズを調整してくれますが、表示サイズが分からなくなりがちです。

表示サイズがわからないと画像上の特定位置に何かを表示しようと思っても、難しくなってしまいます。

ということで、リサイズされた画像のサイズの取得方法の説明です。

resizable

画像を.resiableを設定して表示すると、ウィンドウサイズに応じた表示が行われます。

便利なのですが、以下のように、異なる表示サイズで表示されているかもしれず、そのほかの要素の表示位置に気を付けなければいけません。
SmallSize

LargeSize

特に、絶対値指定の位置は、その時の表示サイズに応じて調整することが必要になります。

Preferenceを使います

SwiftUIでは、Preferenceという概念が導入されていて、子Viewから親Viewへ情報を渡すことができます。

親Viewでは、.onPreferenceChangeでpreferenceが変更された時の振る舞いを指定することができます。

今回は、このPreferenceと表示情報を取得するためのGeometryReaderを組み合わせていきます。

Preference定義

以下のような PreferenceData と PreferenceKey を使って、子Viewから親Viewに情報を伝達します。

PreferenceData, PreferenceKey 定義

struct FrameViewRectPreferenceData: Equatable {
    let name: String
    let rect: CGRect
}

struct FrameViewRectPreferenceKey: PreferenceKey {
    typealias Value = [FrameViewRectPreferenceData]
    
    static var defaultValue:[FrameViewRectPreferenceData] = []
    
    static func reduce(value: inout [FrameViewRectPreferenceData], nextValue: () -> [FrameViewRectPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

Preferenceをセットするタイミング

Preferenceをセットする、つまり、GeometryReader を .background modifier の中で以下のように使っていきます。

PreferenceSetter

struct FrameViewRectPreferenceSetter: View {
    let prefName: String
    var body: some View {
        GeometryReader { geom in
            Rectangle()
                .fill(Color.clear)
                .preference(key: FrameViewRectPreferenceKey.self,
                            value: [FrameViewRectPreferenceData(name: self.prefName, rect: geom.frame(in: .named("mainZStack")))])
        }
    }
}
Image

                Image(nsImage: self.nsImage)
                    .resizable()
                    .scaledToFit()
                    .background(FrameViewRectPreferenceSetter(prefName: "ImageView"))

上記のコードでImageビューのRect情報が、親ViewへPreference経由で渡されます。

Preferenceを取得するタイミング

Preferenceが変更された時のタイミングで実行される .onPreferenceChange で Preference を取得し、必要な処理を行います。

コード

.onPreferenceChange(FrameViewRectPreferenceKey.self, perform: { prefs in
    for pref in prefs {
        if pref.name == "ImageView" {
            self.photoRect = pref.rect
            self.frameOffset = CGSize(width: self.photoRect.size.width / 2, height: self.photoRect.size.height / 2)
        }
    }
})

Imageのサイズが変更されても、その中央に、Rectangleが表示されるサンプルコード

ここまでのコードをまとめると以下となります。

コード

struct ContentView: View {
    @State var nsImage:NSImage = NSImage(contentsOf: Bundle.main.url(forResource: "View", withExtension: "jpg")!)!
    
    @State private var showingFrame = true
    @State private var frameOffset = CGSize.zero
    @State private var frameSize = CGSize(width: 10, height: 10)
    
    @State private var photoRect: CGRect = CGRect.zero
    
    var body: some View {
        VStack {
            ZStack(alignment: .topLeading) {
                Image(nsImage: self.nsImage)
                    .resizable()
                    .scaledToFit()
                    .background(FrameViewRectPreferenceSetter(prefName: "ImageView"))
                if showingFrame {
                    Rectangle()
                        .fill(Color.red)
                        .frame(width: frameSize.width, height: frameSize.height)
                        .offset(frameOffset)
                }
            }
            .onPreferenceChange(FrameViewRectPreferenceKey.self, perform: { prefs in
                for pref in prefs {
                    if pref.name == "ImageView" {
                        self.photoRect = pref.rect
                        self.frameOffset = CGSize(width: self.photoRect.size.width / 2, height: self.photoRect.size.height / 2)
                    }
                }
            })
            .coordinateSpace(name: "mainZStack")
        }
    }

}
struct FrameViewRectPreferenceData: Equatable {
    let name: String
    let rect: CGRect
}

struct FrameViewRectPreferenceKey: PreferenceKey {
    typealias Value = [FrameViewRectPreferenceData]
    
    static var defaultValue:[FrameViewRectPreferenceData] = []
    
    static func reduce(value: inout [FrameViewRectPreferenceData], nextValue: () -> [FrameViewRectPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

struct FrameViewRectPreferenceSetter: View {
    let prefName: String
    var body: some View {
        GeometryReader { geom in
            Rectangle()
                .fill(Color.clear)
                .preference(key: FrameViewRectPreferenceKey.self,
                            value: [FrameViewRectPreferenceData(name: self.prefName, rect: geom.frame(in: .named("mainZStack")))])
        }
    }
}

まとめ: Preferenceを使うと子Viewの情報を親Viewで使うことができます

今回は、子ViewのRect情報を親Viewで使う例になっていますが、このPreferenceという仕組みを使うと、子Viewからの情報を親Viewで使うことができます。

この仕組みと、GeometryReaderを使うことで、子ViewのRect情報を使って、親Viewで別のViewを調整することができるようになっています。

説明は以上です。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です