SwiftUI Text in between Images is not aligned properly [duplicate] - swiftui

I have the following code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("ContentView Header 1")
.border(Color.green, width: 2)
Text("ContentView Header 2")
.border(Color.green, width: 2)
Image(systemName: "arrow.up")
.border(Color.green, width: 2)
Text("ContentView Footer 1")
.border(Color.green, width: 2)
}
}
}
The result looks as follows:
Why is there spacing around the image, but not around the texts? It disappears when I explicitly set spacing for the VStack.

public struct VStack<Content> : View where Content : View {
/// Creates an instance with the given `spacing` and Y axis `alignment`.
///
/// - Parameters:
/// - alignment: the guide that will have the same horizontal screen
/// coordinate for all children.
/// - spacing: the distance between adjacent children, or nil if the
/// stack should choose a default distance for each pair of children.
Last line - "stack should choose a default distance for each pair of children"

Related

SwiftUI Scrollable Charts in IOS16

Using the new SwiftUI Charts framework, we can make a chart bigger than the visible screen and place it into a ScrollView to make it scrollable. Something like this:
var body : some View {
GeometryReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
Chart {
ForEach(data) { entry in
// ...
}
}
.frame(width: proxy.size.width * 2)
}
}
}
Does anybody know if it is possible to programmatically move the scroll to display a certain area of the chart?
I've tried using ScrollViewReader, setting the IDs at the x-axis labels, and trying to use the scrollTo function to navigate to any of those positions with no luck:
Chart {
/// ...
}
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { value in
if let date : Date = value.as(Date.self) {
Text(date, style: .date)
.font(.footnote)
}
}
}
This cheesy workaround seems to do the trick. I put the chart in a ZStack with an HStack overlaying the chart. The HStack contains a bunch of invisible objects that conform to the Identifiable protocol. The quantity, ids, and positions of the invisible objects match the charted data.
Since the ZStack view now contains identifiable elements, ScrollViewReader works as expected.
import SwiftUI
import Charts
struct ChartData: Identifiable {
var day: Int
var value: Int
var id: String { "\(day)" }
}
struct ContentView: View {
#State var chartData = [ChartData]()
#State var scrollSpot = ""
let items = 200
let itemWidth: CGFloat = 30
var body: some View {
VStack {
ScrollViewReader { scrollPosition in
ScrollView(.horizontal) {
// Create a ZStack with an HStack overlaying the chart.
// The HStack consists of invisible items that conform to the
// identifible protocol to provide positions for programmatic
// scrolling to the named location.
ZStack {
// Create an invisible rectangle for each x axis data point
// in the chart.
HStack(spacing: 0) {
ForEach(chartData) { item in
Rectangle()
.fill(.clear)
// Setting maxWidth to .infinity here, combined
// with spacing:0 above, makes the rectangles
// expand to fill the frame specified for the
// chart below.
.frame(maxWidth: .infinity, maxHeight: 0)
// Here, set the rectangle's id to match the
// charted data.
.id(item.id)
}
}
Chart(chartData) {
BarMark(x: .value("Day", $0.day),
y: .value("Amount", $0.value),
width: 20)
}
.frame(width: CGFloat(items) * itemWidth, height: 300)
}
}
.padding()
.onChange(of: scrollSpot, perform: {x in
if (!x.isEmpty) {
scrollPosition.scrollTo(x)
scrollSpot = ""
}
})
}
.onAppear(perform: populateChart)
Button("Scroll") {
if let x = chartData.last?.id {
print("Scrolling to item \(x)")
scrollSpot = x
}
}
Spacer()
}
}
func populateChart() {
if !chartData.isEmpty { return }
for i in 0..<items {
chartData.append(ChartData(day: i, value: (i % 10) + 2))
}
}
}
IMHO this should work out of the SwiftUI box. Apple's comments for the initializer say it creates a chart composed of a series of identifiable marks. So... if the marks are identifiable, it is not a stretch to expect ScrollViewReader to work with the chart's marks.
But noooooo!
One would hope this is an oversight on Apple's part since the framework is new, and they will expose ids for chart marks in an upcoming release.

Dynamically set frame dimensions in a view extension

