SwiftUI Center Content alignment without supporting frames - swift

I'm having trouble trying to center a single element to emulate the navigation modal with a close button.
I would like to center content without using a supporting Rectangle on the sides or spacers.
What i'm trying to achieve is whenever the text grow, if it reaches the left sides where there is the close xmark button it should try to push itself on the right where there is available space until it reaches the right border and after wrap itself if there are no available space on the both sides.
here are some pictures:
expected result 1
expected result 2
current solution short text
current solution long text
i tried using long and short text to test the content behaviour
Currently this is the start of the code and basically i would like to avoid to add the blue rectangle (that would be usually clear)
struct TestAlignmentSwiftUIView: View {
var body: some View {
HStack(spacing: 0) {
Rectangle().fill(Color.blue).frame(width: 44, height: 44)
Text("aaa eee aaa")
.background(Color.red)
.padding(5)
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
.background(Color.green)
}
}
What i've tried so far but doesn't resolve the issue if the code inside the text component grow:
Using a zstack where i place the text and the close button one on
top of each other but the button is pushed to the side using a spacer. It will work for small text or content but is not scalable if the text grows
var body: some View {
ZStack {
HStack {
Spacer()
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
Text("aaa eee aaa random long very long text that should wrap without overlapping. long text")
.background(Color.red)
.frame(maxWidth: .infinity, alignment: .center)
.padding(5)
.opacity(0.7)
}
.background(Color.green)
}
Using alignment guides :
i would create my own center alignment guide, then use this custom alignment on a vstack where i place my content plus a fake filler rectangle that should center the elements on the content side.
the problem is that with swiftui , as far i know, you can only align one descendant element, and doesn't support multiple custom alignments on the stack of elements. so i would have only the text centered or the side button aligned not both aligned one to the center and the other to the trailing edge. and if i put a spacer between them it will just mess the alignment created. If I try with small text they will be both attached.
Heres the code. try to comment the button and you will see that it will center itself or add spacer between them.
extension HorizontalAlignment {
private enum MyAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
d[HorizontalAlignment.center]
}
}
static let myAlignment = HorizontalAlignment(MyAlignment.self)
}
var body: some View {
VStack(alignment: .myAlignment, spacing: 0) {
HStack {
Text("aaa eee aaa random ")
.background(Color.red)
.frame(maxWidth: .infinity, alignment: .center)
.padding(5)
.alignmentGuide(.myhAlignment, computeValue: { dimension in
dimension[HorizontalAlignment.center]
})
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: 44, height: 44)
.background(Color.yellow)
})
}
.background(Color.green)
Rectangle()
.fill(Color.purple)
.frame(width: 10, height: 10, alignment: .center)
.alignmentGuide(.myhAlignment, computeValue: { dimension in
dimension[HorizontalAlignment.center]
})
}
}
Tried with a combination of geometry reader and/or anchor preferences to read with sizes of the text content and side button width and apply the appropriate center offset manually, but it seems too hacky and it never worked as expected without good results
If you're familiar with uikit this problem would be resolved using a
centerX on the container with a minor layout priority and a right constraint from the center to the
close button, and call it a day. But on swiftui it seems soo hard to
handle this simple cases.
So far i haven't found a solution without using a supporting fixed frame on the side that would work with both long and short text. that space is clearly visibile if you try to use long text. and it will leave the user to wonder why is not used.
¯\ (ツ)/¯
EDIT: added possible solution in the answers

From the #Yrb suggestion in the comments, here's what i came up that shrink the blue size so it will center on the available space.
I added a fake text underneath and tracked the size. and if it's over the available space i will take the difference and shrink the blu rectangle.
One thing to keep in mind is that the hidden content if contains some text should have linelimit 1, otherwise it will get a smaller size from wrapping itself.
And i just assume that i know the size of the close button (or at least one side) for center alignment, and even if i don't know it at compile time, i could probably use a preference key to get the size at run time, and have it dynamic.
But for the moment i think it's fine the result that i got.
but honestly i hope to find something more easier in the future.
#State var text: String = "aaa eee aaa"
#State private var fillerWidth: CGFloat = 44
// i assume i know the max size of the close button or at least one side
private let kCloseButtonWidth: CGFloat = 44
private struct FakeSizeTitlteContentKey: PreferenceKey {
static var defaultValue: CGFloat { .zero }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
var body: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .top)) {
GeometryReader { parentGeometry in
titleContent
.lineLimit(1) // hidden text must not wrap
.overlay(GeometryReader { proxyFake in
Color.clear.border(Color.black, width: 0.3)
.preference(key: FakeSizeTitlteContentKey.self, value: proxyFake.frame(in: .local).width
.onPreferenceChange(FakeSizeTitlteContentKey.self) { value in
let availableW = parentGeometry.frame(in: .local).width
let fillSpace = availableW - value - kCloseButtonWidth * 2
fillerWidth = min(kCloseButtonWidth, max(0, fillSpace))
}
})
}
.hidden()
VStack {
HStack(spacing: 0) {
Rectangle()
.fill(Color.blue)
.frame(width: fillerWidth, height: 44)
titleContent
.background(Color.green)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity, alignment: .center)
Button(action: {}, label: {
Image(systemName: "xmark")
.padding(15)
.frame(width: kCloseButtonWidth, height: kCloseButtonWidth)
.background(Color.yellow)
})
}
.coordinateSpace(name: "fullCont")
.background(Color.green)
TextEditor(text: $text)
.frame(maxHeight: 150, alignment: .center)
.border(Color.black, width: 1)
.padding(15)
Spacer()
}
}
}
#ViewBuilder var titleContent: some View {
HStack(spacing: 0) {
Text(text)
.background(Color.red)
.padding(.horizontal, 5)
}
}

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")
}
}
}

