SwiftUI: height and spacing of Views in GeometryReader within a ScrollView - layout

ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
Text("Hello There")
.font(.largeTitle)
}
}
}
The above code results in this:
When we add a GeometryReader, the views lose their sizing and spacing from one another (the .position modifier is just to center the text views within the GeometryReader):
ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
GeometryReader { geometry in
Text("Hello There")
.font(.largeTitle)
.position(x: UIScreen.main.bounds.size.width/2)
}
}
}
}
Can anybody share some help/advice around why this is the behavior and what I may want to look into in order to use a GeometryReader on center aligned views within a ScrollView as I'm trying to do here?
Adding a .frame(minHeight: 40) modifier to the ForEach is one way to force the views to take the space that the Text needs, but that is not a very nice or scalable solution.
Thank you!

If you need just width of screen then place GeometryReader outside of scrollview:
GeometryReader { geometry in // << here !!
ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
Text("Hello There")
.font(.largeTitle)
.position(x: UIScreen.main.bounds.size.width/2)
}
}
}
}
Can anybody share some help/advice around why this is the behavior
ScrollView does not have own size so GeometryReader cannot read anything from it and provides for own children some minimal dimension.

Related

Determine actual height of a SpriteKit SKScene inside a SwiftUI View

I have a SwiftUI View with a VStack of 3 SpriteKit SKScenes inside.
While scoreView and accelerometerView have a fixed height, gameScene does not.
It takes the remaining space and depends somehow on the device.
struct ContentView: View {
#StateObject private var gameScene = GameSKScene()
#StateObject private var scoreView = ScoreSKScene()
#StateObject private var accelerometerView = AccelerometerSKScene()
var body: some View {
VStack {
SpriteView(scene: scoreView)
.frame(height: 50)
SpriteView(scene: gameScene)
SpriteView(scene: accelerometerView)
.frame(height: 50)
}.ignoresSafeArea()
}
}
Now inside gameScene have problems setting up the scene because the scene itself has no information of its size.
I can get the width with UIScreen.main.bounds.width but how do I get the actual height?
If I understand you correctly, you want the gameScene SpriteView to fill the remaining space after accounting for the other two scenes. You can use GeometryReader (documentation here).
Example usage:
var body: some View {
GeometryReader { geo in
VStack {
SpriteView(scene: scoreView)
.frame(height: 50)
SpriteView(scene: gameScene)
.frame(height: geo.size.height - 100)
SpriteView(scene: accelerometerView)
.frame(height: 50)
}
}
}

In SwiftUI Picker expands to occupy entire space inside a HStack

