Ios 将SwiftUI文本包装为两列

Ios 将SwiftUI文本包装为两列,ios,swift,swiftui,Ios,Swift,Swiftui,我正在构建一个小部件,它将保存一些短单词和短语列表文本。大概是这样的: 因为它是一个简短项目的列表,所以最好将它包装成两列 以下是当前的简单代码(删除了字体和间距): 结构WidgetEntryView:View{ var条目:Provider.entry var body:一些观点{ ZStack{ 颜色(entry.Color) VStack(对齐:。前导){ 文本(entry.name) 文本(“在6小时内更新”) 文本(entry.content) } } } } 我发现可以告诉我文

我正在构建一个小部件,它将保存一些短单词和短语列表文本。大概是这样的:

因为它是一个简短项目的列表,所以最好将它包装成两列

以下是当前的简单代码(删除了字体和间距):

结构WidgetEntryView:View{ var条目:Provider.entry var body:一些观点{ ZStack{ 颜色(entry.Color) VStack(对齐:。前导){ 文本(entry.name) 文本(“在6小时内更新”) 文本(entry.content) } } } }
我发现可以告诉我文本是否被截断,但我需要知道哪些文本被截断了,这样我就可以在右侧添加另一个
文本
视图以及剩余的字符。或者理想情况下使用一些本机方法在两个文本视图之间继续文本。

这当然不理想,但我想到了以下方法。要点是,我使用了问题中链接的截断文本范例来获得可用的高度。然后我使用小部件的宽度减去填充来迭代文本,直到它不能再容纳一半的宽度

一些缺点是:(1)左列必须是小部件宽度的一半或更小,而实际上,如果它更大,它有时可以容纳更多的内容,(2)很难100%确定间距都被考虑在内,并且(3)必须硬编码小部件的尺寸

在任何情况下,希望这有助于任何人寻找类似的解决方案

以下是为清晰起见删除了间距和颜色的代码:

struct SizePreferenceKey: PreferenceKey {
  static var defaultValue: CGSize = .zero
  static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}

extension View {
  func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
    background(
      GeometryReader {
        geometryProxy in
        Color.clear
          .preference(key: SizePreferenceKey.self, value: geometryProxy.size)
      })
      .onPreferenceChange(SizePreferenceKey.self, perform: onChange)
  }
}

struct TruncableText: View {
  let text: Text
  @State private var intrinsicSize: CGSize = .zero
  @State private var truncatedSize: CGSize = .zero
  let isTruncatedUpdate: (_ isTruncated: Bool, _ truncatedSize: CGSize) -> Void
  var body: some View {
    text
      .readSize { size in
        truncatedSize = size
        isTruncatedUpdate(truncatedSize != intrinsicSize, size)
      }
      .background(
        text
          .fixedSize(horizontal: false, vertical: true)
          .hidden()
          .readSize { size in
            intrinsicSize = size
            if truncatedSize != .zero {
              isTruncatedUpdate(truncatedSize != intrinsicSize, truncatedSize)
            }
          })
  }
}

/**
 - Parameter text: The entire contents of the note
 - Parameter size: The size of the text area that was used to initially render the first note
 - Parameter widgetWidth: exact width of the widget for the current family/screen size
 */
func partitionText(_ text: String, size: CGSize, widgetWidth: CGFloat) -> (String, String)? {
  var part1 = ""
  var part2 = text
  
  let colWidth = widgetWidth / 2 - 32 // padding
  let colHeight = size.height
  
  // Shouldn't happen but just block against infinite loops
  for i in 0...100 {
    // Find the first line, or if that doesn't work the first space
    var splitAt = part2.firstIndex(of: "\n")
    if (splitAt == nil) {
      splitAt = part2.firstIndex(of: "\r")
      if (splitAt == nil) {
        splitAt = part2.firstIndex(of: " ")
      }
    }
    
    // We have a block of letters remaining. Let's not split it.
    if splitAt == nil {
      if i == 0 {
        // If we haven't split anything yet, just show the text as a single block
        return nil
      } else {
        // Divide what we had
        break
      }
    }
    
    let part1Test = String(text[...text.index(splitAt!, offsetBy: part1.count)])
    let part1TestSize = part1Test
      .trimmingCharacters(in: .newlines)
      .boundingRect(with: CGSize(width: colWidth, height: .infinity),
                    options: .usesLineFragmentOrigin,
                    attributes: [.font: UIFont.systemFont(ofSize: 12)],
                    context: nil)
    
    if (part1TestSize.height > colHeight) {
      // We exceeded the limit! return what we have
      break;
    }
    
    part1 = part1Test
    part2 = String(part2[part2.index(splitAt!, offsetBy: 1)...])
  }

  return (part1.trimmingCharacters(in: .newlines), part2.trimmingCharacters(in: .newlines))
}


func getWidgetWidth(_ family: WidgetFamily) -> CGFloat {
  switch family {
    case .systemLarge, .systemMedium:
      switch UIScreen.main.bounds.size {
        case CGSize(width: 428, height: 926):   return 364
        case CGSize(width: 414, height: 896):   return 360
        case CGSize(width: 414, height: 736):   return 348
        case CGSize(width: 390, height: 844):   return 338
        case CGSize(width: 375, height: 812):   return 329
        case CGSize(width: 375, height: 667):   return 321
        case CGSize(width: 360, height: 780):   return 329
        case CGSize(width: 320, height: 568):   return 292
        default:                                return 330
      }
    default:
      switch UIScreen.main.bounds.size {
        case CGSize(width: 428, height: 926):   return 170
        case CGSize(width: 414, height: 896):   return 169
        case CGSize(width: 414, height: 736):   return 159
        case CGSize(width: 390, height: 844):   return 158
        case CGSize(width: 375, height: 812):   return 155
        case CGSize(width: 375, height: 667):   return 148
        case CGSize(width: 360, height: 780):   return 155
        case CGSize(width: 320, height: 568):   return 141
        default:                                return 155
      }
    }
}


struct NoteWidgetEntryView : View {
  
  @State var isTruncated: Bool = false
  @State var colOneText: String = ""
  @State var colTwoText: String = ""
  
  var entry: Provider.Entry

  @Environment(\.widgetFamily) var family: WidgetFamily
  
  var body: some View {
    ZStack{
      Color(entry.color)
      
      VStack {
        
        Text(entry.name)
        Text("Updated 6 hours ago")
        
        if entry.twoColumn {
          if (isTruncated) {
            HStack {
              Text(colOneText).font(.system(size:12))
              Text(colTwoText).font(.system(size:12))
            }
          } else {
            TruncableText(text: Text(entry.content).font(.system(size:12))) {
              let size = $1
              if ($0 && colTwoText == "") {
                if let (part1, part2) = partitionText(entry.content, size: size, widgetWidth: getWidgetWidth(family)) {
                  colOneText = part1
                  colTwoText = part2
                  
                  // Only set this if we successfully partitioned the text
                  isTruncated = true
                }
              }
            }
          }
        } else {
          Text(entry.content).font(.system(size:12))
        }
      }
    }
  }
}