I've been playing around with giving views a gradient shadow (taken from here and here) and while these achieve most of what I need, they seem to have a flaw: the extension requires you to set a .frame height, otherwise the gradient looks really desaturated (as it's taking up the entire height of the device screen). It's a little hard to describe, so here's the code:
struct RainbowShadowCard: View {
#State private var cardGeometryHeight: CGFloat = 0.0
#State private var cardGeometryWidth: CGFloat = 0.0
var body: some View {
VStack {
Text("This is a card, it's pretty nice. It has a couple of lines of text inside it. Here are some more lines to see how it scales.")
.font(.system(.body, design: .rounded).weight(.medium))
}
.frame(maxWidth: .infinity)
.padding()
.foregroundColor(.white)
.background {
GeometryReader { geo in
Color.black
.onAppear {
cardGeometryHeight = geo.size.height
cardGeometryWidth = geo.size.width
print("H: \(cardGeometryHeight), W: \(cardGeometryWidth)")
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
.padding()
.multicolorGlow(cardHeight: cardGeometryHeight, cardWidth: cardGeometryWidth)
}
}
extension View {
func multicolorGlow(cardHeight: CGFloat, cardWidth: CGFloat) -> some View {
ZStack {
ForEach(0..<2) { i in
Rectangle()
.fill(
LinearGradient(colors: [
.red,
.green
], startPoint: .topLeading, endPoint: .bottomTrailing)
)
// The height of the frame dictates the saturation of
// the linear gradient. Without it, the gradient takes
// up the full width and height of the screen, resulting in
// a washed out / desaturated gradient around the card.
.frame(height: 300)
// My attempt at making the height and width of this view
// be based off the parent view
/*
.frame(width: cardWidth, height: cardHeight)
*/
.mask(self.blur(radius: 10))
.overlay(self.blur(radius: 0))
}
}
}
}
struct RainbowShadowCard_Previews: PreviewProvider {
static var previews: some View {
RainbowShadowCard()
}
}
I've managed to successfully store the VStack height and width in cardGeometryHeight and cardGeometryWidth states respectfully, but I can't figure out how to correctly pass that into the extension.
In the extension, if I uncomment:
.frame(width: cardWidth, height: cardHeight)
The VStack goes to a square of 32x32.
Edit
For the sake of clarity, the above solution "works" if you don't use a frame height value for the extension, but it doesn't work very nicely. Compare the saturation of the shadow in this image to the original, and you'll see a big difference between a non framed approach and a framed approach. The reason for this muddier gradient is the extension is using the screen bounds for the linear gradient, so our shadow gradient isn't getting the benefit of the "start" and "end" saturation of the red and green, but the middle blending of the two.

SwiftUI - view expand from the bottom of frame

I'd like to display a number of values as a continuous value from 0 to 1. I'd like them to grow from the bottom up, from 0 displaying no value, to 1 displaying a full height.
However, I'm unable to make it "grow from the bottom". I'm not sure what a better term for this is - it's a pretty simple vertical gauge, like a gas gauge in a car. I'm able to make it grow from the middle, but can't seem to find a way to make it grow from the bottom. I've played with mask and clipShape and overlay - but it must be possible to do this with just a simple View, and calculations on its height. I'd specifically like to able to show overlapping gauges, as the view below demonstrates.
My ContentView.swift is as follows:
import SwiftUI
struct ContentView: View {
// binding values with some defaults to show blue over red
#State var redPct: CGFloat = 0.75
#State var bluePct: CGFloat = 0.25
let DISP_PCT = 0.8 // quick hack - the top "gauge" takes this much so the sliders display below
var body: some View {
GeometryReader { geom in
VStack {
ZStack {
// neutral background
Rectangle()
.fill(Color.gray)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT)
// the first gauge value display
Rectangle()
.fill(Color.red)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * redPct)
// the second gauge value, on top of the first
Rectangle()
.fill(Color.blue)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * bluePct)
}
HStack {
Slider(value: self.$redPct, in: 0...1)
Text("Red: \(self.redPct, specifier: "%.2f")")
}
HStack {
Slider(value: self.$bluePct, in: 0...1)
Text("Red: \(self.bluePct, specifier: "%.2f")")
}
}
}
}
}
As you play with the sliders, the red/blue views grows "out" from the middle. I would like them to grow "up" from the bottom of its containing view.
I feel like this is poorly worded - if any clarification is needed, please don't hesitate to ask!
Any help would be greatly appreciated!
You can't have them all in the same stacks. The easiest way to do this is to have your gray rectangle be your case view, and then overlay the others on top in VStacks with Spacers like this:
// neutral background
Rectangle()
.fill(Color.gray)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT)
.overlay (
ZStack {
VStack {
Spacer()
// the first gauge value display
Rectangle()
.fill(Color.red)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * redPct)
}
VStack {
Spacer()
// the second gauge value, on top of the first
Rectangle()
.fill(Color.blue)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * bluePct)
}
}
)
The overlay contains them, and the spacers push your rectangles down to the bottom of the stacks.

