Delegate Method Causes infinite Loop, and View Modifier Doesn't Update @State
I'm attempting to devise a way to measure the sizes of particular views in my computed body variable, and successfully created a means for doing so:
extension View {
@MainActor func viewSize() -> CGSize {
var viewSize: CGSize = CGSize(width: 0, height: 0)
let sizeRenderer = ImageRenderer(content: self)
sizeRenderer.render { size, context in
viewSize.width = size.width
viewSize.height = size.height
}
return viewSize
}
}
protocol SizeMaster {
var bodyContentSizes: [CGSize] { get set }
func addViewSizeToCollection(_ size: SizeMaster)
}
struct ViewSizeSender<Content:View>: View {
var sizeMaster: SizeMaster
var view: Content
var body: some View {
view
.onAppear {
sizeMaster.addViewSizeToCollection(view.viewSize())
}
}
init(sizeMaster: SizeMaster, view: () -> Content) {
self.sizeMaster = sizeMaster
self.view = view()
}
}
struct ContentView: View, SizeMaster {
func addViewSizeToCollection(_ size: CGSize) {
bodyContentSizes.append(size)
}
@State var bodyContentSizes = [CGSize]()
var body: some View {
ViewSizeSender(sizeMaster: self) {
Text("Size me up!")
}
.onAppear {
print(bodyContentSizes)
}
}
}
(I did notice that using this approach with multiple views causes the size for each view to be added to the array in reverse order — starting with the view at the bottom of the body block, and ending with the view at top of the body block. Wrapping all these views in something like a Group or VStack, and then putting an .onAppear() on that Group or VStack to print the contents of the bodyContentSizes array shows that the array is empty when that Group or VStack appears, so I can't make a .reversed() call on bodyContentSizes there. I can do that if I apply the .onAppear() to the first of these views that I wrap in the ViewSizeSender, though. I'm just a little nervous about doing that since I'm not sure that the body will always load its contents in that way. The most foolproof approach would probably be to assign each view an index number, and then have the bodyContentSizes array sorted by the views' index value every time a new view size is added. That's also a super wasteful approach...)
Returning back to the main question, I'd decided that having to wrap every view in this ViewSizeSender is a bit cumbersome, so I decided to try refactoring my approach so that my ViewSizeSender was a view modifier that I could apply directly to my view. I replaced ViewSizeSender with the following extension:
extension: View {
@MainActor func sendSizeToSizeMaster(sizeMaster: SizeMaster) -> some View {
sizeMaster.addViewSizeToCollection(self.viewSize()) return self
}
}
and then refactored my ContentView:
struct ContentView: View, SizeMaster {
func addViewSizeToCollection(_ size: CGSize) {
bodyContentSizes.append(size)
}
@State var bodyContentSizes = [CGSize]()
var body: some View {
Text("Size me up!")
.sendSizeToSizeMaster(sizeMaster: self)
.onAppear {
print(bodyContentSizes)
}
}
}
but now the print statement is showing that my bodyContentSizes is empty. I put print statements in my ContentView's addViewSizeToCollection method to try an diagnose the problem:
func addViewSizeToCollection(_ size: CGSize) {
print("Size: \(size)")
bodyContentSizes.append(size)
print(bodyContentSizes)
}
The first print statement shows that the view's size goes through as expected, but the second print shows that the bodyContentSizes array is empty. (Adding these print statements inside the view extension method gives the same results.)
I added an .onChange(of:) modifier in place of my view's .onAppear() to see whether the array was somehow getting updated after the UI had finished updating, but that block never ran. I also tried using an ObservableObject class to act as the SizeMaster in place of my ContentView:
@MainActor protocol SizeMaster {
var bodyContentSizes: [CGSize] { get set }
func addViewSizeToCollection(_ size: SizeMaster)
}
@MainActor class ViewSizeRenderer: ObservableObject, SizeMaster {
@Published var bodyContentSizes = [CGSize]()
func addViewSizeToCollection(_ size: CGSize) {
print(size)
bodyContentSizes.append(size)
print(bodyContentSizes)
}
}
struct ContentView: View {
@StateObject var renderer = ViewSizeRenderer()
var body: some View {
Text("Size me up!")
.sendSizeToSizeMaster(sizeMaster: self)
.onAppear {
print(bodyContentSizes)
}
}
}
and was surprised to discover that I started having an infinite print loop where the printed array generated by print(bodyContentSizes) began to grow endlessly larger. (This happens regardless of whether or not my renderer is an @StateObject or @ObservedObject, or remove the @MainActor from the protocol and my class.)
Does anyone have any guesses as to what's going on?