SwiftUI - Change view height based on height of text - ios

I am creating a custom alert where I want to increase the height of the view based on the height of message in text view. Sharing the code below:
Custom Alert View:
import SwiftUI
struct CustomAlertView: View {
var body: some View {
VStack {
Image(systemName: "circle").resizable().frame(width: 40, height: 32).padding(.top, 20).padding(.bottom, 5).foregroundColor(.blue)
Text("Alert title").foregroundColor(Color.black).font(.title).bold()
Spacer()
Text("This is an alert message.").foregroundColor(Color.secondary).font(.title3).lineLimit(nil)
Spacer()
HStack {
Button(action: { }) {
Text("Cancel")
}
.frame(minWidth: 75.0, idealWidth: 100.0, maxWidth: 150.0, minHeight: 20.0, idealHeight: 25.0, maxHeight: 30.0)
.padding(10.0)
.background(Color.white)
.foregroundColor(Color.gray)
.cornerRadius(12.0)
.font(.headline.bold())
Button(action: { }) {
Text("OK")
}
.frame(minWidth: 75.0, idealWidth: 100.0, maxWidth: 150.0, minHeight: 20.0, idealHeight: 25.0, maxHeight: 30.0)
.padding(10.0)
.background(Color.blue)
.foregroundColor(Color.white)
.cornerRadius(12.0)
.font(.headline.bold())
}.padding(.bottom, 20)
}.frame(minHeight: 200.0)
.background(Color.white)
.cornerRadius(28)
.clipped()
}
}
Content View:
import SwiftUI
struct ContentView: View {
#State var showAlertView: Bool = false
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
if showAlertView {
CustomAlertView()
} else {
Button("Click me") {
withAnimation {
self.showAlertView = true
}
}.frame(width: UIScreen.main.bounds.width/2-30, height: 40).buttonStyle(.borderedProminent)
}
}.background {
Color.gray
}
}
}
If I set "frame(height: 200.0)" on CustomAlertView, then the height is smaller and fits the one line text i.e. "This is an alert message." fine. But with height as 200, if the message becomes larger, then the text does not fit properly within the view.
I tried setting "frame(minHeight: 200.0)" and "frame(maxHeight: .infinity)", but in both cases CustomAlertView's height becomes full screen.
How do I dynamically increase the height of alert view based on the message I display on it?
Thanks!

Related

SwiftUI Custom Navigation Bar VStack doesn't work

I'm trying to make a custom navigation bar with back button, image, VStack (2 labels) but it didn't work. The whole view will stick to the center and not following the alignment I set. Thank you!
struct WeatherNavigation: View {
var body: some View {
HStack {
WeatherNavigation()
}
.
.
.
}
}
//
struct WeatherNavigation: View {
var body: some View {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50, alignment: .leading)
VStack {
Text(weather.description)
.font(.appFont(size: 18))
.foregroundColor(Color(uiColor: .black))
Text(weather.location)
.font(.appFont(size: 12))
.foregroundColor(Color(uiColor: .blue))
}
.frame(width: .infinity, height: 50, alignment: .leading)
}
}
First of all, you shouldn't set frame of the whole view like that. For you problem, it can divide into: make a HStack to store all the container view and make a space between those two of them. Because using HStack you don't need to add leading.
Code will be like this
struct WeatherNavigation: View {
var body: some View {
// make a HStack to store all the attribute
HStack(alignment: .top) {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50)
VStack {
Text("Tokyo")
.foregroundColor(Color(uiColor: .black))
Text("Japan")
.foregroundColor(Color(uiColor: .blue))
}
// make a space at the end
Spacer()
}
}
}
And the usage like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
WeatherNavigation()
}
Spacer()
}
}
}
More over: adding Spacer() means that make space between view. That will solved your problem if you want to keep like your way.

SwiftUI TextEditor Divider doesn't change Y position based on text-line count?

I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:

(SwiftUI) can anyone tell me why my NavigationView is creating extra NavigationBars and blank space in the footer? (video included)

