SwiftUI GeometryReader compact size - swiftui

I would like my LoadingTitle to have a width of 70% of the screen so I use the GeometryReader but it makes the vertical size expand and my LoadingTitle takes much more vertical space. I would like it to remain as compact as possible.
When using hardcoded width: 300 I get the correct layout (except the relative width):
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
LoadingTitle()
Color.blue
}
}
}
struct LoadingTitle: View {
var body: some View {
HStack() {
Color.gray
}
.frame(width: 300, height: 22)
.padding(.vertical, 20)
.border(Color.gray, width: 1)
}
}
Now if I wrap the body of my LoadingTitle in the GeometryReader I can get the correct relative size but then the GeometryReader expands my view vertically:
struct LoadingTitle: View {
var body: some View {
GeometryReader { geo in
HStack() {
Color.gray
.frame(width: geo.size.width * 0.7, height: 22, alignment: .leading)
Spacer()
}
.padding(.vertical, 20)
.border(Color.gray, width: 1)
}
}
}
I tried using .fixedSize(horizontal: false, vertical: true) on the GeometryReader as other suggested but then the resulting view is too much compact and all its paddings are ignored:
How could I achieve the layout of the 1st screenshot with a relative width?

Here is possible approach. Tested with Xcode 11.4 / iOS 13.4 (w/ ContentView unchanged)
struct LoadingTitle: View {
var body: some View {
VStack { Color.clear }
.frame(height: 22).frame(maxWidth: .infinity)
.padding(.vertical, 20)
.overlay(
GeometryReader { geo in
HStack {
HStack {
Color.gray
.frame(width: geo.size.width * 0.7, height: 22)
}
.padding(.vertical, 20)
.border(Color.gray, width: 1)
Spacer()
}
}
)
}
}

Since you have a fixed height for title,
struct LoadingTitle1: View {
var body: some View {
GeometryReader { geo in
HStack {
Color.gray.frame(width: geo.size.width * 0.7)
.padding(.vertical, 20)
.border(Color.gray, width: 1)
Spacer()
}
}.frame(height: 62)
}
}

Related

Variable Rectangle() dimension based on cell size to draw a timeline

