As a part of my effort to learn new technologies, I'm trying to implement one of the screens of an app I'm working on in SwiftUI. I have this rough layout:
This element should stretch to accommodate the container size (devices of different widths) and will be used in a ScrollView.
Here's the preliminary solution I came up with after some stumbling around:
This works well enough for the first attempt but I want to refactor the View by moving all the relevant parts into a separate View. In addition to that, I want that new view to automatically account for paddings and other size modification I might add later.
So, here's a naive rewrite:
As you can readily spot, GeometryReader inside ScrollView assumed the size the parent provided it, effectively hiding most of the View.
There are several "dirty" solutions to this situation: set a fixed frame height to the View, set an aspect ratio that would approximate the correct size (a bit tricky given that there are Text views with fixed heights), keep GeometryReader as the outermost element of the main view and pass the width to the subview as an initialization parameter.
I'm looking for a "clean" solution for this particular layout and, perhaps, for a more general understanding about how Views is SwiftUI regulate their sizes – in WWDC videos it is said that parent View proposes a size but then child View returns it's own size; is there a way to interfere in that process somehow or is it all done through private APIs?
Thank you!
–Baglan
p.s. I thought code screenshot besides the UI preview would be the most illustrative but let me know if I should provide code snippets as well!
I believe my answer is supposed to be a comment, but as it's going to be a lengthy one, I'm posting this as an answer. I answer a similar question if that can bring more light.
It's important in which View component you are using GeometryReader.
If you are inside a typical View that spans up to the screen's size, you get the width & height of the screen itself.
If you are inside a ScrollView environment, you get
height of the container in case of .horizontal alignment
width of the container in case of .vertical alignment
Typically, ScrollView without specifying alignment explicitly defaults to .vertical.
I suppose, the design decision is correct. Because there is a Content View in a Scroll View if you recall UIScrollView from UIKit. You don't know the size of the Content View in advance until you add some contents. In SwiftUI, the GeometryReader inside ScrollView mimics the mathematics of the Content View from UIScrollView. And that is the reason, you don't get the actual size from geometry reader.
This code works fine:
struct GeometryReaderExperiment: View {
var body: some View {
GeometryReader { geometry in
ScrollView {
Text("First")
DesignedFancy(parentGeometry: geometry, rectangleColor: .red)
Text("Second")
DesignedFancy(parentGeometry: geometry, rectangleColor: .green)
Text("Third")
DesignedFancy(parentGeometry: geometry, rectangleColor: .blue)
}
}
}
}
struct DesignedFancy: View {
var parentGeometry: GeometryProxy
var rectangleColor: Color
var body: some View {
HStack(alignment: .bottom) {
VStack {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.foregroundColor(rectangleColor)
Text("One")
}
.frame(width: parentGeometry.size.width * 2/3)
VStack {
Rectangle()
.aspectRatio(contentMode: .fit)
.foregroundColor(rectangleColor)
Text("Two")
}
}
}
}
I think the problem is that DesignedFancy doesn't know how much space it has. Therefore, you must provide this information through a variable. The result is:
Related
I'm putting a ForEach inside an HStack. I hope each element in ForEach will keep its original width and the parent HStack should overflow the screen as needed. However, this results in that the parent HStack fits the width of the screen and each element inside ForEach get narrowed down into 1/n * screenWidth.
Sample Code:
HStack(alignment: .bottom) {
ForEach(data, id: \.id) { element in
SomeView(data: element)
}
}
I could achieve a similar effect by putting the parent HStack in a ScrollView. But what if I don't want the scroll behavior? I wonder how ScrollView achieve this infinity width?
Whatever you are trying to achieve, doing that results into breaking the ui.
Take an image for example, put it into a VStack and don't make it resizable. It will overflow the view. Anything you place in that VStack will overflow too and won't align within the screens boundaries.
So your best bet is to go with a ScrollView and disable the scrolling as discussed here: How to only disable scroll in ScrollView but not content view?
I'm pretty new with SwiftUI and I'm encountering a really unnerving issue.
I've created some custom views that are being shown with the right height inside the preview. When I try to add them inside a VStack, they behave instead like I'd set .frame(minHeight: 0, maxHeight: .infinity). Adding a Spacer() after the custom view, inside the VStack, do not solve the issue.
I don't know the size of those views in advance so I can't set a fixed frame size, is there anything I can do to prevent this behaviour? I even thought about calculate their content size and apply a fixed size accordingly, but it sounds like a developer nightmare :|
Thanks for reading, and for the help!
EDIT: The most simple code that raises the issue:
VStack {
ChipGroup(chips: [
Chip(titleKey: "Chip"),
Chip(titleKey: "Chip"),
Chip(titleKey: "Chip"),
Chip(titleKey: "Chip"),
Chip(titleKey: "Chip"),
Chip(titleKey: "Chip"),
Chip(titleKey: "Chip")
])
Spacer()
}
Chip & ChipGroup are based on the following tutorial: https://prafullkumar77.medium.com/swiftui-building-chips-with-autolayout-container-dbca53bbb848
GeometryReader is the root view of your ChipGroup. GeometryReader always takes up the most space possible, expanding as much as possible.
If you don't want your view to take up all the space, you need to get rid of the GeometryReader - unless you want to set a fixed height, which you specifically mentioned you'd like to avoid. That's just how GeometryReader works.
I need to add a view on top of this chat bubble view, like an Instagram emoji reaction. Let's call it EmojiView. It will need to always be in the bottom left or right corner of the chat bubble. But the chat bubble's size will vary with how much text is in it. So I need to position EmojiView based on the size of the bubble it is on top of.
But there is no clear way for me to read the size of that bubble. GeometryReader takes up all available space, which is a problem in this situation. If I wrap the text inside a GeometryReader, it takes all the height space and I have no reliable height space to read from. If I modify the GeometryReader with .aspectRatio(.fit), the height just becomes equal to the width. If I set anything anywhere to .fixedSize() , the chat bubble no longer adapts to the text size. So please share a reliable way to read this geometry
Please help with this issue
Without GeometryReader, I can't read the geometry
With the GeometryReader, I cannot get a reliable height reading
There is no need to use GeometryReader.
Use ZStack and align the EmojiView on the bottomTrailing or bottomLeading depending on which bottom side you want to display it.
ZStack(alignment: .bottomLeading) {
HStack {
Text("I need to add a view on top of this chat bubble view, like an Instagram emoji reaction. Let's call it EmojiView. It will need to always be in the bottom left or right corner of the chat bubble. But the chat bubble's size will vary with how much text is in it. So I need to position EmojiView based on the size of the bubble it is on top of.")
.background(Color.secondary)
.padding()
}
Text("RABBITS")
.padding(8)
.background(Color.blue)
.padding(20)
}
I have just started experimenting with SwiftUI and might still be thinking too much in terms of UIKit.
I would like to use a ScrollView for my content and put a toolbar on top of it. The ScrollView should cover the whole screen and its contents should shine through the toolbar's visual effect view.
I managed to get the basic toolbar working, including the blur effect. However, as I am using DynamicType, the height of the toolbar is only known at runtime. So I need to pass the current height of the toolbar view as the bottom edge inset of my ScrollView in order to be able to see all of the content of the ScrollView.
This is what I have so far:
ZStack {
ScollView {
Text("Some very long text that does not fit onto the screen…")
}
Toolbar()
.edgesIgnoringSafeArea(.bottom)
}
How can I get the height of the toolbar and pass it to the bottom padding of the scroll view?
What is happening now with constraints in SwiftUI? Do View types adapt automatically for bigger devices etc. or what should we have to do instead?
RIP, Constraints!
SwiftUI doesn't use layout constraints. UIKit is still around, it's not deprecated and fully functional, so if you continue to use the classic approach, you can use as many constraints as you wish.
However, if you choose to go with SwiftUI → rest in peace, constraints!
The core concept to align views with each other is using stacks:
HStack
VStack
If you want to overlay views (i.e. put one view on top of another), you can use a
ZStack
The View protocol itself (to which all view types mysteriously conform) has tons of functions called modifiers that you can use to customize your view's layout.
Examples
Here are some examples how you can achieve specific layouts with those modifiers compared to using constraints:
1. Aspect Ratio
Instead of
view.widthAnchor.constraint(equalTo: view.heightAnchor, multiplier: 2)
in UIKit you would write
view
.aspectRatio(2, contentMode: .fit)
in SwiftUI.
2. Spacing Between Views
Instead of
view2.leadingAnchor.constraint(equalTo: view1.leadingAnchor, constant: 8)
in UIKit you could arrange the views in a horizontal stack and add a spacer between them and add the frame modifier to specify its width:
HStack {
view1
Spacer()
.frame(width: 30)
view2
}
3. Equal Widths
This is where it gets more complicated. You can no longer specify that two views have an equal width. If they are in the same vertical stack (i.e. aligned in a vertical line), fine: just set the contentMode to .fill and control the actual width by setting the stack view's width → mission accomplished. ✅ But if they are not (for example, when they are in a horizontal stack), you have to find other ways to express that. The actual implementation will depend on the concrete layout you're trying to describe.
The general idea of SwiftUI is to keep views as small as possible and compose them. There's a little trade-off here: You pay the price that "constraints" between views in different view hierarchies get a lot more verbose to implement, the ultimate gain is that the layout is declarative and the code to create the most common user interfaces is dramatically simplified.
Screen Adaptation / Responsiveness
Custom views fill the entire available space by default, which means that the top most view automatically fills the entire screen – regardless of the actual screen size. You can use modifiers to change that behavior.
It still has constraint, in WWDC examples we saw HStack and VStack, which seems like UIStackView, so my guess it just clips to edges. You can still add padding to views so if you want constraint a UILabel (Text) 10pt to left you do something like this:
Text("Hello World").padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 0))
Constraints are very well gone, which is great, but there is something called .padding(), which can do some sort of constraint look by putting it on the left side with something like leading parameters which make an image go to the side of a view
struct ContentView: View {
var body: some View {
Image("x")
.padding(.leading)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}