ios - 将 SwiftUI 文本包装成两列
问题描述
我正在构建一个小部件,它将包含一些文本,这些文本是短词和短语的列表。像这样的东西:
因为它是一个简短的项目列表,所以如果它可以包装成两列,效果最好。
这是当前的简单代码(删除了字体和间距):
struct WidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
ZStack {
Color(entry.color)
VStack(alignment: .leading) {
Text(entry.name)
Text("Updated in 6 hours")
Text(entry.content)
}
}
}
}
我发现本指南可以告诉我文本是否被截断,但我需要知道哪些文本已被截断,以便我可以Text
在右侧添加另一个带有剩余字符的视图。或者理想情况下使用一些本机方法在两个文本视图之间继续文本。
解决方案
这当然不是理想的,但这就是我想出的。要点是我使用问题中链接的截断文本范例来获得可用高度。然后我使用小部件的宽度减去填充来遍历文本,直到它不再适合一半宽度。
一些缺点是(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))
}
}
}
}
}
推荐阅读
- typescript - 如何在 Typescript 中创建对象以避免初始化 GET 属性?
- neo4j - neo4j 递归节点数
- python - Spyder 无响应
- linux - sudo openvpn --config 在linux上什么都不输出
- react-native - 硬件背压上可能未处理的承诺拒绝
- java - Google Cloud Objectify - 保存实体时出错
- typescript - 使用装饰器正确扩展构造函数
- amazon-web-services - 推荐人政策:no-referrer-when-downgrade fetch api - AWS API
- javascript - 嵌套条件下的多个 .innerHTML
- apache-kafka - Kafka Stream固定窗口不按键分组