What's with Constraints in SwiftUI? - ios

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

Related

How to make a unlimited width view in SwiftUI that overflow the screen?

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?

(SwiftUI) How to Read Geometry of a reactive view?

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

Configure edge insets based on size of sibling view in a ZStack in SwiftUI

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?

Setting View frame based on GeometryReader located inside that View in SwiftUI

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:

How to set weight in UIStackView in IOS

UIStackView is similar to Android LinearLayout but I could not figure out how to set weight for the subviews.
Suppose I have a vertical UIStackView and 3 UIImageViews in it. I want to set weights 3, 6, 1 consecutively for the UIImageViews. How do I do that?
UIStackView doesn't have the same concept of weights. It can use a subview's intrinsicContentSize as a weight, but setting a specific intrinsicContentSize typically requires making a subclass and it's used in other situations too (unlike the android:layout_weight attribute you're familiar with, which is only used by LinearLayout).
But since UIStackView works by applying constraints to its arranged subviews, you can get the effect of weights by setting additional constraints between the heights of the subviews. (UIStackView is designed to let you add your own constraints to tweak the layout this way.)
In your case, you want to constrain the height of the top view to be 3 times the height of the bottom view, and you want to constrain the height of the middle view to be 6 times the height of the bottom view.
You can set up a proportional-height constraint in a storyboard by creating an equal-height constraint, then editing the constraint's multiplier.
In code, you could do it like this (on iOS 9.0 or later):
NSLayoutConstraint.activateConstraints([
top.heightAnchor.constraintEqualToAnchor(bottom.heightAnchor, multiplier: 3),
middle.heightAnchor.constraintEqualToAnchor(bottom.heightAnchor, multiplier: 6),
])
Intrinsic content size, etc, is totally uninvolved.
To set fractional heights, just set fractional heights:
Fix the height of the stack view (say, the whole screen)
Put in the three views A, B, C
For A, make an height constraint 0.3 of the height of the stack view
For B, make an height constraint 0.6 of the height to the stack view
Set the stack view to distribution:Fill.
If you run this on a powerful iPhone, it will figure out that C is "0.1".
You're done.
First off, do you really need a stack view for this? It would be much easier to arrange this simply using proportional height constraints directly.
However, you can do this with a stack view if you really want to use a stack view. The secret is that the "weight" in question is simply the arranged view's intrinsicContentSize().height. Knowing this, I was easily able to set up a stack view consisting of three image views in the proportions you request:
Those, for purposes of the demonstration, are the same image repeated three times: one at 3x height, one at 6x height, and one at 1x height.
How did I do it? I gave the three image views tag values of 300, 600, and 100 respectively in the storyboard. (Of course I could have used an IBInspectable custom property for this, and in real life, I would do so.) Then I made them all instances of my UIImageView subclass, MyImageView, whose code looks like this:
class MyImageView: UIImageView {
override func intrinsicContentSize() -> CGSize {
print(self.tag)
return CGSizeMake(CGFloat(self.tag), CGFloat(self.tag))
}
}
The stack view's Distribution is configured as Fill Proportionally. The image views are configured with their Content Mode as Scale To Fill. Result: The stack view, in laying out its arranged views, consults the intrinsicContentSize method, and thus does the right thing.

Resources