SwiftUI: How to force a specific view in an HStack to be in the centre

Given an HStack like the following:
HStack{
Text("View1")
Text("Centre")
Text("View2")
Text("View3")
}
How can I force the 'Centre' view to be in the centre?
Here is possible simple approach. Tested with Xcode 11.4 / iOS 13.4
struct DemoHStackOneInCenter: View {
var body: some View {
HStack{
Spacer().overlay(Text("View1"))
Text("Centre")
Spacer().overlay(
HStack {
Text("View2")
Text("View3")
}
)
}
}
}
The solution with additional alignments for left/right side views was provided in Position view relative to a another centered view
the answer takes a handful of steps
wrap the HStack in a VStack. The VStack gets to control the
horizontal alignment of it's children
Apply a custom alignment guide to the VStack
Create a subview of the VStack which takes the full width. Pin the custom alignment guide to the centre of this view. (This pins the alignment guide to the centre of the VStack)
align the centre of the 'Centre' view to the alignment guide
For the view which has to fill the VStack, I use a Geometry Reader. This automatically expands to take the size of the parent without otherwise disturbing the layout.
import SwiftUI
//Custom Alignment Guide
extension HorizontalAlignment {
enum SubCenter: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let subCentre = HorizontalAlignment(SubCenter.self)
}
struct CentreSubviewOfHStack: View {
var body: some View {
//VStack Alignment set to the custom alignment
VStack(alignment: .subCentre) {
HStack{
Text("View1")
//Centre view aligned
Text("Centre")
.alignmentGuide(.subCentre) { d in d.width/2 }
Text("View2")
Text("View3")
}
//Geometry reader automatically fills the parent
//this is aligned with the custom guide
GeometryReader { geometry in
EmptyView()
}
.alignmentGuide(.subCentre) { d in d.width/2 }
}
}
}
struct CentreSubviewOfHStack_Previews: PreviewProvider {
static var previews: some View {
CentreSubviewOfHStack()
.previewLayout(CGSize.init(x: 250, y: 100))
}
}
Edit: Note - this answer assumes that you can set a fixed height and width of the containing VStack. That stops the GeometryReader from 'pushing' too far out
In a different situation, I replaced the GeometryReader with a rectangle:
//rectangle fills the width, then provides a centre for things to align to
Rectangle()
.frame(height:0)
.frame(idealWidth:.infinity)
.alignmentGuide(.colonCentre) { d in d.width/2 }
Note - this will still expand to maximum width unless constrained!
Asperis answer is already pretty interesting and inspired me for following approach:
Instead of using Spacers with overlays, you could use containers left and right next to the to-be-centered element with their width set to .infinity to stretch them out just like Spacers would.
HStack {
// Fills the left side
VStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 120, height: 200)
}.frame(maxWidth: .infinity)
// Centered
Rectangle()
.foregroundColor(Color.red)
.frame(width: 50, height: 150)
// Fills the right side
VStack {
HStack {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 25, height: 100)
Rectangle()
.foregroundColor(Color.red)
.frame(width: 25, height: 100)
}
}.frame(maxWidth: .infinity)
}.border(Color.green, width: 3)
I've put it in a ZStack to overlay a centered Text for demonstration:
Using containers has the advantage, that the height would also translates to the parent to size it up if the left/right section is higher than the centered one (demonstrated in screenshot).

VStack spacing around image, but not around text

I have the following code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("ContentView Header 1")
.border(Color.green, width: 2)
Text("ContentView Header 2")
.border(Color.green, width: 2)
Image(systemName: "arrow.up")
.border(Color.green, width: 2)
Text("ContentView Footer 1")
.border(Color.green, width: 2)
}
}
}
The result looks as follows:
Why is there spacing around the image, but not around the texts? It disappears when I explicitly set spacing for the VStack.
public struct VStack<Content> : View where Content : View {
/// Creates an instance with the given `spacing` and Y axis `alignment`.
///
/// - Parameters:
/// - alignment: the guide that will have the same horizontal screen
/// coordinate for all children.
/// - spacing: the distance between adjacent children, or nil if the
/// stack should choose a default distance for each pair of children.
Last line - "stack should choose a default distance for each pair of children"