为啥 macOS 中的 SwiftUI 多行换行文本在 Preview 中有效,但在实际应用中无效?

Posted

技术标签:

【中文标题】为啥 macOS 中的 SwiftUI 多行换行文本在 Preview 中有效,但在实际应用中无效?【英文标题】:Why SwiftUI multiline wrapping Text in macOS works in Preview, but not in real app?为什么 macOS 中的 SwiftUI 多行换行文本在 Preview 中有效,但在实际应用中无效? 【发布时间】:2021-03-22 23:48:24 【问题描述】:

我有一个显示图像和文本的 SwiftUI 视图。

import SwiftUI
#if canImport(UIKit)
typealias AppColor = UIColor
#else
typealias AppColor = NSColor
#endif

struct ReportErrorUI: View 

   enum Order 
      case even, odd
   

   let error: String
   let order: Order

   init(error: String, order: Order) 
      self.error = error
      self.order = order
   

   var body: some SwiftUI.View 

      let bgColor = (order == .even ? AppColor.systemGreen : .systemRed).withAlphaComponent(0.5)

      return VStack(alignment: .leading, spacing: 0, content: 
         HStack(alignment: .center, spacing: 8, content: 
            Image("icon.error")
            Text(error).font(.body)
         ).padding(8)
         Color(.magenta).frame(height: 2)
      ).background(Color(bgColor))
   

现在我可以预览了:

import SwiftUI

@available(OSX 11.0, *)
struct ReportErrorUI_Previews: PreviewProvider 

   static let shortText = "Etiam habebis sem dicantur magna mollis euismod."
   static let longText = "Tityre, tu patulae recubans sub tegmine fagi  dolor. Idque Caesaris facere voluntate liceret: sese habere. Unam incolunt Belgae, aliam Aquitani, tertiam."

   static var previews: some SwiftUI.View 
      Group 
         VStack(spacing: 0) 
            ReportErrorUI(error: longText, order: .even)
            ReportErrorUI(error: shortText, order: .odd)
         .previewLayout(.sizeThatFits)
         .background(Color.white).padding().frame(width: 320)
      
   

看起来是这样的:

如您所见,Text 视图按预期工作 - 长文本被换行。

现在我通过NSHostingControllerNSHostingView 将这个ReportErrorUI 与老式NSTextField 多行标签一起添加到现有的AppKit 应用程序中。两个 UI 元素都添加到 NSStackView

class MainViewController: ReusableViewController 

   private lazy var stackView = NSStackView()

   override func setupUI() 
      view.addSubview(stackView)
      stackView.translatesAutoresizingMaskIntoConstraints = false

      stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8).isActive = true
      stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8).isActive = true
      stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 8).isActive = true
      stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8).isActive = true

      stackView.widthAnchor.constraint(greaterThanOrEqualToConstant: 120).isActive = true
      stackView.heightAnchor.constraint(greaterThanOrEqualToConstant: 80).isActive = true

      stackView.orientation = .vertical
      stackView.spacing = 8
      stackView.distribution = .fill
      stackView.alignment = .leading

      // Wrappable multiline label. See: https://***.com/a/35920653/1418981
      let label = NSTextField()
      label.stringValue = "Excepteur sint obcaecat cupiditat non proident culpa. A communi observantia non est recedendum."
      label.setContentHuggingPriority(.init(1), for: .horizontal)
      label.setContentCompressionResistancePriority(.init(1), for: .horizontal)
      label.isEditable = false
      label.backgroundColor = NSColor.magenta.withAlphaComponent(0.4)
      label.isBezeled = false
      label.isSelectable = true
      //> Below 4 lines seems not needed.
      // label.usesSingleLineMode = false
      // label.lineBreakMode = .byWordWrapping
      // label.cell?.wraps = true
      // label.cell?.isScrollable = false
      //<
      stackView.addArrangedSubview(label)

      do 
         let ui = ReportErrorUI(error: "Excepteur sint obcaecat cupiditat non proident culpa. A communi observantia non est recedendum.", order: .even)
         let view = NSHostingView(rootView: ui)
         view.setContentHuggingPriority(.init(1), for: .horizontal)
         view.setContentCompressionResistancePriority(.init(1), for: .horizontal)
         view.setContentCompressionResistancePriority(.required, for: .vertical)
         stackView.addArrangedSubview(view)
      

      do 
         let ui = ReportErrorUI(error: "Excepteur sint obcaecat cupiditat non proident culpa. A communi observantia non est recedendum.", order: .odd)
         let vc = NSHostingController(rootView: ui)
         addChild(vc)
         vc.view.setContentHuggingPriority(.init(1), for: .horizontal)
         vc.view.setContentCompressionResistancePriority(.init(1), for: .horizontal)
         vc.view.setContentCompressionResistancePriority(.required, for: .vertical)
         stackView.addArrangedSubview(vc.view)
      

      do 
         let view = ReusableView()
         view.backgroundColor = .yellow
         view.heightAnchor.constraint(greaterThanOrEqualToConstant: 12).isActive = true
         stackView.addArrangedSubview(view)
      

   

结果老派NSTextField 工作正常 - 多行文本在应用程序窗口调整大小时自动换行。但是 SwiftUI Text 视图中的文本没有被换行。

来自 UIKit 世界的技巧,例如 lineLimit(nil).fixedSize(horizontal: false, vertical: true),要么不起作用,要么破坏应用程序窗口布局。

例如.fixedSize(horizontal: false, vertical: true) 的工作原理 - 应用程序窗口布局已损坏。应用程序窗口不能垂直调整大小。

setContentCompressionResistancePrioritysetContentHuggingPriority 的任意组合用于 horizontalvertical 轴设置在 NSHostingViewNSHostingController 上均无济于事。

如何使多行包装 SwiftUI Text 在 macOS 上正常工作?

【问题讨论】:

【参考方案1】:

macOS 的解决方案。

感谢这篇文章:https://zalogatomek.medium.com/swiftui-missing-intrinsic-content-size-how-to-get-it-6eca8178a71f

struct IntrinsicContentSizePreferenceKey: PreferenceKey 
   static let defaultValue: CGSize = .zero

   static func reduce(value: inout CGSize, nextValue: () -> CGSize) 
      value = nextValue()
   


extension View 
   func readIntrinsicContentSize(to size: Binding<CGSize>) -> some View 
      background(GeometryReader  proxy in
         Color.clear.preference(
            key: IntrinsicContentSizePreferenceKey.self,
            value: proxy.size
         )
      )
      .onPreferenceChange(IntrinsicContentSizePreferenceKey.self) 
         size.wrappedValue = $0
      
   


struct MultilineText: View 

   @State private var textSize: CGSize = .zero
   let text: String

   init(_ text: String) 
      self.text = text
   

   var body: some SwiftUI.View 
      Text(text).readIntrinsicContentSize(to: $textSize).fixedSize(horizontal: false, vertical: true).frame(height: textSize.height)
   

【讨论】:

以上是关于为啥 macOS 中的 SwiftUI 多行换行文本在 Preview 中有效,但在实际应用中无效?的主要内容,如果未能解决你的问题,请参考以下文章

WPFListBox GridViewColumn Header 文字换行文字多行显示

如何使 SwiftUI 文本多行文本对齐从顶部和中心开始

CSS换行文本溢出显示省略号,多行

为啥 SwiftUI 中的 getter 即使多行也不需要使用 return 关键字?

SwiftUI 中的多行可编辑文本字段

为啥相同的 SwiftUI 代码可以在 iOS 上运行而在 macOS 上失败?