Can I use a GeometryReader to make a dynamic text size? - swift

I have an app that has a view made of 8 VStacks of Text elements with an Image at the center. This works great on larger phones and tablets but on smaller phones, the Image gets resized so small so that the text content around it can fit.
I am using hardcoded font sizes. Is it a solution to this using a dynamic font size with the use of GeometryReader so that I can have a small "base" font size that will look decent on small screens and for large screens, I can have a multiplier based on the screen height?

See Option Three for GeometryReader
Yes, and here are three ways to do so (but only one uses GeometryReader).
Option One - #ScaledMetric
SwiftUI has a property wrapper called ScaledMetric - here are the docs: https://developer.apple.com/documentation/swiftui/scaledmetric. This is
A dynamic property that scales a numeric value.
So this means that you could assign your font sizes to variables, make them ScaledMetrics, and then they would auto-adjust. Here is some sample code for how to impliment this first option:
struct ExampleView: View {
#ScaledMetric(relativeTo: .body) var fontSize = 50
var body: some View {
Text("This will be scaled according to the Body font.")
.font(.system(size: fontSize))
}
}
Note
While you don't have to use the relativeTo property, this will ensure it scales according to that font. That property is a Font.TextStyle.
Citation: I used this article by Hacking With Swift for information: https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-scaledmetric-property-wrapper.
Option Two - relativeTo in Font.Custom
SwiftUI provides the .font View Modifier. When initializing a font with this modifier, fill in the relativeTo field. See the docs: https://developer.apple.com/documentation/swiftui/font/custom(_:size:relativeto:). Example:
struct ExampleView: View {
var body: some View {
Text("This will be scaled according to the Body font.")
.font(.custom("Times New Roman", size: 24, relativeTo: .body))
}
}
And once again, that relativeTo property is a is a Font.TextStyle. This will scale the Text according to how that default font would normally scale.
Option Three - GeometryReader
Using a GeometryReader, you can get the width and height of a screen. Docs here: https://developer.apple.com/documentation/swiftui/geometryreader. Here is an article that I used for some information, and has a lot of good stuff: https://www.hackingwithswift.com/quick-start/swiftui/how-to-provide-relative-sizes-using-geometryreader. Here is an example:
struct ExampleView: View {
var body: some View {
GeometryReader{ geo in
Text("This will be scaled by screen height.")
.font(.custom("Times New Roman", size: geo.size.height * 0.05))
}
}
}

Related

Conflicts with custom alignment guides in SwiftUI