I am trying to build a List that I want to look like a timeline.
Each cell will represent a milestone.
Down the left hand side of the table, I want the cells to be 'connected', by a line (the timeline).
I have tried various things to get it to display as I want but I have settled with basic geometric shapes , i.e Circle() and Rectangle().
This is sample code to highlight the problem:
import SwiftUI
struct ContentView: View {
var body: some View {
let roles: [String] = ["CEO", "CFO", "Managing Director and Chairman of the supervisory board", "Systems Analyst", "Supply Chain Expert"]
NavigationView{
VStack{
List {
ForEach(0..<5) { toto in
NavigationLink(
destination: dummyView()
) {
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .center, spacing: 0){
Rectangle()
.frame(width: 1, height: 30, alignment: .center)
Circle()
.frame(width: 10, height: 10)
Rectangle()
.frame(width: 1, height: 20, alignment: .center)
Circle()
.frame(width: 30, height: 30)
.overlay(
Image(systemName: "gear")
.foregroundColor(.gray)
.font(.system(size: 30, weight: .light , design: .rounded))
.frame(width: 30, height: 30)
)
//THIS IS THE RECTANGLE OBJECT FOR WHICH I WANT THE HEIGHT TO BE VARIABLE
Rectangle()
.frame(width: 1, height: 40, alignment: .center)
.foregroundColor(.green)
}
.frame(width: 32, height: 80, alignment: .center)
.foregroundColor(.green)
VStack(alignment: .leading, spacing: 0, content: {
Text("Dummy operation text that will be in the top of the cell")
.font(.subheadline)
.multilineTextAlignment(.leading)
.lineLimit(1)
Label {
Text("March 6, 2021")
.font(.caption2)
} icon: {
Image(systemName: "calendar.badge.clock")
}
HStack{
HStack{
Image(systemName: "flag.fill")
Text("In Progress")
.font(.system(size: 12))
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.foregroundColor(.blue)
.background(Color.white)
.cornerRadius(5, antialiased: true)
HStack{
Image(systemName: "person.fill")
Text(roles[toto])
.font(.system(size: 12))
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.foregroundColor(.green)
.background(Color.white)
.cornerRadius(5, antialiased: true)
HStack{
Image(systemName: "deskclock")
Text("in 2 Months")
.font(.system(size: 12))
}
.padding(.horizontal, 4)
.padding(.vertical, 3)
.foregroundColor(.red
)
.background(
Color.white
)
.cornerRadius(5, antialiased: true)
}
})
}.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct dummyView: View {
var body: some View {
Text("Hello, World!")
}
}
struct dummyView_Previews: PreviewProvider {
static var previews: some View {
dummyView()
}
}
but as you can see in the enclosed picture, there are unwanted gaps
So other content in the cell is making the height of the entire cell 'unpredictable' and break the line.
Is there a way to determine the height of the cell and extend the dimensions of the Rectangle, so that it extends to the full height of the cell?
Is there a better approach you recommend for trying to build such a timeline ?
PS: I have tried playing around with .frame and .infinity but that does work.
Many thanks.
Why not just draw the line based on the size of the row. See Creating custom paths with SwiftUI. Remember, everything is a view.
First, you need to decompose what you are doing into subviews. You have too many moving parts in one view to get it correct. Also, I would avoid setting specific padding amounts as that will mess you up when you change devices. You want a simple, smart view that is generic enough to handle different devices.
I would have a row view that has a geometry reader so it knows its own height. You could then draw the line so that it spanned the full height of the row, regardless of the height. Something along the lines of this:
struct ListRow: View {
var body: some View {
GeometryReader { geometry in
ZStack {
HStack {
Spacer()
Text("Hello, World!")
Spacer()
}
VerticalLine(geometry: geometry)
}
}
}
}
and
struct VerticalLine: View {
let geometry: GeometryProxy
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: -30))
path.addLine(to: CGPoint(x: 20, y: geometry.size.height+30))
}
.stroke(Color.green, lineWidth: 4)
}
}

SwiftUI How to vertically center a view inside a VStack

I would like to have one view in the vertical center of the screen, one view at the top of the screen and one view vertically centered between these two views like so:
This took me 5min to do on a storyboard but I don't seem to find a way to do it in SwiftUI 🙃.
I already tried with multiple zstacks, vstacks, multiple custom alignments but this is the closest I got:
struct SelectionView: View {
var body: some View {
ZStack(alignment: .myAlignment) {
Color.green
.edgesIgnoringSafeArea(.all)
VStack {
Image(systemName: "clock")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 156, height: 80)
// Spacer()
Text("My\nmultiline label")
.multilineTextAlignment(.center)
.font(.title)
.foregroundColor(.white)
// Spacer()
VStack(spacing: 16) {
RoundedRectangle(cornerRadius: 5).fill(Color.white).frame(height: 79)
RoundedRectangle(cornerRadius: 5).fill(Color.white).frame(height: 79)
}
.alignmentGuide(VerticalAlignment.myAlignment) { dimension in
dimension[VerticalAlignment.center]
}
.layoutPriority(1)
}
.padding([.leading, .trailing], 24)
}
}
}
struct SelectionView_Previews: PreviewProvider {
static var previews: some View {
LanguageSelectionView()
}
}
// MARK
extension HorizontalAlignment {
enum MyHorizontal: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat
{ d[HorizontalAlignment.center] }
}
static let myAlignment =
HorizontalAlignment(MyHorizontal.self)
}
extension VerticalAlignment {
enum MyVertical: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat
{ d[VerticalAlignment.center] }
}
static let myAlignment = VerticalAlignment(MyVertical.self)
}
extension Alignment {
static let myAlignment = Alignment(horizontal: .myAlignment,
vertical: .myAlignment)
}
I'm keeping the GeometryReader as a last resort as it feels like a too drastic measure for this seemingly simple layout..
I guess I'm approaching this in some wrong way (still too much UIKit/Constraints in my head)..
Here is possible solution. Tested with Xcode 12 / iOS 14
struct SelectionView: View {
var body: some View {
ZStack {
Color.green
.edgesIgnoringSafeArea(.all)
VStack(spacing: 16) {
Color.clear
.overlay(
VStack {
Image(systemName: "clock")
.resizable()
.foregroundColor(.white)
.aspectRatio(contentMode: .fit)
.frame(width: 156, height: 80)
Color.clear
.overlay(
Text("My\nmultiline label")
.multilineTextAlignment(.center)
.font(.title)
.foregroundColor(.white)
)
}
)
RoundedRectangle(cornerRadius: 5).fill(Color.white).frame(height: 79)
RoundedRectangle(cornerRadius: 5).fill(Color.white).frame(height: 79)
Color.clear
}
.padding([.leading, .trailing], 24)
}
}
}

How to align multiple objects horizontally among different Stacks

