Alignment In SwiftUI - swiftui

I'm new to SwiftUI. I'm designing a widget for size systemMedium.
I'm getting stuck in spacing,
If I reduce the size of the Violet Container, Then the rest UI is not aligning properly
Here is My Code
case .systemMedium:
ZStack {
Color("Yellow")
HStack(alignment: .center) {
PercentageMediumView()
.padding(.trailing, 80.0)
Spacer(minLength: 0)
Last14MediumView()
}
}
Here is what it looks like,
My Code Design
I need to eliminate that space(from arrow)
What I need Design
My Actual Design Needed

Related

How to align a view bottom trailing, overlaid on another view that clips over the edge of the screen in SwiftUI?

I have a view which consists of a background image/animation and a foreground control buttons. I want to .overlay the buttons on top of the background, to be aligned at the bottom leading, center, and trailing - see screenshot below (doesn't show the actual background).
However, the background has to be an animation. I'm using a wrapper view around AVPlayer for this. The video that gets played is portrait, but it gets filled to landscape as is thus wider than screen. I need it to fill the vertical space of the screen. That means that I have to use .frame(maxWidth: .infinity, maxHeight: .infinity).scaledToFill().
That leaves vertical alignment just fine - the gray top-center badge and the bottom-center button are rendered at the right places - however, it messes with the horizontal alignment. The bottom-right button aligns itself w.r.t. the video instead of the screen, and is thus aligned out of the bounds of the screen (see the second image).
I need the background to fill the screen, and the overlay to be aligned properly. The videos are recorded portrait, but AVPlayer makes them landscape with black filling on the sides, so unless that can be tweaked, I can't change the videos' aspect-ratios.
The most beneficial thing for me would be to learn how to align components w.r.t. the screen, not the parent in the overlay stack. Is there are way to achieve that? If not, is there a workaround to fix my problem (make the buttons align properly along the horizontal)?
Code
The code below isn't the source of truth, and just generates the demos. It is provided since a gentleman in the comments politely asked for it. The images are the ultimate source of truth. The actual code is much bigger, with a mechanism of randomly (and based on app state) choosing an AVPlayer to play an mp4 video. I don't think that this should be important (encapsulation and stuff), but if it is, tell me why it affects the code structure in the comments, and I will add more details.
A function used in the demos below is:
private func bigBgImage() -> some View {
self.backgroundChooser.render()
.resizable()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.scaledToFill()
}
...where backgroundChooser.render() is to be taken as a blackbox. You can just drop in a dummy Image.
Desired layout
This demo is achieved through a dummy image in a GeometryReader. This is a workaround to bound the overlays despite the bigBgImage having maxWidth: .infinity. Since GeometryReader messes with the animation alignment, I don't want to use it in the final code. Nevertheless, here's the snippet:
GeometryReader { _ in
self.bigBgImage()
}
.overlay(alignment: .top) { self.title() }
.overlay(alignment: .bottom) { self.resetButton() }
.overlay(alignment: .bottomTrailing) { self.toggleButton() }
Undesired layout
self.bigBgImage()
.overlay(alignment: .top) { self.title() }
.overlay(alignment: .bottom) { self.resetButton() }
.overlay(alignment: .bottomTrailing) { self.toggleButton() }
The mechanism behind backgroundChooser is too complex to contain in an SO question in a meaningful way. One can just drop in any image
In this scenario we should separate controls from content. A possible approach is to have independent screen layer (bound always to screen geometry), then move content into background, and controls into overlays, like
Color.clear // << always tight to screen area
//.ignoresSafeArea() // << optional, if needed
.background(self.bigBgImage()) // now independent, but not clipped
.overlay(alignment: .top) { self.title() } // << all on screen
.overlay(alignment: .bottom) { self.resetButton() }
.overlay(alignment: .bottomTrailing) { self.toggleButton() }
So I found a workaround involving manually setting an x offset for the background.
Building on Asperi's partial answer, we add a GeometryReader and offset the background by half the width:
Color.clear
.background() {
GeometryReader { geo in
self.bigBgImage()
.offset(x: -geo.size.width / 2)
}
}
.overlay(alignment: .top) { self.title() }
.overlay(alignment: .bottom) { self.resetButton() }
.overlay(alignment: .bottomTrailing) { self.toggleButton() }
I found that the problem was your .frame and .scaleToFill() after the Image.
Here I have a different solution. (Code is below the image)
Use screen max width and height instead of .infinity with overlay
struct DemoView: View {
var body: some View {
ZStack {
bigBigImage
.overlay(alignment: .top) {
Button("Top") {
}
.padding()
.background(.black)
.cornerRadius(10)
}
.overlay(alignment: .bottom) {
Button("Center") {
}
.padding()
.background(.black)
.cornerRadius(10)
}
.overlay(alignment: .bottomTrailing) {
Button("Leading") {
}
.padding()
.background(.black)
.cornerRadius(10)
.padding(.trailing)
}
}
}
var bigBigImage: some View {
Image("Swift")
.resizable()
.scaledToFill() //here
.frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) //here
.edgesIgnoringSafeArea(.all)
}
}

Text with Text.DateStyle in SwiftUI View in Complication alignment

I want to create a complication rendered by a SwiftUI View that contains a label and a timer value.
I want the label to be on the complication background layer, and the timer value to be on the complication foreground layer so that they get tinted separately.
I would like this line of text, comprised of 2 parts, to be centered.
The trouble is, when using Text.DateStyle.timer, the Text behaves differently within a complication vs in a normal view.
In a normal view the Text frame behaves as any other text, only taking the space it needs.
When displayed in a complication, the Text frame expands to fill all the space it can, and the text within is left aligned.
This makes it so I cannot find a way to center the group of 2 Texts.
I tried a somewhat hacky approach with infinite spacers to try to steal the extra space from the Text that has the expanding frame. This works to center the content, but it causes the Text to truncate.
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
HStack {
Spacer()
.frame(maxWidth: .infinity)
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
Spacer()
.frame(maxWidth: .infinity)
}
A normal preview:
A preview of rendering within complication:
CLKComplicationTemplateGraphicExtraLargeCircularView(
ExtraLargeStack()
)
.previewContext(faceColor: .multicolor)
Edit to show full code
import ClockKit
import SwiftUI
struct ExtraLargeStack: View {
var body: some View {
VStack(alignment: .center) {
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
HStack {
Spacer()
.frame(maxWidth: .infinity)
HStack {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
.complicationForeground()
}
Spacer()
.frame(maxWidth: .infinity)
}
}
.font(.system(size: 18, weight: .regular))
.lineLimit(1)
}
}
struct ExtraLargeStack_Previews: PreviewProvider {
static var previews: some View {
/// Preview normal view
// ExtraLargeStack()
/// Preview as Complication
CLKComplicationTemplateGraphicExtraLargeCircularView(
ExtraLargeStack()
)
.previewContext(faceColor: .multicolor)
}
}
Edit: Another partial solution
Based on suggestions from #Yrb, an overlay provides a partial solution that may be good enough for my use case.
The following does not fully center the 2 part line, but it is pretty close.
HStack {
// Use placeholder text to create a view with the appropriate size for _most_ timer values that I need to support
Text("L: 00:00 ").hidden()
}
.overlay(
// overlay the real content, which is constrained to the frame created by the hidden placeholder.
HStack(spacing: 5) {
Text("L:")
.foregroundColor(.accentColor)
Text(Date() - 3599, style: .timer)
.complicationForeground()
}
)
So, I figured out what the issue with aligning Text(Date(), style: .timer) is. The timer format from hours on down. The document give this as an example: 2:59 36:59:01. It appears that .timer reserves all of the possible space it needs and then is formatted on that possible space, not the space actually used. There does not appear to be any way to change this behavior, even if your goal is a 5 minute countdown timer.
I think you need to consider slight UI change. I did find that you can change the alignment of the displayed Text with a .timer by using .multilineTextAlignment(), but that is about all you can do. The following code demonstrates this:
struct ExtraLargeStack: View {
var body: some View {
// I removed (alignment: .center) as it is redundant. VStacks default to center
VStack {
// I put the negative spacing to tighten the T: with the timer
VStack(spacing: -6) {
Text("T:")
.foregroundColor(.accentColor)
Text(Date(), style: .timer)
// If you center with .multilineTextAlignment the timer
// will be centered
.multilineTextAlignment(.center)
.complicationForeground()
}
HStack {
HStack {
Text(Date(), style: .timer)
.multilineTextAlignment(.center)
.complicationForeground()
.overlay(
Text("T:")
.foregroundColor(.accentColor)
// This offset would need to be computed
.offset(x: -30, y: 0)
)
}
}
}
.font(.system(size: 18, weight: .regular))
}
}
I left the second timer as an HStack, but I put your Text("T") as an .overlay() with a .offset(). I don't particularly like this as it will be fragile if you attempt to adjust the offset for the additional time units, but if you have a limited range, it may work well enough. Also, if you use .monospaced on the timer text, the computation should be a linear amount.