How to align shapes and text accurately in SwiftUI

Is there a way to align object such that they resze according to the unused space?
Here is an example - there are three objects, a background square and foreground text with a highlight rectangle below the text. The text and the highlight should be spaced as though there is an equal amount of space either side (or near enough)...
This is as far as I have got (I have tried a variety of different iterations although the Spacer command seems a little ideosynchratic (depending on a variety of factors I don't entirely understand yet)...
struct hlGroup: View {
var body : some View {
Color(red:20/255,green: 45/255, blue:71/255)
.frame(width:50, height:56)
.cornerRadius(6)
.overlay(alignment:.center){
Spacer()
VStack(){
Text(String(0))
.foregroundColor(.white)
.font(Font.system(size:30, weight:.bold))
HStack(spacing: 2){
hlShow(hlColor: Color(red:100/255, green: 227/255, blue: 50/255))
}
Spacer()
}}}}
struct hlShow: View {
let hlColor :Color
var body: some View {
Rectangle()
.foregroundColor(hlColor)
.frame(width:30, height:6)
.cornerRadius(2)
}}
Unless I'm missing some specific requirement, the layout could be easily achieved using a VStack as the main container (added a border to text acting as a bounds indicator - previewing at 80 × 80):
struct SODemo: View {
var body: some View {
VStack {
Spacer()
Text("Text")
.foregroundColor(.white)
.font(Font.system(size: 30, weight: .bold))
.border(.red)
Spacer()
Color.green.frame(height: 6)
Spacer()
}
.background(Color.blue)
.cornerRadius(6)
}
}

SwiftUI-Text overflowing screen

I am trying to fit a long piece of text inside the screen but it keeps overflowing. Is there a way to wrap the text?
I tried using alignment to make it center but it still goes off the screen.
import SwiftUI
struct OnboardingPage3: View {
var body: some View {
ZStack {
Color("Onboarding")
.edgesIgnoringSafeArea(.all)
VStack {
Color("Onboarding")
.edgesIgnoringSafeArea(.all)
Image("HomeScreen")
.resizable()
.frame(width: 300, height: 600)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 200, trailing: 0))
Text("This is your home screen where you can see how much progress you have made throughout the day as well as a streaks bar to keep track of how many days straight you have been exercising.")
.frame(alignment: .center)
}
}
}
}
struct OnboardingPage3_Previews: PreviewProvider {
static var previews: some View {
OnboardingPage3()
}
}
Your text isn't actually overflowing — it's just being truncated.
To prevent this you can use the fixedSize(horizontal:vertical:) modifier. I also made some other edits to your code — there's no need to use so many .edgesIgnoringSafeAreas, and ZStacks can have some unexpected side effects with positioning.
struct OnboardingPage3: View {
var body: some View {
VStack {
Image("HomeScreen")
.resizable()
.aspectRatio(contentMode: .fit) /// use this to maintain the aspect ratio
.frame(width: 200) /// now you only need to supply 1 dimension
Text("This is your home screen where you can see how much progress you have made throughout the day as well as a streaks bar to keep track of how many days straight you have been exercising.")
.fixedSize(horizontal: false, vertical: true)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(20)
.background(
Color.gray
.edgesIgnoringSafeArea(.all)
)
}
}
Result:

SwiftUI Text not shrinking with .fixedSize()

I'm working on an app with a piece of large text with buttons underneath which I want to take up the same width as the text. My solution is to use a VStack with the .fixedSize() modifier:
VStack {
Text(viewModel.timeString)
.animation(nil)
.minimumScaleFactor(0.1)
.lineLimit(1)
.font(.system(size: 100, design: .monospaced))
.padding(.vertical, -20)
TimerActionsCompact(viewModel: viewModel) // the 3 buttons
}
.fixedSize()
This produces the result I want:
However, when the text becomes too big, it just goes off the screen instead of shrinking, even though I have minimumScaleFactor and lineLimit set.
I found that the problem was .fixedSize(), and removing it causing the text to shrink properly.
Am I doing something wrong, or could this be a bug with swiftUI? If so is there any workaround?
Thanks!
One possible way you can use frame modifier like this example:
struct ContentView: View {
#State private var string: String = "0.0"
var body: some View {
Text(string)
.font(.system(size: 100, design: .monospaced))
.minimumScaleFactor(0.1)
.lineLimit(1)
.frame(width: 200, height: 80) // give your custom size here as you like!
.background(Color.gray.cornerRadius(10.0))
.animation(nil, value: string)
Button("add") { string += String(describing: Int8.random(in: 0...9)) }.padding()
}
}

Making Button span across VStack

I currently have the following SwiftUI view:
HStack {
...
VStack {
TextField { ... }
SecureField { ... }
Button { ... }
}
...
}
I've added a .background(Color.green) to the Button, and as you can see, the view is very snug to the text.
I'm wondering if there's a way to adjust the width of the button so that it fills across VStack - something like a .fill mode for UIStackView.
The best way to do this is via .frame(maxWidth: .infinity)
https://developer.apple.com/documentation/swiftui/view-layout
If you want the button not to be centered you need to specify alignment.
e.g.: .frame(maxWidth: .infinity, alignment: .leading)
Button(action: handleSignInAction) {
Text("Sign In")
}
.frame(maxWidth: .infinity)
.background(Color.green)
Old answer from 2019:
You could use a HStack with a Text and Spacer to get a Button that fills the width of its parent:
Button(action: handleSignInAction) {
HStack {
Spacer()
Text("Sign In")
Spacer()
}
}.background(Color.green)
#d.felber's answer is almost complete, but you'd need a Spacer() on each side to center:
Button(action: {
// TODO: ...
}) {
HStack {
Spacer()
Text("Sign In")
Spacer()
}
}
.frame(maxWidth: .infinity)
Did the trick for me.
If you would like to stick to the method SwiftUI docs suggest you need to use GeometryReader and set the buttons width manually. Geometry reader updates its properties for different devices and upon rotation.
GeometryReader { geometry in
Button().frame(width: geometry.size.width, height: 100)
}
Use frame(maxWidth: .infinity) inside the Button like this:
Button(action: {...}) {
Text("Sign In")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.green)
Not like this, because the tappable area doesn't stretch:
Button(action: {}) {
Text("Sign In")
}
.frame(maxWidth: .infinity)
.background(Color.green)
Make the button text's frame the size of the UIScreen and then set the background color after it (make sure all style changes are done after changing the frame size, otherwise the style changes will only be visible on the original default frame). The frame size will propagate upward to increase the width of the button to the width of the screen as well.:
Button(action: {
// Sign in stuff
}) {
Text("Sign In")
.frame(width: UIScreen.main.bounds.width, height: nil, alignment: .center)
.background(Color.green)
}
You can also add some negative horizontal padding in between setting the frame and background in order to offset from the edge of the screen:
Button(action: {
// Sign in stuff
}) {
Text("Sign In")
.frame(width: UIScreen.main.bounds.width, height: nil, alignment: .center)
.padding(.horizontal, -10.0)
.background(Color.green)
}
Something like this?
Button(action: {
// Do your login thing here
}) {
Capsule()
.frame(height: 44)
.overlay(Text("Login").foregroundColor(Color.white)).padding()
}
I am not sure if there is a .fill method similar to UIStackView but, if what you want to do is provide some spacing on the Button (or any view for that matter) what worked for me is either setting the frame or padding
.frame(width: 300, alignment: .center) (we can also set a height here but, if not it should be able to infer the height based on the button text.
If you don't want to set an arbitrary width, you can also import UIKit and make use of UIScreen and get the devices full width. (There may be a SwiftUI way of getting this but, haven't found it at this time yet)
.frame(width: UIScreen.main.bounds.width, alignment: .center)
and then you can add a little bit of padding for some breathing room
.padding(.all, 20)
The issue with the padding is that it will add onto the additional width of the screen so we would have to take into account when setting the width.
(20 * 2 sides from leading and trailing)
.frame(width: UIScreen.main.bounds.width - 40, alignment: .center)
(breathing room is for when the text covers to the end of the screen or if your alignment is .leading or .trailing)