I am illustrating the problem I have with a simple example below. It all boils down to properly aligning VStacks with text to a Circle. This is the image of what I am trying to get at. Is there any way to align things properly without using hardcoded paddings?
This is the code producing the left image
struct MyAlignedView: View {
var body: some View {
HStack {
VStack(alignment: .center, spacing: 10) {
Circle()
.frame(width: 20, height: 20)
Text("|")
Circle()
.frame(width: 20, height: 20)
Text("|")
Circle()
.frame(width: 20, height: 20)
}
VStack {
VStack{
Text("stack 1")
}
VStack{
Text("stack 2")
Text("text2")
Text("more text")
}
VStack{
Text("stack 3")
Text("text3")
}
}
}
}
}
Here's one way to do it. I put the Circle and the corresponding text into an HStack to keep them aligned. I let every other Circle manage the lines. That keeps them vertically aligned with the Circles.
If you were to continue this, the next Circle would have two lines, or a line and a space if it is the last one.
struct ContentView: View {
var body: some View {
HStack {
VStack(alignment: .center, spacing: 0) {
HStack {
Circle()
.frame(width: 20, height: 20)
Text("stack 1")
.frame(width: 80, height: 40)
}
HStack {
VStack {
Text("|")
Circle()
.frame(width: 20, height: 20)
Text("|")
}
VStack {
Text("stack 2")
Text("text2")
Text("more text")
}
.frame(width: 80, height: 40)
}
HStack {
Circle()
.frame(width: 20, height: 20)
VStack {
Text("stack 3")
Text("text3")
}
.frame(width: 80, height: 40)
}
}
}
}
}
There is a lot of redundancy that needs to be managed. This can be put into a loop that can automatically figure out which lines to add and/or hide:
struct TextLines {
let lines: [String]
}
struct BulletPoints: View {
let textLines: [TextLines]
var body: some View {
HStack {
VStack(alignment: .center, spacing: 0) {
ForEach(0 ..< textLines.count) { idx in
HStack {
VStack {
if !idx.isMultiple(of: 2) {
Text("|")
}
Circle()
.frame(width: 20, height: 20)
if !idx.isMultiple(of: 2) {
Text("|").opacity(idx == self.textLines.count - 1 ? 0 : 1)
}
}
VStack {
ForEach(self.textLines[idx].lines, id: \.self) { line in
Text(line)
}
}
.frame(width: 80, height: 40)
}
}
}
}
}
}
struct ContentView: View {
var body: some View {
BulletPoints(textLines: [
.init(lines: ["stack1"]),
.init(lines: ["stack 2", "text2", "more text"]),
.init(lines: ["stack 3", "text3"]),
.init(lines: ["stack 4"])
])
}
}

How to ensure view appears above other views when iterating with ForEach in SwiftUI?

I have a SwiftUI view that is a circular view which when tapped opens up and is supposed to extend over the UI to its right. How can I make sure that it will appear atop the other ui? The other UI elements were created using a ForEach loop. I tried zindex but it doesn't do the trick. What am I missing?
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(Array(zip(1..., dataPoints)), id: \.1.id) { number, point in
VStack(alignment: .center, spacing: 5) {
DataCircle().zIndex(10)
ChartBar(percentage: point.percentage).zIndex(-1)
Text(point.month)
.font(.caption)
}
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
.offset(x: 30, y: 20)
}
.frame(width: 500, height: 300, alignment: .center)
}
}
}
}
.zIndex have effect for views within one container. So to solve your case, as I assume expanded DataCircle on click, you need to increase zIndex of entire bar VStack per that click by introducing some kind of handling selection.
Here is simplified replicated demo to show the effect
struct TestBarZIndex: View {
#State private var selection: Int? = nil
var body: some View {
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
Rectangle().fill(Color.yellow)//ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(1...10) { number in
VStack(spacing: 5) {
Spacer()
ZStack() { // DataCircle()
Circle().fill(Color.pink).frame(width: 20, height: 20)
.onTapGesture { self.selection = number }
if number == self.selection {
Text("Top Description").fixedSize()
}
}
Rectangle().fill(Color.green) // ChartBar()
.frame(width: 20, height: CGFloat(Int.random(in: 40...150)))
Text("Jun")
.font(.caption)
}.zIndex(number == self.selection ? 1 : 0) // << here !!
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
}
.frame(height: 300)
}
}
}
}

swiftUI Button with width:0 nonetheless active

