Part of the answer to your question lies in the declaration of the
GeometryReader
initializer:
@inlinable public init(@ViewBuilder content: @escaping (GeometryProxy) -> Content)
It takes an
@escaping
block, suggesting that the content won't be fetched until later. In fact, that is exactly how GeometryReader works. Once the layout system has decided on the location and bounds of the GeometryReader view, it will create a
GeometryProxy
instance describing that layout and will invoke the view. So, that's why you're seeing it invoked multiple times.
Why is it only happening when you include the text view, though? In that case, it's because the
GeometryProxy
acts rather like an
@State
variable: SwiftUI keeps an eye on it while running the block, and it can tell if you used it or not. Just as for
@State
values accessed in a
body
implementation, SwiftUI can tell if you actually used some state or not, and will use that knowledge to skip as much work as possible. If a given block didn't look at a certain state value, then if that state value didn't change, it will keep the current value rather than re-invoking the block. Thus by removing the only reference to the
GeometryProxy
in your block, you let SwiftUI know that the value you returned before won't change when the geometry changes, so it doesn't re-create it. When you
do fetch the frame, it knows that your output depends on that state, so it re-invokes the block when the state changes.
Now, as to ways to avoid re-creating the image, you have a few choices. To be honest, since SwiftUI views are really just instructions that may be re-fetched quite promiscuously, instantiating a
UIImage
within your
body
implementation isn't ideal in any case. It would be better where possible to create the
UIImage
ahead of time and pass it as a value into the initializer of a view; this is what the
Image(uiImage:)
initializer does, for example. Alternatively you could implement your view's initializer to do that work at initialization time—though since
View
instances are supposed to be cheap, this makes
init()
more expensive than invoking
body
, so isn't ideal.
Another option might be to have a lazily initialized variable. It would need to be an
@State
variable to get persistent storage (the particular
View
instance that's created doesn't last long; it's copied around, meaning only
@State
variables stay put, since they function as reference types under the hood—that's why you can modify them from your struct without needing a mutating modifier). So you might have something like this:
lazy @State private var image = { << code to load UIImage >> }()
I'm not entirely certain of the semantics there, though—does the
@State
wrap a lazy variable, or is the
@State
wrapper itself lazily initialized? If the former, it'll work. If the latter (as I suspect will be the case) it might or it might not depending on when the structure is copied and how those copies are managed by SwiftUI.
Since you mention CoreData, I think it would be worthwhile to look at transformable attributes in your CoreData model. We used to use these back in the days before
URL
was a built-in attribute type in CoreData's schemas to write a
String
to the data store but have a transformer convert it to and from a
URL
instance for the model values in memory. You change your attribute's type from
Data
to
Transformable
in your schema, and create a
ValueTransformer
subclass to convert between
Data
and your chosen type—in this case,
UIImage
. You then set the metadata for your attribute via the model editor: “Transformer” will be your custom transformer, “Custom Class” will be
UIImage
, and for module select “Current Product Module” from the pull-down menu.
Here's an example value transformer that would do what you want, though if you want to store raw bitmap data there's some ImageIO code in your near future…
import UIKit
/// Call this from `application(_:didFinishLaunchingWithOptions:)` or from
/// the function that initializes your CoreData stack to register the transformer.
func InstallImageTransformer() {
ValueTransformer.setValueTransformer(ImageTransformer(), forName: .imageTransformerName)
}
extension NSValueTransformerName {
/// This string value is what you enter in the CoreData Attribute inspector under “Transformer.”
static let imageTransformerName = NSValueTransformerName(rawValue: "ImageTransformer")
}
class ImageTransformer: ValueTransformer {
/// The type returned from `transformedValue(_:)`.
override class func transformedValueClass() -> AnyClass {
NSData.self // NB: you have to specify a class, so use NSData, not Data (a struct)
}
override class func allowsReverseTransformation() -> Bool {
true
}
/// Convert from UIImage to Data
override func transformedValue(_ value: Any?) -> Any? {
guard let image = value as? UIImage else { return nil }
return image.pngData() // assuming you're storing as PNG
}
/// Convert from Data to UIImage
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
return UIImage(data: data)
}
}