I have a Text and Picker in HStack separated by Spacer. What I want is that the Spacer should should occupy maximum space between picker and text so that the Text and Picker are not the extreme ends of the HStack but, the picker expands to occupy maximum space. Any clue as to how can this can achieved and that the size of Picker is equivalent to its content?
HStack() {
Text(template.name)
.multilineTextAlignment(.leading)
Spacer()
if template.oneItemOnly == "1" {
if let items = template.items {
Picker(selection: $selectedSegment, label:Text("")){
ForEach( 0..<items.count) { index in
Text(items[index].name)
.tag(index)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
You can set max width of Spacer by adding a .frame modifier:
Spacer().frame(maxWidth: .infinity)
Alternatively you can try using GeometryReader to set the width of the Picker relative to the screen width:
GeometryReader { proxy in
...
Picker(selection: $selectedSegment, label: EmptyView()) {
...
}
.frame(width: proxy.size.width / 3)
}
You may also try to implement your own Picker like presented here: Custom Segmented Picker

How to stop SwiftUI view from getting wider than superview

I created a UIHostingController to put a SwiftUI view in my app. The root view contains a VStack, and inside that some HStacks and Pickers. The picker pushes the view wider than the screen by 8 points. It looks bad - text is slightly off screen. Usually I would fix this with auto layout constraints that pin the left and right edges of the view to the superview, with priority 1000 (required). What is the equivalent in SwiftUI?
Some boiled down code that triggers the problem
struct EditSegmentView: View {
#State var seconds: Int = 10
var body: some View {
VStack {
HStack {
Text("Pause after intro")
Spacer()
Text("10 seconds")
}
Picker(selection: $seconds, label: Text("Seconds")) {
ForEach(0..<60) { n in
Text("\(n) sec").tag(n)
}
}
}
}
}
That is displayed from a UIViewController, like this:
let editView = EditSegmentView()
let vc = UIHostingController(rootView: editView)
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
I can't see anything obviously wrong with the code provided. You could try using a GeometryReader to force the VStack to be the width of the available space. Also add some padding(), because otherwise your Text will start right at the edge
var body: some View {
GeometryReader { proxy in
VStack {
//...
} .padding()
.frame(width: proxy.size.width)
}
}
Are there any clues as to why the view is expanding beyond the edges when you debug its view hierarchy?
This is a little late, but I had the same issue and fixed by making sure the outer VStack had it's alignment set to .leading. The inner Vstack also has alignment: leading and then added a .padding() to the inner VStack

SwiftUI - Difference between Spacer() and Color.clear?

Pretty simple question - I'm just curious if there is any perceivable difference between Spacer() and Color.clear in SwiftUI
If you measure the screen, then yes.
I notice a ~2px difference when using both as in this example:
struct ContentView: View {
var body: some View {
HStack {
Spacer()
Text("First")
Spacer()
Text("Second")
Spacer()
}
}
}
Which generates:
(the middle space takes around 84px)
Now using Color.clear:
struct ContentView: View {
var body: some View {
HStack {
Color.clear
Text("First")
Color.clear
Text("Second")
Color.clear
}
}
}
Output:
Notice that the "First" and "Second" strings don't touch the vertical guide anymore...
(the middle "space" now takes around 86px)
Not a huge difference, but I would stick to Spacer.
(xScope is our friend)

JavaFX 2.0 - How to change legend color of a LineChart dynamically?

I am trying to style my JavaFX linechart but I have some trouble with the legend.
I know how to change the legend color of a line chart in the css file:
.default-color0.chart-series-line { -fx-stroke: #FF0000, white; }
.default-color1.chart-series-line { -fx-stroke: #00FF00, white; }
.default-color2.chart-series-line { -fx-stroke: #0000FF, white; }
.default-color0.chart-line-symbol { -fx-background-color: #FF0000, white; }
.default-color1.chart-line-symbol { -fx-background-color: #00FF00, white; }
.default-color2.chart-line-symbol { -fx-background-color: #0000FF, white; }
But this is not enough for my purposes. I have three or more colored toggle buttons and a series of data for every button. The data should be displayed in the same color the button has after I have selected the button. This should be possible with a multiselection of the buttons, so that more than one series of data can be displayed simultaneously.
For the chart lines I have managed it by changing the style after I clicked the button:
..
dataList.add(series);
..
series.getNode().setStyle("-fx-stroke: rgba(" + rgba + ")");
If I deselect the button I remove the data from the list.
dataList.remove(series);
That is working fine for the strokes, but how can I do the same for the legend?
You can see an example below. First I clicked the red button, thus the stroke and the legend is red (default-color0). After that I clicked the blue button. Here you can see the problem. The stroke is blue but the legend is green, because default color1 is used and I do not know how to change the legend color.
I ran into this issue as well. The issue seems to be that when data series are added to the chart, the legend isn't updated at the same time, so when you lookup components with that seriesN style class they don't exist yet. Came up with a work-around that detects when the legend items are created so that dynamic styling can be added to them.
I added a ListChangeListener to the chart legend's "getChildrenUnmodifiable()" ObservableList, which in turn adds a ListChangeListener to each of the legend's children as they get added. From within this listener, we can tell when new items are being added to the legend (or removed). This allow us to then make the dynamic style changes.
for (Node n : lineChart.getChildrenUnmodifiable())
{
if (n instanceof Legend)
{
final Legend legend = (Legend) n;
// remove the legend
legend.getChildrenUnmodifiable().addListener(new ListChangeListener<Object>()
{
#Override
public void onChanged(Change<?> arg0)
{
for (Node node : legend.getChildrenUnmodifiable())
{
if (node instanceof Label)
{
final Label label = (Label) node;
label.getChildrenUnmodifiable().addListener(new ListChangeListener<Object>()
{
#Override
public void onChanged(Change<?> arg0)
{
//make style changes here
}
});
}
}
}
});
}
}
For future reference, this problem can be solved by wrapping your relevant code in a call to Platform.runLater(). For example:
LineChart<Number, Number> plot;
....
Platform.runLater(() -> {
Node nl = plot.lookup(".default-color0.chart-series-line");
Node ns = plot.lookup(".default-color0.chart-line-symbol");
nl.setStyle("-fx-stroke: #333;");
ns.setStyle("-fx-background-color: #333, white;");
});
It seems that this guy is able to lookup the nodes used for legend using their class, and then calling setStyle() on those nodes. (I don't think his problem is relevant for yours)
This solution is based on #Chris' solution
if (checkCombo.getCheckModel().isChecked(0)) {
lineChart.getData().add(seriesTest1);
changeColorSeries(lineChart.getData().size() - 1, "darkgreen");
}
if (checkCombo.getCheckModel().isChecked(3)) {
lineChart.getData().add(seriesTest2);
changeColorSeries(lineChart.getData().size() - 1, "darkred");
}
private void changeColor(int position, String color) {
Platform.runLater(() -> {
Node nl = lineChart.lookup(".default-color" + position + ".chart-series-line");
Node ns = lineChart.lookup(".default-color" + position + ".chart-line-symbol");
Node nsl = lineChart.lookup(".default-color" + position + ".chart-legend-item-symbol");
nl.setStyle("-fx-stroke: " + color + ";");
ns.setStyle("-fx-background-color: " + color + ", white;");
nsl.setStyle("-fx-background-color: " + color + ", white;");
});
}
After digging a lot through google with no success I finally found a lean way to update the legend colors according line color through this method:
private void setLegendItemColor(String shop, String color, Legend lg) {
for (Node n : lg.getChildren()) {
Label lb = (Label) n;
if (lb.getText().contains(shop)) {
lb.getGraphic().setStyle("-fx-background-color:" + color + ";");
}
}
}
So you just need to call the method setLegendItemColor() when you´re going to update the chart lines to update the legend label symbols to the correspondent color.

Resources