Reputation: 766
I am building a widget that will hold some text that is a list of short words and phrases. Suppose the text content is
Foo
Bar
Baz
Bewildered
Add another
And one more
The current result is this:
Because it's a list of short items it would work best if it could wrap into two columns, like below
Foo Add another
Bar And one more
Baz
Bewildered
Here's the current simple code (with font and spacings removed):
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)
}
}
}
}
I found this guide to tell me whether or not the text is truncated, but what I need is to know what text has been truncated so that I can add another Text
view to the right with the remaining characters. Or ideally use some native method to continue the text between two text views.
Upvotes: 2
Views: 697
Reputation: 3525
This can be done using a frame modifier.
Try to create an HStack
and each view in it will get the same frame modifier as .frame(minWidth: 0, maxWidth: .infinity)
.
This will equally distribute the views.
Looking at your code I think this could work.
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")
// your entry.content needs to be formatted to an HStack
HStack {
Text(entry.content)
.frame(minWidth: 0, maxWidth: .infinity)
Text(entry.content)
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
}
See this article too:
SwiftUI: Two equal width columns
Upvotes: 0
Reputation: 766
This is certainly not ideal but here's what I came up with. The gist is that I used the truncated text paradigm linked in the question to get the available height. Then I use the width of the widget minus padding to iterate through the text until it can no longer fit in half the width.
Some downsides are that (1) The left column must be half or less than the width of the widget, when in reality it could sometimes fit more content if it was greater, (2) it is difficult to be 100% certain the spacings are all accounted for, and (3) had to hardcode the dimensions of the widget.
In any case, hope this helps anyone looking for a similar solution!
Here's the code with spacings and colors removed for clarity:
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))
}
}
}
}
}
Upvotes: 1