I have an app with 3 views. they are connected by a NavigationView and have NavigationLinks that link to the next view. however, all views are created with an extra NavigationBar and with lots of blank white space on the bottom that travels up the screen every time you click through the views and go back to the home page. I remember a few days ago having this issue when I went from the HomeView() to the TimerView() but I'm not sure how I got rid of it. and also I definitely did not get rid of it because I'm still having the same problem. and also I don't remember how I worked around this the first time. I have seen other posts which say I should set the navigationBar color to clear but that does nothing. not sure what is going on. most other posts on this topic are about removing the space up top and very few talk about the blank space on the bottom so I'm not really sure what to do.
I go into the capture view hierarchy and I see that white footer bar is created when the view is created but then just seems to stack on top of every other view until the application is unusable. using Xcode 13.1 and running in simulator on iPhone 12 with iOS 15.0.
here's the video as well as pictures from the capture view hierarchy thing https://imgur.com/a/jBprYbN
and here's the code
struct ContentView: View {
#State private var selectedTab = 0
let numTabs = 2
let minDragTranslationForSwipe: CGFloat = 50
#State var secondScreenShown = true
#State private var tabSelection = 0
#State private var tappedTwice:Bool = false
#State private var homeID = UUID()
var handler: Binding<Int> { Binding (
get: {self.tabSelection},
set: {
if $0 == self.tabSelection {
tappedTwice = true
print("tappedTwice = true")
}
self.tabSelection = $0
}
)}
init() {
UITabBar.appearance().isTranslucent = false
}
var body: some View {
TabView(selection:handler) {
NavigationView {
HomeView()
.id(homeID)
.tabItem {
Label("Home", systemImage: "house.fill")
}.tag(0)
.onChange(of: tappedTwice, perform: { tappedTwice in
guard tappedTwice else { return }
homeID = UUID()
self.tappedTwice = false
})
}
}
.edgesIgnoringSafeArea(.all)
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
}
}
here's the HomeView()
struct HomeView: View {
#State var secondScreenShown = false
#State var timerVal = 1
#State private var chosenSound = "None"
var times = [1, 2, 3, 4, 5, 10, 15, 20, 30, 45, 60]
var sounds = ["None", "Creepy Kids", "Conjuring The Dark Ones", "The Forbidden Forest" ]
init() {
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
UITableView.appearance().tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
var body: some View {
// NavigationView {
VStack {
VStack {
VStack {
Group {
VStack {
NavigationLink(
destination: TimerView(timerScreenShown: $secondScreenShown, timerVal: timerVal, chosenSound: chosenSound, initialTime: timerVal),
isActive: $secondScreenShown,
label: {Text("Go")
.font(.title2)
.padding()
.frame(minWidth: 250)
.overlay(
Capsule(style: .continuous)
.stroke(Color.black, lineWidth: 3)
)
})
.frame(minWidth: /*#START_MENU_TOKEN#*/0/*#END_MENU_TOKEN#*/, maxWidth: 100, alignment: .bottom)
.padding(.top, 10)
} // third group
}
} // VStack just outside rectangle
.frame(
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
.padding()
// ) // rectangle.overlay()
} // VStack
.edgesIgnoringSafeArea([.top, .bottom])
.background(Color.pink)
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
// } // NavigationView
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarTitle("main level")
}
}
}
And here's the TimerView()
struct TimerView: View {
#Binding var timerScreenShown:Bool
#State var timerVal:Int
#State var chosenSound:String
#State private var showNextView = false
#State private var paused = false
var initialTime:Int
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
VStack {
VStack {
NavigationLink(
destination: HomeView(),
label: {Text("Home Page")})
.font(.title2)
.frame(minWidth: 0,maxWidth: 200, alignment: .center)
.overlay(
Capsule(style: .continuous)
.stroke(Color.black, lineWidth: 3)
)
.padding(.top, 35)
// } // else
} // innermost VStack
.frame(
alignment: .center
)
.padding()
// )
} // VStack 2
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
} // VStack
.edgesIgnoringSafeArea([.top, .bottom])
.background(Color.purple
// Image("HomePageBackground")
// .resizable()
// .ignoresSafeArea()
)
.frame(
minWidth: UIScreen.main.bounds.width,
maxWidth: .infinity,
minHeight: UIScreen.main.bounds.height,
maxHeight: .infinity,
alignment: .center
)
.onAppear(){
UITableView.appearance().tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
UITableView.appearance().tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: Double.leastNonzeroMagnitude))
}
.navigationBarTitle("timer view")
} // View
}
so yeah it's obvious there's extra navigation views being created somewhere but I'm not sure where. it's also obvious that blank space in the footer is created when the view is loaded but I don't know where. so yeah any help would be appreciated.
if there are any brackets/parenthesis missing it's because I accidentally deleted it while removing commented out extraneous code.
The issue is caused by the UITabBar.appearance().isTranslucent = false line -- without that, as you confirmed in the comments, it behaves as expected.
Here's a working version with a note about that line:
import SwiftUI
struct ContentView: View {
init() {
UITabBar.appearance().isTranslucent = false //REMOVE THIS and functionality will be as-expected
}
var body: some View {
TabView {
NavigationView {
HomeView()
}.tabItem {
Label("Home", systemImage: "house.fill")
}
.navigationViewStyle(StackNavigationViewStyle())
.tag(0)
}
}
}
struct TimerView: View {
var body: some View {
VStack {
NavigationLink(
destination: HomeView(),
label: {Text("Home Page")})
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.background(Color.purple)
.edgesIgnoringSafeArea([.top, .bottom])
.navigationBarTitle("timer view")
}
}
struct HomeView: View {
var body: some View {
VStack {
NavigationLink(
destination: TimerView(),
label: {
Text("Go")
})
.frame(minWidth: 0, maxWidth: 100, alignment: .bottom)
.padding(.top, 10)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.pink)
.edgesIgnoringSafeArea([.top, .bottom])
.navigationBarTitle("main level")
}
}

SwiftUI - Button with Image is clickable outside

I have a ScrollView with multiple Buttons. A Button contains a Image and a Text underneath.
As the images are pretty large I am using .scaledToFill and .clipped. And it seems that the 'clipped' part of the image is still clickable even if it's not shown.
In the video you see I am clicking on button 1 but button 2 is triggered.
This is my Coding. The Image is inside the View Card.
struct ContentView: View {
#State var useWebImage = false
#State var isSheetShowing = false
#State var selectedIndex = 0
private let images = [
"https://images.unsplash.com/photo-1478368499690-1316c519df07?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2706&q=80",
"https://images.unsplash.com/photo-1507154258-c81e5cca5931?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2600&q=80",
"https://images.unsplash.com/photo-1513310719763-d43889d6fc95?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1585766765962-28aa4c7d719c?ixlib=rb-1.2.1&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1485970671356-ff9156bd4a98?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2734&q=80",
"https://images.unsplash.com/photo-1585607666104-4d5b201d6d8c?ixlib=rb-1.2.1&auto=format&fit=crop&w=2700&q=80",
"https://images.unsplash.com/photo-1577702066866-6c8897d06443?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2177&q=80",
"https://images.unsplash.com/photo-1513809491260-0e192158ae44?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2736&q=80",
"https://images.unsplash.com/photo-1582092723055-ad941d1db0d4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2700&q=80",
"https://images.unsplash.com/photo-1478264635837-66efba4b74ba?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjF9&auto=format&fit=crop&w=2682&q=80"
]
var body: some View {
NavigationView {
ScrollView {
VStack(spacing: 40) {
Text(useWebImage ? "WebImage is used." : "SwiftUI Image is used")
.font(.system(size: 18))
.bold()
.kerning(0.5)
.padding(.top, 20)
Toggle(isOn: $useWebImage) {
Text("Use WebImage")
.font(.system(size: 18))
.bold()
.kerning(0.5)
.padding(.top, 20)
}
ForEach(0..<images.count) { index in
Button(action: {
self.selectedIndex = index
self.isSheetShowing.toggle()
}) {
Card(imageUrl: self.images[index], index: index, useWebImage: self.$useWebImage)
}
.buttonStyle(PlainButtonStyle())
}
}
.padding(.horizontal, 20)
.sheet(isPresented: self.$isSheetShowing) {
DestinationView(imageUrl: self.images[self.selectedIndex], index: self.selectedIndex, useWebImage: self.$useWebImage)
}
}
.navigationBarTitle("Images")
}
}
}
struct Card: View {
let imageUrl: String
let index: Int
#Binding var useWebImage: Bool
var body: some View {
VStack {
if useWebImage {
WebImage(url: URL(string: imageUrl))
.resizable()
.indicator(.activity)
.animation(.easeInOut(duration: 0.5))
.transition(.fade)
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.clipped()
} else {
Image("image\(index)")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.clipped()
}
HStack {
Text("Image #\(index + 1) (\(useWebImage ? "WebImage" : "SwiftUI Image"))")
.font(.system(size: 18))
.bold()
.kerning(0.5)
Spacer()
}
}
.padding(2)
.border(Color(.systemRed), width: 2)
}
}
Do you have an idea how to fix this issue?
I already tried to use .resizable(resizingMode: .tile) but I need to shrink the image before I could use just a tile.
For detailed information you can also find the project on GitHub GitHub Project
I would appreciate your help a lot.
The .clipped affects only drawing, and by-default Button has all content clickable not depending what it is.
So if you want make your button clickable only in image area, you have to limit hit testing only to its rect explicitly and disable everything else.
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4.
Demo code (simplified variant of your snapshot):
struct ButtonCard: View {
var body: some View {
VStack {
Image("sea")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 250, maxHeight: 250, alignment: .center)
.cornerRadius(12)
.contentShape(Rectangle()) // << define clickable rect !!
.clipped()
HStack {
Text("Image #1")
.font(.system(size: 18))
.bold()
.kerning(0.5)
Spacer()
}.allowsHitTesting(false) // << disable label area !!
}
.padding(2)
.border(Color(.systemRed), width: 2)
}
}
struct TestClippedButton: View {
var body: some View {
Button(action: { print(">> tapped") }) {
ButtonCard()
}.buttonStyle(PlainButtonStyle())
}
}

