SwiftUI HStack fill whole width with equal spacing - ios

I have an HStack:
struct BottomList: View {
var body: some View {
HStack() {
ForEach(navData) { item in
NavItem(image: item.icon, title: item.title)
}
}
}
}
How do I perfectly center its content with equal spacing automatically filling the whole width?
FYI just like Bootstraps CSS class .justify-content-around

The frame layout modifier, with .infinity for the maxWidth parameter can be used to achieve this, without the need for an additional Shape View.
struct ContentView: View {
var data = ["View", "V", "View Long"]
var body: some View {
VStack {
// This will be as small as possible to fit the data
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.border(Color.red)
}
}
// The frame modifier allows the view to expand horizontally
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity)
.border(Color.red)
}
}
}
}
}

The various *Stack types will try to shrink to the smallest size possible to contain their child views. If the child view has an ideal size, then the *Stack will not expand to fill the screen. This can be overcome by placing each child on top of a clear Rectangle in a ZStack, because a Shape will expand as much as possible. A convenient way to do this is via an extension on View:
extension View {
func inExpandingRectangle() -> some View {
ZStack {
Rectangle()
.fill(Color.clear)
self
}
}
}
You can then call it like this:
struct ContentView: View {
var data = ["View", "View", "View"]
var body: some View {
VStack {
// This will be as small as possible to fit the items
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.border(Color.red)
}
}
// Each item's invisible Rectangle forces it to expand
// The .fixedSize modifier prevents expansion in the vertical direction
HStack {
ForEach(data, id: \.self) { item in
Text(item)
.inExpandingRectangle()
.fixedSize(horizontal: false, vertical: true)
.border(Color.red)
}
}
}
}
}
You can adjust the spacing on the HStack as desired.

I inserted Spacer() after each item...but for the LAST item, do NOT add a Spacer():
struct BottomList: View {
var body: some View {
HStack() {
ForEach(data) { item in
Item(title: item.title)
if item != data.last { // match everything but the last
Spacer()
}
}
}
}
}
Example list that is evenly spaced out even when item widths are different:
(Note: The accepted answers .frame(maxWidth: .infinity) did not work for all cases: it did not work for me when it came to items that have different widths)

If items are fullwidth compatible, it will be done automatically, you can wrap items between spacers to make it happen:
struct Resizable: View {
let text: String
var body: some View {
HStack {
Spacer()
Text(text)
Spacer()
}
}
}
So you. can use it in a loop like:
HStack {
ForEach(data, id: \.self) { item in
Resizable(text: item)
}
}

You can also use spacing in stacks ... ie
HStack(spacing: 30){
Image("NetflixLogo")
.resizable()
.scaledToFit()
.frame(width: 40)
Text("TV Show")
Text("Movies")
Text("My List")
}
.frame(maxWidth: .infinity)
output result looks like this ...

If your array has repeating values, use array.indices to omit a spacer after the last element.
HStack() {
ForEach(data.indices) { i in
Text("\(data[i])")
if i != data.last {
Spacer()
}
}
}

Related

Image positionning issue in SwiftUI

I am having an alignment problem while using SwiftUI.
Maybe I should say a layout issue. Anyway here is the situation:
This is the relevant part of the app interface:
One can see that while the text ("+++++") is centered, the flag is not. It is slightly shifted to the left. This left-shifting is precisely my problem. I would like the image to be centered as the text is.
Here follows the code, I would like to know what I am doing wrong for the image not to be centered:
import SwiftUI
struct TheNiceView: View {
........
var body: some View {
VStack {
HStack {
Spacer()
TheButtonView()
Spacer()
}
HStack {
Spacer()
Button(action: {})
{
Text("+++++")
.font(.largeTitle)
.foregroundColor(.gray)
.fontWeight(.heavy)
}
Spacer()
}
}
}
}
struct TheButtonView: View {
........
let imgSide:CGFloat = 72.0
var body: some View {
HStack {
Button(action: {})
{
Image(uiImage: ThaiFlagImg)
.resizable()
.frame(width: imgSide, height: imgSide)
}
}
}
}
Just in case this may be useful, this is the image used for the flag:
1
Thailand flag has five horizontal stripes in the colours red, white, blue, white and red. The image you use has 7 srtips.
2
Using the Spacers and HStack are unnecessary.
I used the following image without the Spacers and HStack, both the +++ button and the flag are aligned in the center.
https://upload.wikimedia.org/wikipedia/commons/a/a9/Flag_of_Thailand.svg
struct TheNiceView: View {
var body: some View {
VStack {
TheButtonView()
Button(action: {}) {
Text("+++++")
.font(.largeTitle)
.foregroundColor(.gray)
.fontWeight(.heavy)
}
}
}
}
struct TheButtonView: View {
let imgSide:CGFloat = 72.0
var body: some View {
Button(action: {}){
Image( "ThaiFlagImg")
.resizable()
.frame(width: imgSide, height: imgSide)
}
}
}

