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