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 ifelse 完成的,根据是否有任何项目使用不同的视图。随着模型的更改,按照步骤 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 的编辑?

如何在 UIViewRepresentable SwiftUI 中重新加载集合视图

SwiftUI:如何调整 UIViewRepresentable 输出的大小?