How to align ScrollView content to top when both axes are used?

Here's a simple example:
struct Example: View {
var body: some View {
ScrollView([.horizontal, .vertical], showsIndicators: false, content: {
LazyVStack(content: {
ForEach(1...10, id: \.self) { count in
Text("Item \(count)")
}
})
})
}
}
The problem is that when both axes are used ([.horizontal, .vertical]), ScrollView automatically centers any content inside vertically and horizontally. I have a big data table in the ScrollView, and I need it to be aligned to top instead but I couldn't figure out how to do this. Usual stuff with Vstacks and Spacers doesn't work here at all.
I made an example, with a Slider so you can interactively test it.
This works by making sure the content within the ScrollView is at least as high as the ScrollView itself. This leads to the content filling the whole vertical space, so it will start at the top.
Code:
struct Example: View {
#State private var count: Double = 10
var body: some View {
VStack {
GeometryReader { geo in
ScrollView([.horizontal, .vertical], showsIndicators: false, content: {
VStack(spacing: 0) {
LazyVStack {
ForEach(1 ... Int(count), id: \.self) { count in
Text("Item \(count)")
}
}
Spacer(minLength: 0)
}
.frame(width: geo.size.width)
.frame(minHeight: geo.size.height)
})
}
Slider(value: $count, in: 10 ... 100)
}
}
}
In some cases you may need to use .frame(minWidth: geo.size.width) rather than just width. This can be in the same line as the minHeight.

How to stop SwiftUI change the fixed padding when we switch button content between Text and Image?

So I have two buttons on the bottom of my screen, button A and button B, somewhere along the line I need to replace the image in the button with text, so we do so by changing the Bool in the top.
Although we apply the same modifiers, the padding of button B changes, and the UI moves around, it seems as if the text claims more space. Desired situation: button A and B, should not move around when changing the button Image to Text.
import SwiftUI
private var showImage: Bool = true
struct SwiftUIView: View {
var body: some View {
VStack {
Spacer()
Button(action: {
print("CLICK")
}) {
Image(systemName: "a.circle")
.modifier(TestButtonModifier())
}
.padding(10)
Button(action: {
print("CLICK")
}) {
if showImage {
Image(systemName: "b.circle")
.modifier(TestButtonModifier())
} else {
Text("B")
.modifier(TestButtonModifier())
}
}
.padding(10)
} //: VSTACK
}
}
struct TestButtonModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 52, weight: .regular))
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 100)
.background(Color.black)
.padding(2)
.foregroundColor(Color.white)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
It is due to VStack spacing, which by default differs between different pairs of subviews, so specify some explicit (or remove at all, ie set to zero)
struct SwiftUIView: View {
var body: some View {
VStack(spacing: 0) { // << here !!
// .. other code

Adding shapes in List row of SwiftUI

I am trying to create a List View where rows looks like this:
However, I am unable to align the Circle on the leading side. Tried using Spacer(), HStack within VStack, it just doesn't work. Here's my code and its output.
struct PeopleView: View {
let people = ["Adam", "James"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
HStack {
Circle()
VStack {
Text("\(person)")
}
}
}
}
.navigationBarTitle("People", displayMode: .inline)
}
}
}
Actually you don't need shape itself in this case, but only as a mask to visually present text in circle.
So the solution can be like following
HStack {
Text(person.prefix(2).uppercased()).bold()
.foregroundColor(.white)
.padding()
.background(Color.red)
.mask(Circle()) // << shaping text !!
Spacer()
VStack {
Text("\(person)")
}
}
Some views in SwiftUI fill all available space. Such views are shapes, colors, spacers, dividers, and GeometryReader.
Your Circle is a shape and it behaves similarly like a Spacer (in terms of filling space).
If you replace Circle with an image of a circle it will work:
ForEach(people, id: \.self) { person in
HStack {
Image(systemName: "circle.fill")
.imageScale(.large)
Spacer()
VStack {
Text("\(person)")
}
}
}
That is happening because you did not give a fixed (or relative) frame to the Circle Shape, so the Circle is taking up the maximum available width.
If you add a frame(width:height:), everything should work correctly:
struct PeopleView: View {
let people = ["Adam", "James"]
var body: some View {
NavigationView {
List {
ForEach(people, id: \.self) { person in
HStack {
Circle()
.frame(width: 50, height: 50)
VStack {
Text("\(person)")
}
}
}
}
.navigationBarTitle("People", displayMode: .inline)
}
}
}