I set the width of a SwiftUI Button to 0 to "deactivate" it.
If the with of the button is set to 0, the button disappears as expected, but clicking in the left edge of the yellow Stack activates the Button.
Why does this happen?
How can I avoid it?
struct ContentView: View {
#State var zeroWidth = false
var body: some View {
VStack{
ButtonLine( leftButtons: [ButtonAttr( label: "LB1",
action: {print("LB1")},
iconSystemName : "person"
)],
zeroWidth: zeroWidth
)
Button("Toggle width \(zeroWidth ? "On" : "Off" ) "){ self.zeroWidth.toggle() }
}
}
}
struct ButtonLine: View {
let leftButtons : [ButtonAttr]
let zeroWidth : Bool
var body: some View {
HStack(spacing: 0) {
ForEach(leftButtons.indices, id: \.self)
{ i in
HStack(spacing: 0.0)
{
Button(action: { self.leftButtons[i].action() }) {
ButtonLabel( singleline: false,
buttonAttr: self.leftButtons[i]
)
.padding(0)
//.background(Color.green) // not visible
}
.buttonStyle(BorderlessButtonStyle())
.frame( width: self.zeroWidth ? 0 : 100, height: 50)
.background(Color.green)
.clipped()
.foregroundColor(Color.white)
.padding(0)
}
// .background(Color.blue) // not visible
}
// .background(Color.blue) // not visible
Spacer()
Text("CONTENT")
.background(Color.green)
.onTapGesture {
print("Content tapped")
}
Spacer()
}
.background(Color.yellow)
.onTapGesture {
print("HS tapped")
}
}
}
struct ButtonLabel: View {
var singleline : Bool
var buttonAttr : ButtonAttr
var body: some View {
VStack (spacing: 0.0) {
Image(systemName: buttonAttr.iconSystemName).frame(height: singleline ? 0 : 20).clipped()
.padding(0)
.background(Color.blue)
Text(buttonAttr.label)
.padding(0)
.background(Color.blue)
}
.padding(0)
.background(Color.red)
}
}
struct ButtonAttr
{ let label : String
let action: ()-> Void
let iconSystemName : String
}
Instead of tricky "deactivate", just use real remove, like below
HStack(spacing: 0.0)
{
if !self.zeroWidth {
Button(action: { self.leftButtons[i].action() }) {
ButtonLabel( singleline: false,
buttonAttr: self.leftButtons[i]
)
.padding(0)
//.background(Color.green) // not visible
}
.buttonStyle(BorderlessButtonStyle())
.frame(width: 100, height: 50)
.background(Color.green)
.clipped()
.foregroundColor(Color.white)
.padding(0)
}
}.frame(height: 50) // to keep height persistent
there is very simple explanation.
try next snippet
struct ContentView: View {
var body: some View {
Text("Hello").padding().border(Color.yellow).fixedSize().frame(width: 0)
}
}
Why?
.frame(..)
is defined as a function of View, which return another View, as any kind of View modifier. The resulting View has .zero sized frame, as expected.
It is really true? Let's check it!
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle()
.fill(Color.orange)
.frame(width: 100, height: 100)
Text("Hello")
.padding()
.border(Color.black)
.fixedSize()
.frame(width: 0, height: 0)
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.blendMode(.exclusion)
}
}
}
Just add .clipped modifier to your Text View
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle()
.fill(Color.orange)
.frame(width: 100, height: 100)
Text("Hello")
.padding()
.border(Color.black)
.fixedSize()
.frame(width: 0, height: 0)
.clipped()
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.blendMode(.exclusion)
}
}
}
and the Text "disappears" ...
It disappears from the screen, but not from View hierarchy!. Change the code again
struct ContentView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle()
.fill(Color.orange)
.frame(width: 100, height: 100)
Text("Hello")
.padding()
.border(Color.black)
.fixedSize().onTapGesture {
print("tap")
}
.frame(width: 0, height: 0)
.clipped()
Rectangle()
.fill(Color.green)
.frame(width: 100, height: 100)
.blendMode(.exclusion)
}
}
}
and you see, that there is still some "invisible" area sensitive on tap gesture
You can disable you Button by adding a .disabled(self.zeroWidth)
Button(action: { self.leftButtons[i].action() }) {
ButtonLabel( singleline: false,
buttonAttr: self.leftButtons[i]
)
.padding(0)
//.background(Color.green) // not visible
}
.disabled(self.zeroWidth)
.buttonStyle(BorderlessButtonStyle())
.frame( width: self.zeroWidth ? 0 : 100, height: 50)
.background(Color.green)
.clipped()
.foregroundColor(Color.white)
.padding(0)
You can debug the view hierarchy by clicking that icon in xcode: