SwiftUI Fixed constraint from bottom of view to top of screen - swift

Question: How to set fixed "constraint" from buttom of view to top of the screen?
My problem explanation:
I'm trying to implement such element on screen:
Rounded shape which has stable height in all screens
I use Oval to draw it like
public var body: some View {
VStack {
ZStack(alignment: .bottom) {
Ellipse()
.fill(Color.surfacePrimary)
.frame(width: 546, height: 364)
.offset(x: 0, y: -20)
Text("Text")
.padding(.bottom, 60)
.foregroundColor(.onPrimary)
}
.frame(minWidth: 0, idealWidth: .infinity, maxHeight: 20)
.edgesIgnoringSafeArea(.top)
Text("This text should be right below yellow shape, and it is, but on screens with not rectangle forms, on screens with rectangle form, it pops up on yellow zone")
Spacer()
}
}
Problem: that it works good on iphones 11, 12. But on 8, 8+, SE (where screens have right rectangle form) frame of ZStack move on top but Ellipse left on the same place. And Views which should be below yellow zone move to yellow zone.
UPD: I solved problem with different heights of StatusBar, but now I have another problem which was not observable before.
My new code:
public var body: some View {
ZStack(alignment: .bottom) {
Ellipse()
.fill(.yellow)
Text("Text")
.padding(.bottom, 42)
.foregroundColor(.red)
}
.frame(width: 546, height: 364)
.position(x: UIScreen.main.bounds.size.width / 2, y: Spacing.padding_0_5)
.edgesIgnoringSafeArea(.top)
.background(Color.red)
}
This ZStack fills almost all height of the screen. But I expect it should has fixed height. It looks like:
(hmmm ... some problem with uploading image)

Related

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 Center Content alignment without supporting frames

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

How can I implement a slowly sliding image that has been clipped to shape using SwiftUI?

So far I have this circle and image inside it on my view:
and here is the code for it:
Image(chosenImage)
.resizable()
.scaledToFit()
.clipShape(Circle())
.shadow(radius: 20)
.overlay(Circle().stroke(Color.white, lineWidth: 5).shadow(radius: -20))
.frame(width: width, height: width, alignment: .center)
.offset(y: -height * 0.05)
How can I make the image slide slowly to the left within the circle?
The circle should not move, only the image within it should move.
As the image ends another copy of it should be displayed and the action repeated. Or the image could quickly jump back to its previous position and start moving again. Another way to do this is when the image reaches its end it starts slowly moving to the right.
Any ideas on how to do this or is there any libraries that can help me achieve this?
The answer Jeeva Tamil gave is almost correct however it moves the image while staying in the shape of a circle (shown bellow).
Whereas I need it to "show different parts of the image as it moves".
Use .mask before the .overlay modifier to move the image within the circle. Here, I've used Draggesture to demonstrate the behaviour.
#State private var horizontalTranslation: CGFloat = .zero
.
.
.
Image(chosenImage)
.resizable()
.scaledToFit()
.clipShape(Circle())
.offset(x: horizontalTranslation)
.gesture(
DragGesture()
.onChanged({ (value) in
withAnimation {
horizontalTranslation = value.translation.width
print(horizontalTranslation)
}
})
.onEnded({ (value) in
withAnimation {
horizontalTranslation = .zero
}
})
)
.mask(Circle())
.shadow(radius: 20)
.overlay(Circle().stroke(Color.white, lineWidth: 5).shadow(radius: -20))
.frame(width: width, height: width, alignment: .center)

Prevent view from overflowing in both directions

I'm trying to build a user-expandable view that will show more of its content by increasing its height. To accomplish this I will add .clipped() so that the content shown out of its bounds will be hidden, just like you would add overflow: hidden; in CSS.
However, it seems like by default VStack is centering its children, so when the height is smaller than the sum of the children's height, they overflow in both the top and the bottom.
Here is an example of what I'm talking about:
struct ExandableView: View {
var body: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.red)
.frame(height: 50)
.padding(.horizontal)
Rectangle()
.fill(Color.green)
.frame(height: 50)
.padding(.horizontal)
}
.frame(height: 90)
.background(Color.blue)
}
}
Is there any way to make it behave so that the red item (the first) is always inside the blue container and the children can only overflow from the bottom?
Change frame alignment according to ExandableView. like this
struct ExandableView: View {
var body: some View {
// Other VStack code
}
.frame(height: 90, alignment: isExpand ? .top : .center) //<--Here
.background(Color.blue)
}
}

Aligning objects in ZStack

The white bar is supposed to be aligned at the left of the darker bar. I've tried using spacers, or changing the alignment of the individual objects but nothing works. This is my code:
VStack {
HStack {
Text("Calories eaten today:")
.foregroundColor(.white)
.fontWeight(.semibold)
Spacer(minLength: 100)
HStack {
ZStack(alignment: .leading) {
Capsule()
.rotation(.degrees(90))
.frame(width: 20, height: CGFloat((calorieGoal/proportion)))
.foregroundColor(.black)
.opacity(0.2)
Capsule()
.rotation(.degrees(90))
.frame(width: 20, height: CGFloat((Swift.min((eatendatabase.dayNutrients[0]/proportion), 180))), alignment: .leading)
}
Spacer(minLength: 20)
Text("\(String(format: "%.0f", eatendatabase.dayNutrients[0]))")
.font(.caption)
.foregroundColor(.white)
.frame(alignment: .trailing)
}
Spacer()
}
}
.padding(.horizontal, 30)
As calorieGoal, proportional, and eatendatabase.dayNutrients were not provided as values, I have used the following constant and variable to keep track of the capsule's widths, which I will denote in the code as the "progress bar":
let MAX_WIDTH: CGFloat = 100.0
let currentProgress: CGFloat = 40.0
With that in mind, first you should consider moving the calorie count label ("832") outside of the HStack containing the progress bar. Second, you nested both the background and foreground capsules forming the progress bars in a ZStack. We can take the foreground capsule and align it to the left by nesting it in an HStack with a Spacer:
HStack {
Capsule()
.rotation(.degrees(90))
.frame(width: 20.0, height: currentProgress /*This is the progress width of the bar, out of 100*/)
Spacer()
}
Of course, we need to set the width of the HStack to the width of the background capsule, so that the foreground capsule aligns properly with the leading edge of the background capsule:
HStack {
...
}.frame(width: MAX_WIDTH)
Overall, here is a possible solution to your issue:
VStack {
HStack {
Text("Calories eaten today:")
.foregroundColor(.white)
.fontWeight(.semibold)
// Made this font smaller for the preview
.font(.footnote)
// Add auto space between the label and the progress bar
Spacer()
// Separate the capsule progress bar from the calorie count
ZStack {
Capsule()
.rotation(.degrees(90))
.frame(width: 20.0, height: MAX_WIDTH /*This is the max width of the bar*/)
.foregroundColor(.black)
.opacity(0.2)
// Nesting the progress Capsule in an HStack so we can align it to the left
HStack {
Capsule()
.rotation(.degrees(90))
.frame(width: 20.0, height: currentProgress /*This is the progress width of the bar, out of 100*/)
// Adding a Spacer will force the Capsule to the left when it is in an HStack
Spacer()
}
// Set the frame width of the HStack to the same width as the background capsule
.frame(width: MAX_WIDTH)
}
// Add auto space between the progress bar and the calorie count
Spacer()
// The calorie count text, nested in an HStack with the label and the progress bar
Text("832")
.font(.caption)
.foregroundColor(.white)
.frame(alignment: .trailing)
}
}
.padding(.horizontal, 30)
// Added a gray background for the preview
.background(Color.gray)
And it would look like this: