applying fixedSize only if there is enough space - swift

I want to build a VStack that is only as wide as the widest element is. If there is (horizontally) not enough space, e.g., the screen is too narrow, the content should shrink.
Here is a simple example:
VStack {
HStack {
Text("title1")
Spacer()
Text("title2")
Spacer()
Text("title3")
}
HStack {
Text("fo0o0o0o0o0o0o0o0oo0o0o0")
Spacer()
Text("fo0o0o0o0o")
Spacer()
Text("fo0")
}
HStack {
Text("fo0o0o0o0o")
Spacer()
Text("fo0o")
Spacer()
Text("fo0")
}
}.fixedSize()
How it should look like, if the VStack does not have to shrink.
How it should look like, if the VStack has to shrink.
If fixedSize is applied, the content obviously does not shrink and goes over the edges of the screen. If fixedSize is not applied, the spacers use all (horizontally) available space and the table is too wide. Hard coding the width is not an option because the content is dynamic and, therefore, unknown.
Basically: if there is enough space, apply fixedSize. If not, don't apply fixedSize and leave it as it is.
How can I archive that behavior with SwiftUI?

I hope I understood what you mean.
If you are using Xcode 14 and can deploy iOS 16 or later, you can simply replace VStack with Grid and HStack with GridRow; this will place the text in columns. Then, you can remove the Spacer() all over. If the text is too long, it will increase the lines inside its own column.
Like this:
Grid {
GridRow {
Text("title1")
Text("title2")
Text("title3")
}
GridRow {
Text("fo0o0o0o0o0o0o0o0")
Text("fo0o0o0o0o")
Text("fo0")
}
GridRow {
Text("fo0o0o0o0o")
Text("fo0o")
Text("fo0")
}
}
Result in the images (short vs. long text):

you could try this approach, using .fixedSize(horizontal: _, vertical: _) and some space calculation logic, together with a #State var.
struct ContentView: View {
#State var dofix = false // <-- here
var body: some View {
VStack {
// for testing, do your space calculation logic
Button("dofix") {
dofix.toggle() // <-- here
}
HStack {
Text("title1")
Spacer()
Text("title2")
Spacer()
Text("title3")
}
HStack {
Text("fo0o0o0o0o0o0o0o0oo0o0o0")
Spacer()
Text("fo0o0o0o0o")
Spacer()
Text("fo0")
}
HStack {
Text("fo0o0o0o0o")
Spacer()
Text("fo0o")
Spacer()
Text("fo0")
}
}
.fixedSize(horizontal: dofix, vertical: dofix) // <-- here
}
}

Related

How to keep an item exactly in the center of an HStack with other items in the HStack

I have two items in an HStack. I want one of them to be perfectly horizontally centered and the other to be on the left side (have an alignment of .leading). Here is my code so far:
HStack {
Text("1")
.frame(alignment: .leading)
Text("Hello")
.frame(alignment: .center)
}
I have figured out that the .overlay method works. However, I still want Text("Hello") to be aware of the position of Text("1") so that if Text("Hello") were a longer string, it would know not to cover up Text("1"), which is exactly what happens when the .overlay function is used. So I'm wondering if there are any other solutions.
You can add one more Text view inside HStack as hidden which have the same content as your left align text view along with Spacer. Something like this.
HStack {
Text("1")
Spacer()
Text("Good Morning, Hello World. This is long message")
.multilineTextAlignment(.center)
Spacer()
Text("1") // Set the same content as left aligned text view
.hidden()
}
.padding(.horizontal)
Preview
Not sure if it's the best way, but you can use a GeometryReader to get the screen width and skip the first half.
To place the text exactly in the center, you should also calculate the width of the text. you can use a simple extension to get the size of your text:
extension String {
public func size(withFont font: UIFont) -> CGSize {
(self as NSString).size(withAttributes: [.font: font])
}
}
Here is the example code:
GeometryReader { geo in
HStack {
Spacer()
.frame(width: (geo.size.width - "center".size(withFont: UIFont.systemFont(ofSize: 18)).width) / 2)
HStack {
Text("center")
Text("another")
}
}
}

