UIViewRepresentable 在 SwiftUI 中的列表单元格中的位置
Posted
技术标签:
【中文标题】UIViewRepresentable 在 SwiftUI 中的列表单元格中的位置【英文标题】:Placement of UIViewRepresentable within list cells in SwiftUI 【发布时间】:2020-01-16 10:34:20 【问题描述】:在 SwiftUI 中将自定义 UILabel
添加到 List
时,单元格重用出现错误,其中某些单元格上的标签根本不可见,而在某些单元格上,它被放置在左上角没有任何考虑单元格的填充。它总是在初始单元格上完美呈现。
使用ScrollView
时不会出现此问题。这是一个已知的错误吗?有没有好的解决方法?
GeometryReader geometry in
List
ForEach(self.testdata, id: \.self) text in
Group
AttributedLabel(attributedText: NSAttributedString(string: text), maxWidth: geometry.size.width - 40)
.padding(.vertical, 20)
struct AttributedLabel: UIViewRepresentable
let attributedText: NSAttributedString
let maxWidth: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel
let label = UILabel()
label.preferredMaxLayoutWidth = maxWidth
label.attributedText = attributedText
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.backgroundColor = UIColor.red
return label
func updateUIView(_ label: UILabel, context: UIViewRepresentableContext<Self>)
【问题讨论】:
我在使用 UIViewRepresentable 中的 UILabel(呈现属性字符串)时遇到了看起来相同的问题。我只看到它发生在 List 中。 我遇到了完全相同的问题! @Chris 你找到解决这个问题的方法了吗?我还试图通过 UIViewRepresentable 将 UIKit 中的自定义 UIView 放置在 SwiftUI 列表中。而且我不知道这个 UIKit 视图是布局的。我以为我不需要指定任何约束,它们只是在列表项的可用空间内调整大小 【参考方案1】:与 ScrollView 或 SwiftUI 错误无关。
我认为您的AttributedLabel
课程有问题。我尝试使用普通的Text
,它工作正常。
List
ForEach(self.testdata, id: \.self) text in
Group
Text(student.name)
.background(Color.red)
.padding(.vertical, 20)
【讨论】:
是的,它适用于任何 SwiftUI 组件,例如Text
。我的AttributedLabel
没有我能看到的任何奇怪的东西(这很简单),而且它只会在重复使用的表格视图单元格上中断 - 其中破损不是在UILabel
实现内部引起的,而是在它的定位。当我记录生成的frame
时,许多人得到(0.0, 0.0, 0.0, 0.0)
,即使他们有内容。【参考方案2】:
确实似乎可以解决这个问题。
第一步是让模型首先返回一个空的项目数组,然后返回实际的更新。这将强制视图更新。然后,在短暂的停顿之后,可以进行实际的更新。对于这种情况,这还不够。单独这样做仍然会导致布局问题。不知何故,该列表(可能由正在积极回收其细胞的UITableView
支持)仍然设法保持状态以某种方式引起了麻烦。所以……
第二步是让视图在没有项目时提供列表以外的东西。这是使用 SwiftUI if
和 else
完成的,根据是否有任何项目使用不同的视图。随着模型的更改,按照步骤 1,每次更新都会发生这种情况。
执行步骤 (1) 和 (2) 似乎可以解决该问题。下面的示例代码还包括视图上的.animation(.none)
方法。这在我的代码中是必需的,但在下面的示例代码中似乎不需要。
此解决方法的一个缺点是您将丢失动画。很明显,如果苹果未来做出改变,它可能无法继续工作。 (不过,也许到那时这个错误已经被修复了。)
import SwiftUI
struct ContentView: View
@ObservedObject var model = TestData()
var body: some View
VStack()
GeometryReader geometry in
// handle the no items case by offering up a different view
// this appears to be necessary to workaround the issues
// where table cells are re-used and the layout goes wrong
// Don't show the "No Data" message unless there really is no data,
// i.e. skip case where we're just delaying to workaround the issue.
if self.model.sampleList.isEmpty
Text("No Data")
.foregroundColor(self.model.isModelUpdating ? Color.clear : Color.secondary)
.frame(width: geometry.size.width, height: geometry.size.height) // centre the text
else
List(self.model.sampleList, id:\.self) attributedString in
AttributedLabel(attributedText: attributedString, maxWidth: geometry.size.width - 40)
.animation(.none) // this MAY not be necessary for all cases
Spacer()
Button(action: self.model.shuffle() ) Text("Shuffle") .padding(20)
struct AttributedLabel: UIViewRepresentable
let attributedText: NSAttributedString
let maxWidth: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> UILabel
let label = UILabel()
label.preferredMaxLayoutWidth = maxWidth
label.attributedText = attributedText
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.backgroundColor = UIColor.red
return label
func updateUIView(_ label: UILabel, context: UIViewRepresentableContext<Self>)
// function required by protoocol - NO OP
class TestData : ObservableObject
@Published var sampleList = [NSAttributedString]()
@Published var isModelUpdating = false
private var allSamples = [NSAttributedString]()
func shuffle()
let filtered = allSamples.filter _ in Bool.random()
let shuffled = filtered.shuffled()
// empty the sampleList - this will trigger the View that is
// observing the model to update and handle the no items case
self.sampleList = [NSAttributedString]()
self.isModelUpdating = true
// after a short delay update the sampleList - this will trigger
// the view that is observing the model to update
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01)
self.sampleList = shuffled
self.isModelUpdating = false
init()
generateSamples()
shuffle()
func generateSamples()
DispatchQueue.main.async
var samples = [NSAttributedString]()
samples.append("The <em>quick</em> brown fox <strong>boldly</strong> jumped over the <em>lazy</em> dog.".fromhtml)
samples.append("<h1>SwiftUI</h1><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p><p>At the time of writing, still very much a <em>work in progress</em>. Normal and <em>italic</em>. And <strong>strong</strong> too.</p>".fromHTML)
samples.append("<h1>Test Cells</h1><p>Include cells that have different heights to demonstrate what is going on. Make some of them really quite long. If they are all showing the list is going to need to scroll at least on smaller devices.</p><p>Include cells that have different heights to demonstrate what is going on. Make some of them really quite long. If they are all showing the list is going to need to scroll at least on smaller devices.</p><p>Include cells that have different heights to demonstrate what is going on. Make some of them really quite long. If they are all showing the list is going to need to scroll at least on smaller devices.</p> ".fromHTML)
samples.append("<h3>List of the day</h3><p>And he said:<ul><li>Expect the unexpected</li><li>The sheep is not a creature of the air</li><li>Chance favours the prepared observer</li></ul>And now, maybe, some commentary on that quote.".fromHTML)
samples.append("Something that is quite short but that is more than just one line long on a phone maybe. This might do it.".fromHTML)
self.allSamples = samples
extension String
var fromHTML : NSAttributedString
do
return try NSAttributedString(data: Data(self.utf8), options: [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue
], documentAttributes: nil)
catch
return NSAttributedString(string: self)
struct ContentView_Previews: PreviewProvider
static var previews: some View
ContentView()
【讨论】:
【参考方案3】:我遇到了类似的问题,并通过使用getTextFrame(text)
向 UIViewRepresentable 添加框架来解决它,
GeometryReader geometry in
List
ForEach(self.testdata, id: \.self) text in
Group
AttributedLabel(attributedText: NSAttributedString(string: text), maxWidth: geometry.size.width - 40)
// add this frame
.frame(width: getTextFrame(text).width height: getTextFrame(text).height)
.padding(.vertical, 20)
func getTextFrame(for text: String, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> CGSize
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.preferredFont(forTextStyle: .body)
]
let attributedText = NSAttributedString(string: text, attributes: attributes)
let width = maxWidth != nil ? min(maxWidth!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
let height = maxHeight != nil ? min(maxHeight!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
let constraintBox = CGSize(width: width, height: height)
let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
return rect.size
【讨论】:
以上是关于UIViewRepresentable 在 SwiftUI 中的列表单元格中的位置的主要内容,如果未能解决你的问题,请参考以下文章
在 SwiftUI 中从 UIViewRepresentable 呈现视图
更新 SwiftUI 中的 inputAccessoryView (UIViewRepresentable)
如何在解雇后停止播放 AVPlayerViewController 作为 UIViewRepresentable?
如何在 SwiftUI 中结束 UIViewRepresentable 的编辑?