I'm running into this error in the title, but perhaps there's a better solution for the problem I have. There's too much code to post... but I have a parent view that shows a list of child views:
var body: some View {
ScrollView(.vertical) {
VStack(spacing: 0.0) {
ForEach(self.messagesModel.messages, id: \.self) { msg in
MessageView(msg: msg)
}
}
}
}
But some and only some of the MessageView's are a UIViewRepresentable
wrapping a WKWebView, which means in order to determine their height, I have to let them render to the screen, wait for them to settle, and then inspect their needed size.
We don't want the user watching the view jumping around as this is happening, so we wrap the above ScrollView
in a ZStack and plaster an annoying progressView over top of the ScrollView
. It'd also be nice to automatically scroll the ScrollView to an appropriate MessageView, and we need to wait for the scrollview's size to stop changing for that to be effective. Both of these actions require the parent view to be informed when all (if any) of the child views have finally settled down.
The standard way of communicating this information from the child up to the parent is apparently a PreferenceKey. So we push the UIViewRepresentable
one layer deeper, add a callback to be informed when the rendering is done, then update the preference:
struct MessageView: View {
@State var isWebViewRendering: Int
@State var myHeight: CGFloat
init (...) {
_isWebViewRendering = State(initialValue: 1)
}
var body: some View {
MessageWebView(...) { height in
self.myHeight = height
self.isWebViewRendering = 0
}.preference(key: RenderedPreferenceKey.self, value: isWebViewRendering)
.frame(width: nil, height: myHeight, alignment: .top)
}
}
struct RenderedPreferenceKey: PreferenceKey {
static var defaultValue: Int = 0
static func reduce(value: inout Int, nextValue: () -> Int) {
value = value + nextValue() // sum all those remain to-be-rendered
}
}
And then the parent adds the onPreferenceChange
:
var body: some View {
ZStack {
ScrollView(.vertical) {
VStack(spacing: 0.0) {
ForEach(self.messagesModel.messages, id: \.self) { msg in
MessageView(msg: msg)
}
}.onPreferenceChange(RenderedPreferenceKey.self) { remainingToBeRendered in
guard remainingToBeRendered == 0 else { return }
// all the fun UI behaviors can go now
}
}
TwiddleYourThumbsForSwiftUI()...
}
}
The preference change is fired numerous times on the count down to zero, but the Bound preference RenderedPreferenceKey tried to update multiple times per frame
error stops it from firing. This is fine unless the error occurs during the final execution when the sum of all the preferences would be zero.
Any ideas what to do?
I have ensured that the callback from MessageWebView is only called once per view and that all callbacks are properly dispatched on the main thread as needed, so that it seems like multiple views are publishing to the preference key "simultaneously" and causing this problem. But it also seems like it's designed for that...so not sure what the issue is.
I have considered trying to pass this responsibility to the view models governing this, but it's such a strictly UI related function that this feels most appropriate. But, if anyone thinks that's the better solution, I'm game. I'd have to capture the ScrollReader and pass it out to the model to make it work, though.
At every turn, I wished I had stuck with UIKit. Everything feels like a hack in SwiftUI.
I also considered making the parent view a single WKWebView, allowing for only a single preference update, but with the amount of interaction needed with each message, React Native or Cordova would have made more sense, and nobody wants that.