Remove extra spacing on List

I can't figure out how to make the swipeActions have the size of the view that is on dark, I tried placing a spacer below or text, and it just creates an ever bigger mess with some space. What would be the correct solution for this?
In case it's not clear, If you notice the purple square it's bigger than what's on it's left side, I want them both to be the same size.
List(someItems, id: \.self) { item in
VStack {
TheView()
}
.swipeActions(allowsFullSwipe: true) { // Adds custom swipe actions
Button {
} label: {
Label("Hide", systemImage: "eye.slash")
}
.tint(.accentColor)
}
.listRowBackground(Color(.red)) // Sets Row color or a custom color: .listRowBackground(Color("DarkBackground"))
.listRowInsets(EdgeInsets()) // Removes extra padding space in rows
.listRowSeparator(.hidden) // Removed lines between items
.cornerRadius(10)
.shadow(radius: 2)
.padding(.bottom)
}
.listStyle(PlainListStyle()) // Removes extra spaces around the whole List
The extra space at the bottom is because of the .padding(.bottom), if I remove it the size is correct, but the items would be all close together and I need some space.
Edit:
This is the view I'm using above called "TheView()", it's not much, I don't think the problem is there.
var body: some View {
VStack {
HStack {
HStack(alignment: .center) {
SomeImage()
}
HStack(alignment: .top) {
VStack {
HStack {
Text("sometext")
.foregroundColor(.primary)
.bold()
Spacer()
}
HStack {
Text(String(xyz!))
.font(.footnote)
.fontWeight(.light)
Spacer()
}
}
HStack {
Text("$515")
.font(.footnote)
.fontWeight(.light)
}
}
}
.padding(5)
.padding(.horizontal, 3)
}
.background(LinearGradient(gradient: Gradient(colors: [Color("SomeColor")]), startPoint: .topLeading, endPoint: .bottomTrailing))
}

SwiftUI truncate content of ForEach