SwiftUI - How to perfectly align multiple Text with different font sizes in VStack?

I have 2 Text elements in a VStack. They are set to different font size.
How do I get the first glyph of the 2 Text elements to align perfectly left? Is that even possible?
Below is the code snippet:
extension Font {
static let countDownTitle: Font = Font.system(size: 46, weight: .regular, design: .rounded).leading(.tight)
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello!!")
Text("20.49").font(.countDownTitle)
}
}
}
You have a bit of an artificial test going on here. Remember, each character takes up a different amount of room in a proportional font takes up a different amount of room. When they are laid out, they are put in a space that is controlled by the font designer, not us. You can see this is you cycle through different numbers. The 4 is pretty much right on, but the 5 is way off. This is one of those things that your ability to control it is lacking.
While, I am not at all recommending this, you could get perfect alignment like this, using a monospaced font and an .offset():
extension Font {
static let countDownTitle: Font = Font(UIFont.monospacedSystemFont(ofSize: 46, weight: .regular))
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello!!")
Text("50.49").font(.countDownTitle)
.offset(x: -2.2, y: 0)
}
}
}
But, this is really ugly.

SwiftUI - How to size a view relative to its parent when the parent is inside a scrollview?

I'm trying to dynamically size some views which end up being placed inside of a scrollview. Here is the simplest sample code I can think of:
struct RootView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
// More views above
HStack(spacing: 16) {
MyView()
MyView()
}
.padding([.leading, .trailing], 16)
// More views below
}
}
}
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading, spacing: 24) {
Image("myImage")
.resizable()
.scaledToFill()
VStack(alignment: .leading, spacing: 0) {
Text("Text")
OtherView()
}
}
}
}
EDIT: I think really the main issue I'm having is regarding how to dynamically size each MyView inside of the HStack.
If I wanted the Image in MyView to be sized to fill its width and grow vertically to maintain its aspect ratio, and then also size each MyView in RootView to be 40% of RootView's width, what is the best way to accomplish this? I've tried using GeometryReader but when it's nested inside the ScrollView, it causes the view its used in to collapse in on itself. If I use it outside of the ScrollView, I'm effectively always going to be getting the screen width (in this application) which isn't always what I need. On top of that, imagine that MyView is nested deeper in the view hierarchy and not called directly from RootView, but rather one of its child views. Or better yet, imagine that we don't know that RootView doesn't know its rendering a MyView if the view is determined at runtime.
To give a little context to anyone who is interested in some backstory, the app I'm trying to build is very modular in nature. The idea is that we really only have one "container view" struct that determines which views to render at runtime. We basically have a ScrollView in this container view and then any number of subviews. I'm really struggling with why it seems so difficult to set a view's content dimensions relative to its parent, any assistance would be hugely appreciated.
The best way I can think of is using a GeometryReader view. Here is an example.
GeometryReader { geometry in
RoundedRect(cornerRadius: 5).frame(width: geometry.size.width * 0.8, height: geometry.size.height * 0.8)
}
Typically I use the GeometryReader as a "Root" view and scale everything off of it, however you can place it inside of another view or even as an overlay to get the parent view size. For example.
VStack {
GeometryReader { geometry in
//Do something with geometry here.
}
}
Check it out here.
If I understood your goal correctly it is just needed to make images resizable (that makes them fill available space taking into account aspect ratio), like
VStack(alignment: .leading, spacing: 24) {
Image("myImage")
.resizable() // << here !!
.aspectRatio(contentMode: .fill)

GeometryReader inside a ScrollView collapses the GeometryReader, regardless of child sizes

I'm trying to make a sub view which contains a drawing which I want the size to be proportional to a fraction of the parent container.
This works fine except when put inside a scroll view somewhere up the hierarchy.
As seen from the screenshots when inside of the stack the loudest as expected. But when you put inside a scroll view the size of the geometry reader collapses to near zero and its children overflow its boundaries. As though it's rendering its children on a different Z index. In fact if you remove the wrapping VStack from the geometry render contents it doesn't fact layout all its children as though it were wrapped in a ZStack.
EDIT:
To be clear: the ScrollView is not owned by the component. The component should not be aware if it's in a ScrollView or not - it's just a poor helpless component, being thrown around. That's why I put the ScrollView into the preview code, not the component.
I've tried all sorts of combinations of fixed sizes and men and mags and ideal. The only work around I could find was this hacky solution - the edited part of the question near the bottom where the width reported by the geometry reader is captured through a workaround into a state variable and then re-used to set the frame size of a sibling view.
Note, I'm a complete beginner. There seems to be some interaction between the scroll view and the geometry reader that is beyond my current understanding.
Seems to be a confusing topic.
import SwiftUI
struct ScrollViewGeoReader: View {
var body: some View {
GeometryReader{ g in
VStack{
let width = g.size.width
Circle().frame(width: width/3, height: width/3, alignment: .center)
Text("inside geo")
Text("inside geo")
Text("inside geo \(width)")
Text("inside geo")
Text("inside geo")
}
.border(Color.green, width: 2)
}
.border(Color.red, width: 3)
}
}
struct ScrollViewGeoReader_Previews: PreviewProvider {
static var previews: some View {
VStack {
// VStack {
ScrollView {
ScrollViewGeoReader()
Text("Next scrollview item")
}
.border(Color.blue, width: 2)
}
}
}
ZStack (expected layout):
ScrollView (see how the red frame of the geometry read it has collapsed to size 10):
EDIT:
Also note that the same problem occurs with or without the circle, which is also a problem I have. So it's not chicken / egg as far as the width capturing is concerned, at least in terms of Circles. I would have though the Text components know their own size, and would tell the GR.
vs
Here is scratchy (w/o subviews separation) possible solution for layout. If/when internal subviews separated the geometry width can be injected by constructor arguments.
Tested with Xcode 12.
struct TestScrollViewWithGeometry: View {
var body: some View {
GeometryReader{ g in
ScrollView {
VStack{
let width = g.size.width
Circle().frame(width: width/3, height: width/3, alignment: .center)
Text("inside geo")
Text("inside geo")
Text("inside geo \(width)")
Text("inside geo")
Text("inside geo")
}
.border(Color.green, width: 2)
Text("Next scrollview item")
}
.border(Color.blue, width: 2)
}
.border(Color.red, width: 3)
}
}