Center View horizontally in SwiftUI

How can I center horizontally a View (Image) in an HStack? I want a button to be left aligned and the image to be centered horizontally the view.
Currently I have this structure:
VStack {
HStack {
Button(action: {
print("Tapped")
}, label: {
Image("left-arrow")
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
}).padding(.leading, 20)
Spacer()
Image("twitter-logo")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
}
Spacer()
}
Which is giving me this:
But I want to achieve this:
You can embed two HStack's in a ZStack and place spacers accordingly for the horizontal spacing. Embed all that in a VStack with a Spacer() to have everything pushed up to the top.
struct ContentView : View {
var buttonSize: Length = 30
var body: some View {
VStack {
ZStack {
HStack {
Button(action: {
}, label: {
Image(systemName: "star")
.resizable()
.frame(width: CGFloat(30), height: CGFloat(30), alignment: .leading)
}).padding(.leading, CGFloat(20))
Spacer()
}
HStack {
Image(systemName: "star")
.resizable()
.frame(width: CGFloat(30), height: CGFloat(30), alignment: .center)
}
}
Spacer()
}
}
}
Note: In the second HStack, the image should automatically be center aligned, but if it isn't, you can place a Spacer() before and after the image.
Edit: Added the VStack and Spacer() to move everything to the top like the OP wanted.
Edit 2: Removed padding on image because it caused the image to be slightly offset from the center. Since it is in its own HStack and center-aligned, it does not need padding.
Edit 3: Thanks to #Chris Prince in the comments, I decided to make a simple NavigationBar-esque custom view that you can provide left, center, and right arguments to create the effect that the OP desired (where each set of views are aligned independently of each other):
struct CustomNavBar<Left, Center, Right>: View where Left: View, Center: View, Right: View {
let left: () -> Left
let center: () -> Center
let right: () -> Right
init(#ViewBuilder left: #escaping () -> Left, #ViewBuilder center: #escaping () -> Center, #ViewBuilder right: #escaping () -> Right) {
self.left = left
self.center = center
self.right = right
}
var body: some View {
ZStack {
HStack {
left()
Spacer()
}
center()
HStack {
Spacer()
right()
}
}
}
}
Usage:
struct ContentView: View {
let buttonSize: CGFloat = 30
var body: some View {
VStack {
CustomNavBar(left: {
Button(action: {
print("Tapped")
}, label: {
Image(systemName: "star")
.resizable()
.frame(width: self.buttonSize, height: self.buttonSize, alignment: .leading)
}).padding()
}, center: {
Image(systemName: "star")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
}, right: {
HStack {
Text("Long text here")
Image(systemName: "star")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.padding(.trailing)
}.foregroundColor(.red)
})
Spacer()
Text("Normal Content")
Spacer()
}
}
}
What's about saving button size to a property and add a negative padding to the image? And pay attention to an additional spacer after the image.
struct ContentView: View {
var buttonSize: Length = 30
var body: some View {
VStack {
HStack {
Button(action: {
print("Tapped")
}, label: {
Image(systemName: "star")
.resizable()
.frame(width: buttonSize, height: buttonSize, alignment: .leading)
})
Spacer()
Image(systemName: "star")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
.padding(.leading, -buttonSize)
Spacer()
}
Spacer()
}
}
}
The result:
Easiest way for me:
ZStack(){
HStack{
Image("star").resizable().foregroundColor(.white).frame(width: 50, height: 50)
Spacer()
}
Image("star").resizable().font(.title).foregroundColor(.white).frame(width: 50, height: 50)
}
You center the view using position property try this code
Group{ // container View
Image("twitter-logo")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
}.position(x: UIScreen.main.bounds.width/2)
the right way to center the Title like navigationbar:
HStack {
Spacer()
.overlay {
HStack {
Image(systemName: "star")
Spacer()
}
}
Text("Title")
Spacer()
.overlay {
HStack {
Spacer()
Image(systemName: "star")
}
}
}
You can place the view that you want to center into a VStack and then set the alignment to center. Make sure that you also set the frame(maxWidth: .infinity) or else it will be centering your view in the VStack but the VStack might not take up the entire width of the screen so you might not get the appearance you are trying to achieve.
To make it even easier, write it as a function that extends the View object
extension View {
func centerInParentView() -> some View {
VStack(alignment: .center) {
self
}
.frame(maxWidth: .infinity)
}
}
And then you can just call it as you would a view modifier i.e.
VStack {
HStack {
Button(action: {
print("Tapped")
}, label: {
Image("left-arrow")
.resizable()
.frame(width: 30, height: 30, alignment: .leading)
}).padding(.leading, 20)
Spacer()
Image("twitter-logo")
.resizable()
.frame(width: 30, height: 30, alignment: .center)
}
Spacer()
}
.centerInParentView()
Works every time for me
I have got an alternative solution. I used a hidden Image as placeholder.
HStack {
Image("left-arrow").padding()
Spacer()
Image("twitter-logo")
Spacer()
// placeholder to keep layout symmetric
Image("left-arrow").padding().hidden()
}
Of course you can replace the Images with Buttons or other Views as you prefer.
Here is what worked for me
HStack {
Image(systemName: "star.fill")
.frame(maxWidth: .infinity, alignment: .leading)
Image(systemName: "star.fill")
.frame(maxWidth: .infinity, alignment: .center)
Text("")
.frame(maxWidth: .infinity, alignment: .trailing)
}
.foregroundColor(.yellow)
Inspired by SwiftUI - How to align elements in left, center, and right within HStack?
Let me propose a different solution:
https://gist.github.com/buscarini/122516641cd0ee275dd367786ff2a736
It can be used like this:
HStack {
Color.red
.frame(width: 0, height: 50)
.layoutPriority(1)
GlobalHCenteringView {
Text("Hello, world!")
.lineLimit(1)
.background(Color.green)
}
.background(Color.yellow)
Color.red
.frame(width: 180, height: 50)
.layoutPriority(1)
}
}
This will center the child view in the screen if it fits, or leave it as is if it doesn't. It is currently using UIScreen, so it only works on iOS, but you could easily pass the screen or parent width to the constructor of the view, getting it from a GeometryReader or whatever.

Resources