Is there a way to define the width of my SwiftUI Image to be relative to screen size using Geometry Reader?

I am working through a sample app to familiarize myself with Swift and SwiftUI and am wanting to combine the ideas of composite layouts using custom views and GeometryReader to determine the screen-size of the device.
The page I'm trying to build is a scrollable list of friends where each row item is a button that can be selected to expand the list to see more details of each friend. I've built the button and all containing views as a FriendView to use inside of my FriendListView. I have defined 2 GeometryReader views within the FriendListView but do not know how I can define the size of each row from within the FriendView to make it maintain the appropriate size.
The need for this comes from not every image that could be set by a user later on for their friend will have the same dimensions so I want to keep it consistent. I understand I could set a .frame() modifier and hardcode the width but I would like it to be relational to the screen to be consistent across all phone screens.
Example: When a row is not expanded, the image should take up half of the device width. When it is expanded I would want it to take up one quarter of the device screen width.
Screenshot
Application View
Sample Code
struct FriendListView: View {
var pets = ["Woofer", "Floofer", "Booper"]
var body: some View {
GeometryReader { geo1 in
NavigationView {
GeometryReader { geo2 in
List(self.pets, id: \.self) { pet in
FriendView(name: pet)
}// End of List
}
.navigationBarTitle(Text("Furiends"))
}// End of NavigationView
}// End of GeometryReader geo1
}// End of body
}
struct FriendView: View {
#State private var isExpanded = false
var randomPic = Int.random(in: 1...2)
var name = ""
var breed = "Dawg"
var body: some View {
Button(action: self.toggleExpand) {
HStack {
VStack {
Image("dog\(self.randomPic)")
.resizable()
.renderingMode(.original)
.scaledToFill()
.clipShape(Circle())
if self.isExpanded == false {
Text(self.name)
.font(.title)
.foregroundColor(.primary)
}
}
if self.isExpanded == true {
VStack {
Text(self.name).font(.title)
Text(self.breed).font(.headline)
}
.foregroundColor(.primary)
}
}
}// End of Button
}
func toggleExpand() {
isExpanded.toggle()
}
}
You can define a GeometryReader in the top level FriendListView and pass the screen width to the FriendView:
struct FriendListView: View {
var pets = ["Woofer", "Floofer", "Booper"]
var body: some View {
GeometryReader { geo in
NavigationView {
List(self.pets, id: \.self) { pet in
FriendView(name: pet, screenWidth: geo.size.width)
}
.navigationBarTitle(Text("Furiends"))
}
}
}
}
Then use the .frame to set the maximum width for an image.
struct FriendView: View {
...
var screenWidth: CGFloat
var imageWidth: CGFloat {
isExpanded ? screenWidth / 4 : screenWidth / 2
}
var body: some View {
Button(action: self.toggleExpand) {
HStack {
VStack {
Image("dog\(self.randomPic)")
.resizable()
.renderingMode(.original)
.scaledToFill()
.clipShape(Circle())
.frame(width: imageWidth, height: imageWidth) // <- add frame boundaries
if self.isExpanded == false {
Text(self.name)
.font(.title)
.foregroundColor(.primary)
}
}
if self.isExpanded == true {
VStack {
Text(self.name).font(.title)
Text(self.breed).font(.headline)
}
.foregroundColor(.primary)
}
}
}
}
func toggleExpand() {
isExpanded.toggle()
}
}
In case you'd want your picture to be centered you can use Spacer:
HStack {
Spacer()
FriendView(name: pet, screenWidth: geo.size.width)
Spacer()
}

Resources