我一直在使用ScrollView看到偏好键的一些奇怪行为。如果我将onPreferenceChange
放在ScrollView
内,它将不会被调用,但是如果我将其放置在struct WidthPreferenceKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
之外,它将被调用!
我已经按照以下步骤设置了宽度首选项键:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
.onPreferenceChange(WidthPreferenceKey.self) {
print($0) // Not being called, we're in a scroll view.
}
}
}
}
以下简单视图不显示:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
但这可行:
onPreferenceChange
我知道我可以使用后一种方法来解决此问题,但有时我处于无法访问其父滚动视图但仍要记录首选项键的子视图中。
关于如何在ScrollView
内部被叫Bound preference WidthPreferenceKey tried to update multiple times per frame.
的想法?
注意:将函数放在滚动视图中时,我得到
`var wentOffline, wentOnline; function handleConnectionChange(event){ if(event.type == "offline"){ console.log("You lost connection."); wentOffline = new Date(event.timeStamp); } if(event.type == "online"){ console.log("You are now back online."); wentOnline = new Date(event.timeStamp); console.log("You were offline for " + (wentOnline — wentOffline) / 1000 + "seconds."); } } window.addEventListener('online', handleConnectionChange); window.addEventListener('offline', handleConnectionChange);`
,这可能会说明发生了什么,但我无法弄清楚。
谢谢!
答案 0 :(得分:12)
尽管我使用的方法只是解决方法之一,但我一直在努力找出这个问题,并且找到了解决方法。
使用
onAppear
至ScrollView
带有标志,以使其子代出现。
...
@State var isShowingContent = false
...
ScrollView {
if isShowingContent {
ContentView()
}
}
.onAppear {
self.isShowingContent = true
}
或者,
使用List
代替它。
它具有滚动功能,您可以使用自己的功能和UITableView外观(在UI方面)对其进行自定义。最重要的是它能按预期工作。
[如果您有时间阅读更多内容]
让我说说我对这个问题的看法。
我已经确认,放置在onPreferenceChange
内的视图的 bootstrap 时间没有调用ScrollView
。我不确定这是否是正确的行为。但是,我认为这是错误的,因为ScrollView
必须能够包含任何视图,即使其中一些视图使用PreferenceKey在其中的视图之间传递任何数据。如果这是正确的行为,当我们尝试创建自定义视图时,我们很容易遇到麻烦。
让我们更加详细。
我猜想ScrollView
的工作方式与其他容器视图(例如List,(H / V)Stack)在引导时设置其子视图时略有不同。换句话说,ScrollView
会尝试以自己的方式吸引(或布置)孩子。不幸的是,这种方式会影响儿童的布局机制,使其无法正常运行。我们可以在调试视图中猜测以下消息的发生情况。
TestHPreferenceKey tried to update multiple times per frame.
可能有证据告诉我们,ScrollView
正在对其设置进行某些操作时发生了子代更新。那时,可以猜测到对PreferenceKey的更新已被忽略。
这就是为什么我试图将放置子视图的位置放到onAppear
上的原因。
我希望这对在SwiftUI上遇到各种问题的人有用。
答案 1 :(得分:2)
我认为您的示例中的onPreferenceChange没有被调用,因为它的功能与preference(key ...)有很大不同
preference(key:..)为其使用的视图设置首选项值。 而onPreferenceChange是在父视图上调用的函数-父视图位于视图树层次结构中较高位置的视图。它的功能是遍历其所有子代和子代子并收集它们的preference(key :)值。当找到一个值时,它将对这个新值和所有已经收集的值使用PreferenceKey中的reduce函数。收集并减少所有值后,将对结果执行onPreference关闭。
在您的第一个示例中,从未调用此闭包,因为Text(“ Hello”)视图没有设置偏好键值的子级(实际上,该视图根本没有子级)。在第二个示例中,“滚动”视图有一个子级,用于设置其首选项值(“文本”视图)。
所有这些并不能解释每帧错误多次-这很可能是不相关的。
最近更新(24.4.2020): 在类似的情况下,我可以通过更改PreferenceData的Equatable条件来引发onPreferenceChange的调用。 PreferenceData必须是相等的(可能是要检测它们中的变化)。但是,Anchor类型本身已不再等同。要提取包含在Anchor中的值,需要使用GeometryProxy。您可以通过GeometryReader获得GeometryProxy。为了不将视图中的某些视图封装到GeometryReader中来干扰视图的设计,我在PreferenceData结构的equatable函数中生成了一个视图:
struct ParagraphSizeData: Equatable {
let paragraphRect: Anchor<CGRect>?
static func == (value1: ParagraphSizeData, value2: ParagraphSizeData) -> Bool {
var theResult : Bool = false
let _ = GeometryReader { geometry in
generateView(geometry:geometry, equality:&theResult)
}
func generateView(geometry: GeometryProxy, equality: inout Bool) -> Rectangle {
let paragraphSize1, paragraphSize2: NSSize
if let anAnchor = value1.paragraphRect { paragraphSize1 = geometry[anAnchor].size }
else {paragraphSize1 = NSZeroSize }
if let anAnchor = value2.paragraphRect { paragraphSize2 = geometry[anAnchor].size }
else {paragraphSize2 = NSZeroSize }
equality = (paragraphSize1 == paragraphSize2)
return Rectangle()
}
return theResult
}
}
亲切的问候
答案 2 :(得分:1)
您只能在superView中阅读它,但是在设置后可以使用transformPreference
进行更改。
struct ContentView: View {
var body: some View {
ScrollView {
VStack{
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}.transformPreference(WidthPreferenceKey.self, {
$0 = 30})
}.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
现在的最后一个值为30
。希望这就是你想要的。
您可以从其他层阅读
ScrollView {
Text("Hello").preference(key: WidthPreferenceKey.self, value: CGFloat(40.0))
.backgroundPreferenceValue(WidthPreferenceKey.self) { x -> Color in
print(x)
return Color.clear
}
}
答案 3 :(得分:0)
似乎问题不一定与ScrollView
有关,而与您使用PreferenceKey
有关。例如,这是一个示例结构,其中根据PreferenceKey
的宽度设置Rectangle
,然后使用.onPreferenceChange()
打印所有ScrollView
内部。拖动Slider
以更改宽度时,密钥将更新,并执行打印关闭。
struct ContentView: View {
@State private var width: CGFloat = 100
var body: some View {
VStack {
Slider(value: $width, in: 100...200)
ScrollView(.vertical) {
Rectangle()
.background(WidthPreferenceKeyReader())
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
.frame(width: self.width)
}
}
}
struct WidthPreferenceKeyReader: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
如您所述,第一次尝试设置键时,控制台会打印“绑定首选项WidthPreferenceKey试图每帧更新多次”,但是此后立即设置一个真实值,并且它将继续动态更新。
您实际上试图设置什么值,并且您打算在.onPreferenceChange()
中做什么?
答案 4 :(得分:0)
这里的问题实际上不在ScrollView
中,而是在使用中-这种机制允许在viewTree中向上传输数据:
具有多个子视图的视图会自动将其值组合为一个 赋予其祖先可见的单个值。
此处的关键字- with multiple children
。这意味着您可以在viewTree中将它从子级传递到父级。
让我们检查一下您的代码:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
.onPreferenceChange(WidthPreferenceKey.self) {
print($0) // Not being called, we're in a scroll view.
}
}
}
}
您现在可以看到-子级将值传递给自身,而不是父级传递值-因此,按照设计,这不想起作用。
工作情况:
struct ContentView: View {
var body: some View {
ScrollView {
Text("Hello")
.preference(key: WidthPreferenceKey.self, value: 20)
}
.onPreferenceChange(WidthPreferenceKey.self) {
print($0)
}
}
}
在这里,ScrollView
是父母,Text
是孩子,孩子与父母交谈-一切正常。
所以,我一开始就很伤心,这里的问题不在ScrollView中,而是在用法和Apple文档中(您需要像往常一样读几次)。
关于此:
绑定首选项
WidthPreferenceKey
尝试每次更新多次 框架。
这是因为u可能会同时更改乘法值,并且无法渲染View,请尝试使用.receive(on:)
或DispatchQueue.main.async
作为解决方法(我想这可能是一个错误)