I currently have a fairly hacky solution to ensuring (albeit, not well) that quote text from my API calls fit within my widget. I'm wondering if there is a more robust way to ensure that I'm only displaying quotes that'll fit within the user's widget, such that the text, author, like button, and like count all fit.
I currently call this getRandomQuoteByClassification
method to fetch a quote from my API, until it finds a quote that'll fit. The values I use are entirely arbitrary, and I've had to play around with them.
while isQuoteTooLong(text: quote.text, context: context, author: quote.author) {
// Fetch a new quote
getRandomQuoteByClassification(classification: data.getQuoteCategory().lowercased()) { newQuote, _ in
if let newQuote = newQuote {
quote = newQuote
}
}
}
The current (bad) way I'm checking for whether the quote fits in the widget is this:
// Helper function to check if a quote is too long
private func isQuoteTooLong(text: String, context: Context, author: String?) -> Bool {
let maxWidth: CGFloat = {
switch context.family {
case .systemSmall:
return 20
case .systemMedium:
return 200
case .systemLarge:
return 300
case .systemExtraLarge:
return 400
case .accessoryCircular:
return 120
case .accessoryRectangular:
return 180
case .accessoryInline:
return 100
@unknown default:
return 100
}
}()
var maxHeight: CGFloat = {
switch context.family {
case .systemSmall:
return 20
case .systemMedium:
return 40
case .systemLarge:
return 100
case .systemExtraLarge:
return 200
case .accessoryCircular:
return 120
case .accessoryRectangular:
return 180
case .accessoryInline:
return 60
@unknown default:
return 20
}
}()
// Check if the author is going to take up 2 lines and adjust the maxHeight accordingly
// adjusted
if let author = author, (!author.isEmpty && author != "Unknown Author" && author != "" && author != "NULL") {
let authorFont = UIFont.systemFont(ofSize: 14) // Use an appropriate font size for the author
let authorBoundingBox = author.boundingRect(
with: CGSize(width: maxWidth, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin],
attributes: [NSAttributedString.Key.font: authorFont],
context: nil
)
if authorBoundingBox.height > maxHeight / 2 {
maxHeight = maxHeight * 0.85 // Adjust the factor as needed
}
}
let font = UIFont.systemFont(ofSize: 16) // Use an appropriate font size
let boundingBox = text.boundingRect(
with: CGSize(width: maxWidth, height: maxHeight),
options: [.usesLineFragmentOrigin],
attributes: [NSAttributedString.Key.font: font],
context: nil
)
// Check if the quote has an author
// adjusted
if let author = author, (!author.isEmpty && author != "Unknown Author" && author != "" && author != "NULL") {
return boundingBox.height > maxHeight
} else {
// Allow the quote to be 5% longer when there is no author
let maxAllowedHeight = maxHeight * 1.05
return boundingBox.height > maxAllowedHeight
}
}
As you can read in the following code for the View
, only widgets with a size greater than .systemSmall
will get the like button and count (intended):
Text("\(widgetQuote.text)")
.font(Font.custom(availableFonts[data.selectedFontIndex], size: 16)) // Use the selected font
.foregroundColor(colors[1]) // Use the second color for text color
.padding(.horizontal, 10)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 5, trailing: 0))
if family == .systemSmall {
if (widgetQuote.author != "Unknown Author" && widgetQuote.author != nil && widgetQuote.author != "" && widgetQuote.author != "NULL") {
Text("— \(widgetQuote.author ?? "")")
.font(Font.custom(availableFonts[data.selectedFontIndex], size: 14)) // Use the selected font for author text
.foregroundColor(colors[2]) // Use the third color for author text color
.padding(.horizontal, 10)
}
} else {
HStack {
if (widgetQuote.author != "Unknown Author" && widgetQuote.author != nil && widgetQuote.author != "" && widgetQuote.author != "NULL") {
Text("— \(widgetQuote.author ?? "")")
.foregroundColor(colors[2]) // Use the third color for author text color
.padding(.horizontal, 10)
}
if #available(iOSApplicationExtension 17.0, *) {
Button(intent: LikeQuoteIntent()) {
Image(systemName: isLiked ? "heart.fill" : "heart")
.foregroundStyle(colors[2])
}
} else {
Image(systemName: isLiked ? "heart.fill" : "heart")
.foregroundStyle(colors[2])
}
Text("\(widgetQuote.likes ?? 69)")
.foregroundColor(colors[2])
}
.padding(.horizontal, 5)
.font(Font.custom(availableFonts[data.selectedFontIndex], size: 14))
}
So, how can I make my isQuoteTooLong
method more robust? Currently, it's very prone to mishaps, as such (there should never be quotes too long, because then they're cut off and ellipses show):
You can try font and minimumScaleFactor to achieve similar result.
You can check this example to see that the text size is dynamic depending on the length of the text and size of the parent view.
VStack(alignment: .leading) {
Text(text)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
}
.frame(width: 300, height: 500)
.background(.red)
.onTapGesture {
text = "This is my long text which is very long"
}
This is the main code that you have to use inside any view. Other things are just to show example.
Text(text)
.font(.system(size: 500))
.minimumScaleFactor(0.01)
You can play around with the text size as per your needs. Make sure to keep it on larger size and the text will scale accordingly.