In my project I have to use custom alignment guides with nested views. It is quite complex, but I'll try to simplify my case in the following way:
There are 3 possible views: a circle, an upContainer and a downContainer. The two containers are just arrays of other circles/containers. The downContainer aligns only first array to the current line (where there are, for example, the other circles), regardless of what is inside the second array; the upContainer does the opposite. Here's an image to visualize them:
I want to be able to build complex views dynamically using this three elements. Thus I create a data model (which is a simple enum) with nested associated types:
enum Data: Hashable {
case circle
case upContainer([Data], [Data])
case downContainer([Data], [Data])
}
In order to manually align the three elements I create a custom SwiftUI alignment in this way:
extension VerticalAlignment {
struct MyAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
static let myAlignment = VerticalAlignment(MyAlignment.self)
}
I want the circles in a specific container to have the same color, so I create an extension to quickly generate a random color:
extension Color {
static var random: Color {
return Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
Now I am able to create the views corresponding to the three elements:
struct CirclesView: View {
var circles: [Data]
var color: Color = Color.random
var body: some View {
HStack(alignment: .myAlignment) {
ForEach(circles, id: \.self) { value in
switch value {
case .circle:
Circle()
.frame(width: 20, height: 20)
.foregroundColor(color)
case .upContainer(let firstData, let secondData):
UpContainerView(container: (firstData, secondData))
case .downContainer(let firstData, let secondData):
DownContainerView(container: (firstData, secondData))
}
}
}
}
}
struct UpContainerView: View {
var container: ([Data], [Data])
var color = Color.random
var body: some View {
VStack(alignment: .leading) {
CirclesView(circles: container.0, color: color)
CirclesView(circles: container.1, color: color)
.alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
}
}
}
struct DownContainerView: View {
var container: ([Data], [Data])
var color = Color.random
var body: some View {
VStack(alignment: .leading) {
CirclesView(circles: container.0, color: color)
.alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
CirclesView(circles: container.1, color: color)
}
}
}
At this point the problem should be solved. For example, if I wanted to render this image:
I should be able to do it by writing:
struct ContentView : View {
var myData: [Data] = [.circle, .circle, .downContainer(
[.circle, .circle, .circle, .downContainer(
[.circle, .circle, .circle, .circle], [.circle]
)], [.circle, .circle, .upContainer(
[.circle, .circle], [.circle, .circle, .circle, .circle]
)]
)]
var body: some View {
CirclesView(circles: myData)
}
}
However this is the result:
As you can see, the first container (the brown one) is not rendered as downContainer, in fact it is in centrally aligned to the first two circles green circles. This is because even if we set explicit alignment guides for only one of the two arrays, SwiftUI takes into account also the alignment guides of the subviews of the other array (even if we want to ignore them) and there is a conflict.
struct DownContainerView: View {
var container: ([Data], [Data])
var color = Color.random
var body: some View {
VStack(alignment: .leading) {
CirclesView(circles: container.0, color: color)
.alignmentGuide(.myAlignment) { $0[VerticalAlignment.center] }
//We set the explicit alignment guide only for the first array, I am looking for a way to ignore completely the alignment guides of the second one and to calculate the alignment based only on the first one.
CirclesView(circles: container.1, color: color)
}
}
}
To solve the conflict we should simply ignore the alignment guides of the other array.
The actual question becomes "How can we ignore those alignment guides, which are implicitly and automatically calculated by SwiftUI?"
Note: I now that a different solution could be to use PreferenceKeys, but I believe this problem should be solved using only alignment guides, since it is a mere alignment problem. I also thought that a solution could be to dynamically create a new custom alignment guide for every subcontainer, but I don't how to do it (if it is possible in Swift).
First up some background info that might help:
EVERY view has EXACTLY ONE alignment which is an x and y value in the coordinate system of the view. Note there is no name (like .top or .leading) mentioned here, those are just ways to tell the view which x an y value to return as the alignment guide for this layout.
If you don't tell the view which alignment to use, it defaults to .center.
The parent view uses the child alignment guides by calculating where that coordinate is in its own coordinate system and places all the children so the alignments are all coincident. Some parents may ignore the child value and place the child where it wants (such as a HStack which ignores the x value of its children and places the children side by side.)
The OP asks if the problem can be solved with layout alone, or are preferences required?
The answers are: it can be done with layout alone depending on how sophisticated you need to be, but you may need preferences depending on how sophisticated you need to be.
Looking at the OPs example we see that a CirclesView is an alias for a HStack and both up and down containers are aliases for a VStack containing two CirclesViews.
Also the test case only has CirclesViews with Circles and either an UpContainerView or a DownContainerView (never both up and down, which gives a false impression of how well the layout is working, it is not even as good as the OP thinks).
So unrolling the test data, using the HStacks and VStacks directly, using Text with fixed colours so we can tell what is what and putting it all into one view so we can play with alignment gives:
struct SOAlignment: View {
var body: some View {
HStack { //CirclesView (solid gray)
Text("C")
.foregroundColor(Color.blue)
.overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
Text("C")
.foregroundColor(Color.blue)
.overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
VStack(alignment: .leading) { //DownContainerView (black dashes)
HStack { //CirclesView (top gray dashes)
Text("C")
.foregroundColor(Color.red)
Text("C")
.foregroundColor(Color.red)
Text("C")
.foregroundColor(Color.red)
VStack(alignment: .leading) { //DownContainerView (yellow)
HStack {
Text("C")
Text("C")
Text("C")
Text("C")
}
HStack {
Text("C")
}
}
.foregroundColor(Color.yellow)
}
.foregroundColor(Color.red)
.overlay(Rectangle()
.inset(by: 1.0)
.strokeBorder(style: StrokeStyle(lineWidth: 0.5, dash: [2.5,2.5]))
.foregroundColor(Color.gray))
HStack { //CirclesView (bottom gray dashes)
Text("C")
.foregroundColor(Color.red)
Text("C")
.foregroundColor(Color.red)
VStack(alignment: .leading) { //UpContainerView (pink)
HStack {
Text("C")
Text("C")
}
HStack {
Text("C")
Text("C")
Text("C")
Text("C")
}
}
.foregroundColor(Color.lightPink)
}
.overlay(Rectangle()
.inset(by: 1.0)
.strokeBorder(style: StrokeStyle(lineWidth: 0.5, dash: [2.5,2.5]))
.foregroundColor(Color.gray))
}
.overlay(Rectangle().strokeBorder(style: StrokeStyle(lineWidth: 1.0, dash: [5,5])))
}
.border(Color.gray.opacity(0.5))
.padding()
}
}
I have added .leading horizontal alignment so the output is as close to the circle example as possible, but these play no role so can be ignored. We are only interested in the vertical alignment.
We get the following output.
I have put borders around the top level views. The top CirclesView is solid gray, it contains a blue Circle, a blue Circle and a DownContainerView all with black dashes, and the DownContainerView contains two CirclesViews with gray dashes.
None of the views have an alignment, by which I mean every view has exactly one alignment which has the default .center value. It is worth pausing for a second here—we are trying to align circles and they all have alignment guides at their centres but the centres of the blue circles do not align with any of the centres of the other coloured circles! The centres of the two black dashed blue circles are of course aligned with the centre of the black dashed DownContainerView/VStack.
The fact that the circles have been substituted for text hints at a possible solution. This is precisely the same problem that Apple solved with .firstTextBaseline and .lastTextBaseline. We want our top three red circles to align with the top row of the yellow down container, and we want the bottom two red circles to align with the bottom row of the pink up container. We can achieve this by putting .firstTextBaseline on the top gray dashed CirclesView and .lastTextBaseline on the bottom gray dashed CirclesView. Now that those are aligned we can also put .firstTextBaseline on the solid gray CirclesView.
e.g.
HStack(alignment: .firstTextBaseline) { //CirclesView (solid gray)
And we get...
Hooray problem solved! Not quite: we put the alignment property on the parent CirclesView/HStack not the child Up/DownContainer/VStack. The point is illustrated by changing the solid gray view alignment to .lastTextBaseline
HStack(alignment: .lastTextBaseline) { //CirclesView (solid gray)
The black dashed DownContainerView has been turned into a black dashed UpContainerView without any changes to itself!
This is what I hinted at previously about having only one type of container in a CirclesView in the test example data. We need to ensure that CirclesViews with both up and down containers work but with this solution the property on the CirclesView (first or last text baseline) turns all the immediate child container views into one type (either up or down).
What we need here is a custom alignment and the OP’s custom alignment is perfect. So let’s use it and replace all the text baselines with myAlignments.
e.g.
HStack(alignment: .myAlignment) { //CirclesView (solid gray)
We get the same as before with all centre alignments because myAlignment defaults to .center. The black dashed down container still doesn’t know how to choose between all the myAlignments of its children without some help from the developer. We need to explicitly tell the DownContainerView/VStack how to calculate the value of myAlignment which is half a circle hight from its top and then give it that alignment guide. Similarly the UpContainerView/VStack needs a .myAlignment guide half a circle height above its .bottom.
VStack(alignment: .leading) { //DownContainerView
.
.
.
}
.alignmentGuide(.myAlignment) { d in
d[.top] + halfCircleHeight
}
and
VStack(alignment: .leading) { //UpContainerView
.
.
.
}
.alignmentGuide(.myAlignment) { d in
d[.bottom] - halfCircleHeight
}
Now critically the CirclesView specifies what alignment it wants with (alignment: .myAlignment) and the Up/DownContainerView returns where that is with .alignmentGuide(.myAlignment)
This is the result:
Now the eagle eyed amongst you will have noticed that the top row blue, red and yellow circles do not line up exactly, and similarly the bottom red and pink circles do not line up exactly. This is because I have used a constant
private let halfCircleHeight = 6.0
for my calculations (half the default system font height.) Text has a little bit of white space above and below the line that I have not accounted for, and variations in font size would also misalign things slightly.
This is where preferences would come in. If you have different sized Circles or .padding or some other effect you may need to get the Up/DownContainerViews to interrogate their child view preferences to get the correct value to use in their alignment guide calculation. If this was wanted the child would use an anchorPreference to record its centre and the Up/DownContainerView would choose between all the child view preferences and use the appropriate one in its alignment guide calculation. (This is left as an exercise for the reader.) By the way, an anchorPreference is simply a preference that carries with it information about the coordinate system it is defined. The child can set the value in its coordinate system (its own centre) and the parent can read the value in its coordinate system (where that is in the parent view).
In summary there are no conflicts with alignment guides because there is only one value and therefore nothing to conflict with. Hopefully this explains why the OPs two green circles .myAlignment which have defaulted to .center aligns with the VStack (in the DownContainerView) .myAlignment which has also defaulted to .center.

WatchOS ScrollView Doesn't Wrap Text Properly

Right now I am able to see the text I want from my 'articles' if I set a frame with a desired width and height.
If the height is long enough it will show all of the article 'body' but with tons of space. I would like to have it where the frame can adjust it's size based on the size of the text I have so that the scrollview can scroll properly based on the desired text frames for each article body.
import SwiftUI
let articles = [
Article (id: 0, title: "Trump as President", body: "Some very short string for the article at the moment."),
Article (id: 1, title: "Obama as President", body: "He may not have been the worst but was he every really the best? Could he have done more or less from what we know?"),
Article (id: 2, title: "Tanner Fry as President", body: "Who knows how well that would work as if Tanner Fry was the president. However we do know a little bit about him from his experience as a programmer. I mean who can just pick up different langauges just off the bat and start creating code for apps to run on their watch and/or phone? Not too many people know how to do that but you know who does? Tanner Fry does, that's right.")
]
var height = 0
struct ContentView: View {
var body: some View {
// Watch res = 448 - 368
ScrollView(.vertical) {
VStack(spacing: 10){
Text("Your News").font(.title)
ForEach(0..<articles.count) {index in
Text(articles[index].title)
Text(articles[index].body).frame(width: 170, height: 170)
// Text(articles[index].body).lineLimit(50).padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
// Height needs to be variable based on the amount of text in the
// articles description. OR find a wrapper
// We're talking about the frame for the body of text
}
}
}
}
}
I am able to scroll all of my content if my height for the frame of the article.body is long enough. Otherwise it truncates the text. Is there any way to make the height more variable to the text length so that the watchOS works properly when scrolling via the ScrollView? Am I missing something?
Thank you for your time, much appreciated.
Define .lineLimit(x) to what you want to be the maximum of line the Text is able to expand. Then add .fixedSize(horizontal: false, vertical: true) to secure that the size is not shrinking back due the SwiftUI layout engine. See below.
struct ContentView: View {
var body: some View {
// Watch res = 448 - 368
ScrollView(.vertical) {
VStack(spacing: 10){
Text("Your News").font(.title)
ForEach(0..<articles.count) {index in
Text(articles[index].title)
Text(articles[index].body)
.lineLimit(nil)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// Text(articles[index].body).lineLimit(50).padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
// Height needs to be variable based on the amount of text in the
// articles description. OR find a wrapper
// We're talking about the frame for the body of text
}
}
}
}
}

sap.ui.table.Table "VisibleRowCountMode.Auto" mode does not work

I'm having trouble setting the number of rows for a table to automagically fill the available estate of its encapsulating container.
According to the API, setting the visibleRowCountMode property to sap.ui.table.VisibleRowCountMode.Auto should render the table to
"[...] automatically fills the height of the surrounding container.
The visibleRowCount property is automatically changed accordingly. All
rows need the same height, otherwise the auto mode doesn't always work
as expected."
I have used the following code:
var oTable = new sap.ui.table.Table( {
rowHeight : 30,
height : "100%",
// The below property is seemingly ignored... What did I do wrong?
visibleRowCountMode : sap.ui.table.VisibleRowCountMode.Auto
});
...but as you can see in this jsbin example http://jsbin.com/vazuz/1/edit it just shows the default 10 rows, and certainly doesn't "change the visibleRowCount property accordingly" :-(
Anyone has a solution?
Thanks in advance!
=====================
EDIT: Thanks to #matz3's answer below, I was ultimately able to solve this issue.
Setting the surrounding container DIV to 100%, this seems to be ignored. Setting it to a fixed height, however, worked just fine. But what I really wanted, if a user resized the window, the number of available rows needs to be adjusted accordingly. Setting it to a fixed height is therefor not an option...
However, the trick was in some extra CSS: not only the DIV needed to be set to 100% height, also both BODY and HTML (!!) needed to have a height set to 100%:
html, body {
height: 100%
}
div#uiArea {
height: 100%
}
Now, the table spans the full height of the available viewport, and resizing the window adjusts the table rather nicely. See the final working solution here: http://jsbin.com/bosusuya/3/edit
Matz3, thanks for your help!
CSS hacks is a dirty way. In my application I use to bind visibleRowCount to Array.length
For example, if you have model with this data:
[{firstName: 'John', lastName: 'Smith',
{firstName: 'David', lastName: 'Ericsson'}]
You can bind to Array property length like this:
var oTable = new sap.ui.table.Table({
visibleRowCount : '{/length}'
})
[...] automatically fills the height of the surrounding container [...]
Your surrounding container is the view, so you have to set the height of it also to a value (e.g. 100%)
this.setHeight("100%");
And your view will be set into the uiArea-div, so this one also needs a height (e.g. 500px)
<div id="uiArea" style="height:500px"></div>
With these changes it now works as expected
I'm with the same issue. I "resolve" that in this manner. This is not perfect, but it's better than UI5 resizing...
  _resizeTableRow: function () {
var oTable = this.getView().byId("referenceTabId");
var sTop = $('#' + oTable.getId()).offset().top;
var sHeight = $(document).height();
//if there a row, you can take the row Height
//var iRowHeight = $(oTable.getAggregation("rows")[0].getDomRef()).height();
var iRowHeight = 40;
var iRows = Math.trunc((sHeight - sTop ) / iRowHeight);
oTable.setVisibleRowCount(iRows);
   },
Other option is to put the Table in sap.ui.layout.Splitter:

Make Firefox Panel fit content

I'm manually porting an extension I wrote in Chrome over to Firefox. I'm attaching a panel to a widget, and setting the content of that panel as an HTML file. How can I make the panel shrink and grow with the content? There's a lot of unsightly scroll bars and grey background right now.
var data = require("self").data;
var text_entry = require("panel").Panel({
width: 320,
height: 181,
contentURL: data.url("text-entry.html"),
contentScriptFile: data.url("get-text.js")
});
require("widget").Widget({
label: "Text entry",
id: "text-entry",
contentURL: "http://www.mozilla.org/favicon.ico",
panel: text_entry
});
Not setting the height property of the panel makes it quite tall.
You might want to check out this example that resizes the panel based on the document loaded. If you want to resize based on changes to the content size, at least on initial load:
https://builder.addons.mozilla.org/package/150225/latest/
( sorry for the delay in respinding, been afk travelling )

Titanium.UI.Label property height

In my code I am doing this:
var taskLabel = Ti.UI.createLabel({color:'#777', top:3, textAlign:'center', height:'auto', text:task.title});
Ti.API.info('Next info is: taskLabel.height');
Ti.API.info(taskLabel.height);
But, the output from this is:
[INFO] [123,883] Next info is: taskLabel.height
And nothing more, it looks like it breaks silently, but I guess it shouldn't, based on the API.
I am trying to sum some heights of the elements, but I would prefer it behaved like html postion:relative. Anyway, I'd like to read the height in float, how can I achieve that?
You need to set a fixed width when you use an auto height. For example:
var taskLabel = Ti.UI.createLabel({color:'#777', top:3, textAlign:'center', height:'auto', width: 200, text:task.title});
you are not going to get the height until it is actually rendered and added to view or window.
You cant read the height property off like that, if you didn't manually define it.
It has to be added to a view, and then displayed (assuming it doesn't auto display) before Titanium will return anything about the height.
var window = Ti.UI.createWindow();
var taskLabel = Ti.UI.createLabel({color:'#777', top:3, textAlign:'center', height:'auto', text:task.title});
window.add(taskLabel);
window.open();
Ti.API.info('Next info is: taskLabel.height');
Ti.API.info(taskLabel.height);
That should work to show the height.
This should work.
var lbl_obj = Ti.UI.createLabel( { height: 'auto', text:'Test Label', top:10 } );
var height = lbl_obj.toImage().height;
Ti.API.info(height);