SwiftUIでImageに.resizableをつけると、画像のサイズを調整してくれますが、表示サイズが分からなくなりがちです。
表示サイズがわからないと画像上の特定位置に何かを表示しようと思っても、難しくなってしまいます。
ということで、リサイズされた画像のサイズの取得方法の説明です。
Sponsor Link
resizable
画像を.resiableを設定して表示すると、ウィンドウサイズに応じた表示が行われます。
便利なのですが、以下のように、異なる表示サイズで表示されているかもしれず、そのほかの要素の表示位置に気を付けなければいけません。
特に、絶対値指定の位置は、その時の表示サイズに応じて調整することが必要になります。
Preferenceを使います
SwiftUIでは、Preferenceという概念が導入されていて、子Viewから親Viewへ情報を渡すことができます。
親Viewでは、.onPreferenceChangeでpreferenceが変更された時の振る舞いを指定することができます。
今回は、このPreferenceと表示情報を取得するためのGeometryReaderを組み合わせていきます。
Preference定義
以下のような PreferenceData と PreferenceKey を使って、子Viewから親Viewに情報を伝達します。
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 の中で以下のように使っていきます。
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(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を調整することができるようになっています。
説明は以上です。
Sponsor Link