In the following code snippet, the red circles obviously don't fit onto the screen and therefore only the trailing part of the HStack is shown. Instead of that behavior, I want the leading text of the HStack to always be visible and truncate the trailing red circles that don't fit into the available space (replaced with ...).
How can I do this?
struct ContentView: View {
var body: some View {
HStack {
Text("This text should always be visible")
.layoutPriority(1)
Spacer()
ForEach(0..<20) { index in
Image(systemName: "circle.fill")
.font(.body)
.foregroundColor(.red)
}
}
}
}
So I want this:
instead of this:
Can get everything except the ellipses with:
struct ContentView: View {
var body: some View {
HStack {
Text("This text should always be visible")
.fixedSize()
ForEach(0..<20) { index in
Image(systemName: "circle.fill")
.font(.body)
.foregroundColor(.red)
}
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}
In order to get the ellipses you'll probably have to calculate frame sizes or try to get your images into an attributed string which could get pretty complicated. You might also consider a scroll view or grid.

Center and align SwiftUI elements in a ScrollView

I have the following SwiftUI view:
import SwiftUI
struct MySwiftUIView: View {
var body: some View {
VStack {
HStack {
Text("top leading text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("centered text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("bottom trailing text")
}
}
}
}
It looks like this when run:
If I embed the view in a ScrollView like this:
import SwiftUI
struct MySwiftUIView: View {
var body: some View {
ScrollView {
VStack {
HStack {
Text("top leading text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("centered text")
Spacer()
}
Spacer()
HStack {
Spacer()
Text("bottom trailing text")
}
}
}
}
}
Then it looks like this when run:
How do I make the centered text centered, and the bottom trailing text rest on the bottom, when they are embedded in a ScrollView?
In a way, I want to use SwiftUI to replicate this scrolling behaviour seen in Xcode's inspectors, where the text "Not Applicable" is centered and scrollable:
Wrap the ScrollView inside GeometryReader. Then apply a minimum height to the scroll view equal to the geometry.size.height. Now the spacers you applied should fill the VStack the way you intended them to. Try the code below:
GeometryReader { geometry in
ScrollView(.vertical) {
VStack {
HStack {
Text("top leading text")
Spacer()
}
Spacer()
HStack {
Text("centered text")
}
Spacer()
HStack(alignment: .bottom) {
Spacer()
Text("bottom trailing text")
}
}
.padding()
.frame(width: geometry.size.width)
.frame(minHeight: geometry.size.height)
}
}
Also check this post for more discussion on this issue: SwiftUI - Vertical Centering Content inside Scrollview

Center Item Inside Horizontal Stack

If I have 3 items inside a Horizontal Stack, I thought I could do something like this:
HStack{
Text("test")
Spacer()
item2()
Spacer()
Text("test")
}
to center item2() in between the two Text views. However, the problem with this is that item2() isn't necessarily always centered, because, lets say Text("test") changes to Text("a") or something. This causes problems, and the second item isn't always centered on the screen.
How can I make it so item2() is always centered?
Thanks
I would propose the following start point (simplest case... read below why)
As it's seen it really gives centred w/o frame shift with correctly aligned side elements, but ... there is drawback - it will work in such simplest variant only if it is known in advance that those three text elements should never overlap in user run-time. If it is the case (really there are such) then this approach just goes. However if left/right text might grow in run-time, then more calculations will be needed to limit their width by .frame(maxWidth:) depending on the width of centred element... that variant is more complicated, but it is feasible.
var body: some View {
ZStack {
HStack {
Text("Longer side")
Spacer()
Text("One")
}
item2()
}
}
private func item2() -> some View {
Text("CENTER")
.background(Color.yellow)
.border(Color.red)
}
Update: here is possible approach to limit one of the side to not overlap centred one (contains async updates, so should be tested in Live Preview or Simulator)
So... if left text is dynamic and the requirement to cut trailing symbols, here is how it could go ...
and it automatically fit well on device orientation change
struct TestHorizontalPinCenter: View {
#State var centerFrame: CGRect = .zero
private let kSpacing: CGFloat = 4.0
var body: some View {
ZStack {
HStack {
Text("Longer side very long text to fit")
.lineLimit(1)
.frame(maxWidth: (centerFrame == .zero ? .infinity : centerFrame.minX - kSpacing), alignment: .leading)
Spacer()
Text("One")
}
item2()
.background(rectReader($centerFrame))
}
}
private func item2() -> some View {
Text("CENTER")
.background(Color.yellow)
.border(Color.red)
}
func rectReader(_ binding: Binding<CGRect>) -> some View {
return GeometryReader { (geometry) -> AnyView in
let rect = geometry.frame(in: .global)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return AnyView(Rectangle().fill(Color.clear))
}
}
}
And if it is needed to wrap left side, then .lineLimit(nil) and additional layout will be needed, and solution growth, but the idea is the same. Hope this will be helpful for someone.
I had the same problem and the solution from #Asperi works, but i had problems with multiline texts and some performance issues if i use it in a list.
The following solution solved all the problems.
HStack(alignment: .center) {
Text("test")
.frame(maxWidth: .infinity)
item2()
Text("test")
.frame(maxWidth: .infinity)
}
To center views you can use a ZStack:
ZStack {
item2()
HStack {
Text("test")
Spacer()
Text("test")
}
}
You may need to add some customized Alignment components.
extension HorizontalAlignment {
private enum MyHAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[HorizontalAlignment.center]
}
}
static let myhAlignment = HorizontalAlignment(MyHAlignment.self)
}
HStack {
Spacer()
Text("jjjjjjjjjj")
Spacer()
Image("image").alignmentGuide(.myhAlignment) { (ViewDimensions) -> CGFloat in
return ViewDimensions[HorizontalAlignment.center]
}
Spacer()
Text("test")
}
.frame(alignment: Alignment(horizontal: .myhAlignment, vertical: .center))