I'm still somewhat new to SwiftUI and I'm getting a weird case that I don't fully understand. Basically, I have a VStack that contains some Text Views but also has a background View. Ideally, I'd like the background to grow in width as much as it needs to up to a point. I figure that is what the minWidth and maxWidth are for in .frame()
I started with this and it seems to be working:
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message")
}
.background(
Rectangle()
.foregroundColor(Color.blue)
.frame(minWidth: 0,
maxWidth: 270)
)
}
}
So far so good, but when I make the text big enough that it would need to wrap, this is what I get.
So it seems that by putting the frame around the background only makes the min/max affect that background View.
If I then try to put the frame around the VStack, I get this:
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message")
}
.frame(minWidth: 0,
maxWidth: 270)
.background(
Rectangle()
.foregroundColor(Color.blue)
)
}
}
Even though I don't think I have something pushing it out, it still pushes out the the full maxWidth.
I've also tried moving the frame to the Text but that gives the same result.
What is the correct way to get a VStack with background to only grow with its contents up to a maxWidth?
Thank you!
Well I'm dumb, literally right after posting I remembered something about how the order of the modifiers on a View matter.
I put the frame after the background and it worked.
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message abcdefghijklmnopqrstuvwxyz")
}
.background(
Rectangle()
.foregroundColor(Color.blue)
)
.frame(minWidth: 0,
maxWidth: 270)
}
}
I'll leave my question here just incase it somehow helps someone else someday.
Related
I'm trying to get views to take up as much room/space as possible based on the minimum requirements of other views in the layout, but I can't seem to get the exact result I want.
Consider this code...
struct MainView: View {
var body: some View {
VStack {
Color.red
HStack {
Color.green
Color.blue
.frame(maxWidth: 100)
}
.frame(maxHeight: 100)
}
.padding()
}
}
This produces the following output. However, this is only because HStack and Blue have their maxHeight and maxWidth values set respectively.
What I'm trying to do is have the opposite... I want the minimum size of blue to dictate the size of the others.
In this example, I want green to push blue as far to the right as it can until blue says 'I'm as small as I can be!' and then have green fill the rest horizontally.
Likewise, I want red to push the HStack down as far as it can go until the HStack says 'I can't get any shorter (also because of blue) and have red fill in the rest of the vertical space.
Now from the documentation, I thought it stated if you specify red.frame(maxHeight: .infinity) and green.frame(maxWidth: .infinity), it should work, but they seem to have no effect at all, let alone giving me the desired result.
struct MainView: View {
var body: some View {
VStack {
Color.red
.frame(maxHeight: .infinity)
HStack {
Color.green
.frame(maxWidth: .infinity)
Color.blue
.frame(minWidth: 100, minHeight: 100)
}
}
.padding()
}
}
That code produces this...
So what am I missing? How can I let the minimum size of blue dictate the rest of the layout?
Use the layoutPriority modifier with a value larger than zero to tell SwiftUI you want Color.red to get as much space as possible. From the documentation:
Views typically have a default priority of 0 which causes space to be apportioned evenly to all sibling views. Raising a view’s layout priority encourages the higher priority view to shrink later when the group is shrunk and stretch sooner when the group is stretched.
Here's your second attempt, with layoutPriority(1) attached to Color.red:
struct MainView: View {
var body: some View {
VStack {
Color.red
.layoutPriority(1)
HStack {
Color.green
Color.blue
.frame(minWidth: 100, minHeight: 100)
}
}
.padding()
}
}
Output:
I am trying to replicate what I used to do in UIKit when presenting a ViewController using UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning.
So, for example, from a view that looks like this:
I want to present a view (I would say a modal view but I am not sure if that is the correct way to go about it in SwiftUI) that grows from the view A into this:
So, I need view B to fade in growing from a frame matching view A into almost full screen. The idea is the user taps on A as if it wanted to expand it into its details (view B).
I looked into SwiftUI transitions, things like this:
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
So, I think I need to build a custom transition. But, I am not sure how to go about it yet being new to this.
How would I build a transition to handle the case as described? Being able to have a from frame and a to frame...?
Is this the right way of thinking about it in SwiftUI?
New information:
I have tested matchedGeometryEffect.
Example:
struct TestParentView: View {
#State private var expand = false
#Namespace private var shapeTransition
var body: some View {
VStack {
if expand {
// Rounded Rectangle
Spacer()
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
.padding()
.foregroundColor(Color(.systemGreen))
.animation(.easeIn)
.onTapGesture {
expand.toggle()
}
} else {
// Circle
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(width: 100, height: 100)
.foregroundColor(Color(.systemOrange))
.animation(.easeIn)
.onTapGesture {
expand.toggle()
}
Spacer()
}
}
}
}
It looks like matchedGeometryEffect could be the tool for the job.
However, even when using matchedGeometryEffect, I still can't solve these two things:
how do I include a fade in / fade out animation?
looking at the behavior of matchedGeometryEffect, when I "close" view B, view B disappears immediately and what we see animating is view A from where B was back to view A's original frame. I actually want view B to scale down to where A is as it fades out.
You would have to use the .matchedGeometryEffect modifier on the two Views that you would like to transition.
Here is an example:
struct MatchedGeometryEffect: View {
#Namespace var nspace
#State private var toggle: Bool = false
var body: some View {
HStack {
if toggle {
VStack {
Rectangle()
.foregroundColor(Color.green)
.matchedGeometryEffect(id: "animation", in: nspace)
.frame(width: 300, height: 300)
Spacer()
}
}
if !toggle {
VStack {
Spacer()
Rectangle()
.foregroundColor(Color.blue)
.matchedGeometryEffect(id: "animation", in: nspace)
.frame(width: 50, height: 50)
}
}
}
.padding()
.overlay(
Button("Switch") { withAnimation(.easeIn(duration: 2)) { toggle.toggle() } }
)
}
}
Image should be a GIF
The main two parts of using this modifier are the id and the namespace.
The id of the two Views you are trying to match have to be the same. They then also have to be in the same namespace. The namespace is declared at the top using the #Namespace property wrapper. In my example I used "animation", but it can really be anything, preferably something that can uniquely identify the Views from other types of animations.
Another important piece of information is that the '''#State''' variable controlling the showing/hiding of Views is animated. This is done through the use of withAnimation { toggle.toggle() }.
I'm also quite new to this, so for some more information you can read this article I found from the Swift-UI Lab:
https://swiftui-lab.com/matchedgeometryeffect-part1/
I'm trying to figure out the basics of ScrollViews in SwiftUI.
I figured if I created a Text with a frame of the width of the screen and .infinite height, which I understood to mean "as large as the available space, e.g. safe area", and dropped it into a ScrollView with another Text companion, I'd get a screen-sized Text that could scroll horizontally to the companion Text.
struct ContentView: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
GeometryReader { geometry in
VStack {
Text("crash me")
}
.frame(width: geometry.size.width,
height: .infinity,
alignment: .topLeading)
}
Text("crash me")
}
}
}
If I run this, it just crashes. What's so stupid about it?
Instead of:
.frame(height: .infinity)
You should use:
.frame(maxHeight: .infinity)
Setting the exact height to .infinity is not possible, but maxHeight means that the view will stretch its height to the maximum value possible. This would be the size of the screen, or any other limiting factor set by the parent.
I want to completely color the background of my app while at the same time positioning content at the top so that its far enough from the status bar and top notch on those devices that have it.
If I do something like this:
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
Text("Top of my view")
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.yellow)
}
}
}
This displays the text at the top of the content, below the safe area. But only the content area is yellow. The safe areas are white.
So I add .edgesIgnoringSafeAreas(.all) below the .background modifier.
Now my text is below the notch (or smack up at the top of the screen below the Status bar text) and not visible.
I don't want to randomly guess at a top padding because that might be fine on phones with notches but look wrong on phones or iPads without notices.
If I place my VStack inside of a GeometryReader, the reader.safeAreaInsets.top is zero (0) when the top safe area is ignored.
I hope this a clear enough question. Has anyone run into this and have a solution?
You need to apply the edgesIgnoringSafeArea modifier to Color.yellow only.
A possible solution is to use a ZStack:
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
ZStack {
Color.yellow
.edgesIgnoringSafeArea(.all)
VStack {
Text("Top of my view")
Spacer()
}
}
}
}
}
One way to fix it is to apply .edgesIgnoringSafeArea(.all) just to the Color.yellow inside of .background():
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
VStack {
Text("Top of my view")
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.yellow.edgesIgnoringSafeArea(.all))
}
}
}
I'm trying to center a bunch of views in a VStack within a ScrollView in SwiftUI. To simplify things, I'm just trying to get it to work with a single Text view. Here's what I've come up with so far:
var body: some View {
ScrollView(alwaysBounceVertical: true){
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
Spacer()
} //HStack
.background(Color.green)
} //ScrollView
.background(Color.gray)
}
This results in this:
I want the text to be in the middle like this:
So the HStack should be full-width and the Text should be centered within it. It seems like this should be easy, but I don't get what I'm doing wrong. :)
Using GeometryReader, you can get information about the size of the containing view and use that to size your view.
var body: some View {
GeometryReader { geometry in <--- Added
ScrollView(alwaysBounceVertical: true){
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
Spacer()
} //HStack
.frame(width: geometry.size.width) <--- Added
.background(Color.green)
} //ScrollView
.background(Color.gray)
}
}
edit: after looking into this more, it seems that part of the problem is that you are using a ScrollView. If you remove that parent, the spacers in the HStack will automatically cause stretching to fill the view. I'm guessing the automatic stretching doesn't happen in ScrollViews because there's no finite limit to how big it can be, how much would it stretch? (because a ScrollView can scroll in any direction)
This seems to be a bug in Xcode 11.0 beta, ScrollView content wouldn't fill the scroll view. If you replace the ScrollView with a List it will work as expected. But if you have to use a scroll view, one workaround is to fix the scroll view's content width.
So your code will look something like this:
ScrollView(alwaysBounceVertical: true) {
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
Spacer()
} // HStack
.frame(width: UIScreen.main.bounds.width) // set a fixed width
.background(Color.green)
} // ScrollView
.background(Color.gray)
Result:
You should use the modifier frame and set its maxWidth: .infinity.
So it tells its parent: "the wider, the better" :)
var body: some View {
ScrollView(.vertical, showsIndicators: true){
HStack(alignment: .center) {
Spacer()
Text("This Is a Test")
.frame(maxWidth: .infinity) // <- this
Spacer()
} //HStack
.background(Color.green)
} //ScrollView
.background(Color.gray)
}
And this works regardless its parent.
Scrollview or whatever View it's set in.
Paul is doing a great job clarifying it to all of us here:
https://www.hackingwithswift.com/quick-start/swiftui/how-to-give-a-view-a-custom-frame
Answer compatible with Xcode 12.1 (12A7403)
I hope this helps 👍
dsa