I am trying to make a "reusable" template for views in my app. As part of this I started prototyping this:
var body: some View {
NavigationView {
ZStack {
VStack {
// Spacer()
Image("progress_bar")
.resizable()
.scaledToFit()
.foregroundColor(Color.gray)
.background(Color.green)
HStack{
}
Spacer()
}
VStack{
}
}
}
.navigationBarHidden(true)
}
}
The ZStack contains 2 VStack. First one is my template and will be part of multiple of my screens later on. Second Stack is destined to be replaced by #ViewBuilder parameter so that I can reuse that in multiple screens easily.
The progress_bar image is a SVG file imported into assets, preserving Vector Data and rendered as template (So I can change colour).
My issue, as shown on the following screenshot, is that the image somehow extends toward the top of the screen. The green area correspond to the green coloured background added to the image. The progress bar is the grey line across the screen.
progress bar extending toward top of the screen
If I change my code to (commented out the spacer):
// Spacer()
Image("progress_bar")
.resizable()
.scaledToFit()
.foregroundColor(Color.gray)
.background(Color.green)
HStack{
}
Spacer()
}
I get this, progress bar shifts down in the screen (not wanted but expected) but the green area that was added on top of the image disappears:
updated screen with progress_bar shifted down and not over extending
I did try setting up a maxHeight to my Image view but this did not work out.
What am I missing? Is there a way I can stop this from happening?
Edit:
After more looking around, my issue is coming from the fact that the whole thing is embedded in a NavigationView. Apparently space is saved for the navigation bar even though it is hidden.
Related
I have a VStack with multiple child views (the one with blue background). The VStack has horizontal padding. I want to have this padding set for each child, but sometimes I have exception where I want that child to reach edges of the display completely (Two grey lines above "Checkout" button). Is there any way how to allow this to happen? I don't wanna set padding for every single child separately.
You can apply a negative padding on the view that you applied on the VStack, that means if you applied a padding of 16 points to the VStack like this for example .padding(16) for all directions which is the default. then you can apply a .padding(.horizontal,-16) to the lines and they will stretch to the end of the screen
here is a sample code and a screenshot for the behavior you want.
struct VStackPadding: View {
var body: some View {
VStack{
RoundedRectangle(cornerRadius: 4)
.frame(width: .infinity,height: 3)
.padding(.horizontal, -16)
.padding(.bottom,16)
RoundedRectangle(cornerRadius: 4)
.frame(width: .infinity,height: 3)
}.padding(16)
}
}
Xcode Version - 13.1
I'm having some issues with a Background Img & the grouping Form{}. What's happening is when placing a background image in a ZStack and include the grouping option, Form{}, the Background Img disappears.
Below my code showing my Background Image inside a ZStack. Also including a link to a screen shot of the Preview -> https://i.stack.imgur.com/u12Sw.png
var body: some View {
ZStack {
Image("login")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
}
}
However, when I add a Form{} inside the ZStack, the background image completely disappears and only the Form{} (w/ a TextField &SecureTextField) appears on the Preview Sans the Background Img. Below is the code w/ and a link to the Screen shot of the Preview -> https://i.stack.imgur.com/BI3U2.png
var body: some View {
ZStack {
Image("login")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
Form {
TextField(
"Username (email)",
text: self.$username)
.autocapitalization(.none)
.disableAutocorrection(true)
SecureTextField(text: $password)
}
}
}
My assumption is that as long as the Form is inside the ZStack, the form should overlay the Background Img.
I like how the Form looks rather than two separate TextFields. Are there anyways to do this w/ the Form or something similar to a Form?
This is what I do at the moment:
I either add this modifier to the form.
.onAppear {
UITableView.appearance().backgroundColor = .clear
}
Or in an init() of the View
this has some side effects on the rest of the view if you have List or Forms but it works for me.
I have a List within a NavigationView where each view under List should have navigatable elements attached to it (cover image, user avatar + name, etc.) For example, clicking the cover image navigates to view A, while clicking the user's name/avatar navigates to view B. Sadly, in all cases, the entire list element was clickable and did not grant the intended behavior.
At first, I tried wrapping my content within a NavigationLink.
NavigationLink(destination: Text("Media"), tag: .media, selection: $selection) {
WebImage(url: URL(string: activity.media?.coverImage?.extraLarge ?? ""))
.resizable()
.placeholder { color }
.cornerRadius(8)
.frame(width: 90, height: 135)
}
This causes an arrow to appear to indicate the view is navigatable for the user but is unwanted in this situation. It was also taking up a lot of space from the view unnecessarily.
My next attempt was to wrap the view and NavigationLink in a ZStack.
ZStack {
NavigationLink(destination: Text("Media"), tag: .media, selection: $selection) {
EmptyView()
}.hidden()
WebImage(url: URL(string: activity.media?.coverImage?.extraLarge ?? ""))
.resizable()
.placeholder { color }
.cornerRadius(8)
}.frame(width: 90, height: 135)
The .hidden() modifier was applied to the NavigationLink to prevent the arrow from appearing when the image was transparent. While this solution both hides the arrow and cleans up the extra space, there are two issues:
The entire list element is still clickable.
A ZStack covered by the .frame modifier requires I know how large I want to make it. The user's name & avatar view can't easily overcome this dilemma.
Thirdly, I tried wrapping the view in a Button where the label was the cover image and the action was to change selection to navigate programmatically, but this brought the spacing issue from #1 and the overall issue of the list element being clickable.
I later discovered a solution that would cut down the previous issues I had, but brought one problem. To understand it, this is what my main activity view looks like:
NavigationView {
List(viewModel.activities) { activity in
ActivitySelectionView(activity: activity, selection: $selection)
}.navigationTitle("Activity Feed")
}.onAppear {
viewModel.fetchActivities()
}
By encapsulating List(...) {...} in a ScrollView and changing List to a ForEach, I was able to produce the output I wanted: clickable view within an element, the cover image became lighter when clicking on it, opposed to the list element becoming darker as a whole until let go, etc.
However, this is not a list. It does not look good, nor will it look better on other platforms (this is an iOS project). For example, this code does not respect the edges as a list does. It also does not include a divider, but the Divider struct can help. I feel this is not the right solution to this problem.
To sum it all up, how do I create a List inside a NavigationView where the list respects what views inside an element are navigatable?
I found an elegant solution to my problem, so I'd like to share it for people who may stumble upon this question in the future.
You need to use a ScrollView within the List {...} somewhere. In the ScrollView block, it's perfectly suitable to make certain elements in the list cell navigatable.
NavigationView {
List(1..<11) { num in
ScrollView {
Text("\(num)!")
NavigationLink(destination: Text("Number: \(num)")) {
Text("Click me")
}
}
}
}
When you use a SwiftUI Form it creates padding on the leading edge, which is fine for Form input but I would like to add additional content to the Form e.g. an image that takes the entire width of the screen, but because the image is in the Form, padding gets applied and the image gets pushed off screen slightly. How can I remove all Form padding?
struct MyForm: View {
var body: some View {
Form {
Image(uiImage: someImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: UIScreen.main.bounds.width)
TextField("Name", text: $name)
// Other fields
}
}
}
I know that the underlying view for Form is a UITableView where I can do things like UITableView.appearance().backgroundColor = .clear to change the Form appearance, but I can't figure out how to remove the leading padding.
I also know I can move the Image view outside the Form and put everything in a stack, but that creates other issues with scrolling that I'd like to avoid.
Here is a solution. Tested with Xcode 11.4 / iOS 13.4
Image(uiImage: someImage)
.resizable()
.aspectRatio(contentMode: .fill)
.listRowInsets(EdgeInsets()) // << this one !!
Note: hardcode to UIScreen.main.bounds.width is not needed
I can't figure out how to align Image view on top of ZStack, by default views in SwiftUI are placed at the center of their parent, and we then use stacks to align them, I have the following piece of code:
struct ContentView: View {
var body: some View {
ZStack {
Image("bgImage")
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack
}
}
How can I position the image to the top ?
To tell the ZStack to align things a particular way within it, configure it with the alignment parameter:
ZStack(alignment: .top) {
Color.clear
Image(...)
Text("Hello, World!")
}
(Color.clear expands to fill all available space, so this forces your ZStack to be as large as the enclosing view without needing to add a .frame().)
That will align everything at the top of course, which might not be what you want. You could fix that by making a nesting your ZStacks to align as you want them to:
ZStack{
ZStack(alignment: .top) {
Color.clear
Image(...) // This will be at the top
}
Text("Hello, World!") // This will be centered
}
That said, I'd probably use a .background for this example.
ZStack {
Color.clear
Text("Hello, World!")
}
.background(Image(...), alignment: .top)
And if you only have one view, you can get rid of the ZStack and use a frame instead:
Text("Hello, World!")
.frame(maxHeight: .infinity)
.background(Image(uiImage:#imageLiteral(resourceName: "image.jpg")),
alignment: .top)
Keep in mind that in this case the image will draw outside its frame. In many cases that's fine (and it's completely legal), but it can matter sometimes (for example, if you put this inside a stack). You can add .border(Color.green) to the end to see how that works.
This example really gets to the heart of SwiftUI layout, so it's worth understanding what's going on. This isn't a workaround or a trick, so you should get to the place where this feels very normal.
The top-level content view (the one that contains the ZStack) offers its entire space to the ZStack. A ZStack is always exactly the size that contains its contents, so first the ZStack needs to layout its children. It lays them out according to its alignment, and then sizes itself exactly to fit around them. So with top-alignment (but without Color.clear), the Image is at the top of the ZStack. The ZStack is just exactly the same size as the Image.
The top-level content view then places the ZStack in its center.
The way the ZStack lays out its children is similar to how the content view did. It offers all the space it was offered, and then the child-views decide their sizes. Views always decide their own sizes. The Image and Text are fixed-sized views, so they are just the size of their contents. But Color is a flexible-sized view. It accepts the entire space that the ZStack offered (which is the same space that the top-level content view offered) and returns that as its size. Since a ZStack must exactly contain its children, the ZStack is now the size of the top-level content view, and things behave as you expect, aligning at the top of the screen.
Let's compare to using .frame() as you originally did:
ZStack {
Image("bgImage")
Text("Hello, World!")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red) //this is for debugging purposes, to show the area of the ZStack
First, I want to focus on your comment, because it's not correct. This is not the area of the ZStack. The ZStack is exactly the size of its contents (the Image and the Text). You put the background on the frame view, and the frame view is larger.
A key confusion people have is that they think .frame(...) changes the size of the View it's attached to. That's not correct at all. As before, a ZStack is always the size of its contents. .frame() creates a completely new view of the requested size. It then positions the wrapped view inside itself according to the frame's alignment. So in this example it works like this:
Top-level - Background - Frame - ZStack - { Image Text }
The top-level view offers all its space to the Background. Backgrounds are the size of what they contain, so it offers all of that space to the Frame. The Frame is flexible in both directions (due to the max fields), and so it ignores its child's size and chooses to be the size it was offered.
The Frame then offers all that space to the ZStack. The ZStack lays out its children, and returns its size as exactly the size that contains them.
The Frame then places the ZStack according to the Frame's alignment (.center, since that's the default). If you'd set the Frame's alignment to .top, then the ZStack would have been placed at the top of the frame (but the text would be centered in the ZStack not in the Frame).
It then reports to the Background that it is as large as the top-level view (since its flexible).
The Background then claims that same size to the top-level content view.
And finally, the top-level content view places the Background in its center.
You could always just put the things you want to be at the top in a VStack and use a Spacer.
ZStack(){
Image(...)
Spacer()
}
The complete code should look something like this:
struct ContentView: View {
var body: some View {
ZStack(){
Text("Hello, World!")
VStack {
Image(...)
Spacer()
}
}
}
}
You could do this with HStacks as well. Important to notice that if the image has no limits to its size, it will always take up as much space as possible. That would remove the purpose of the Spacer. Hope this helps :-)
So one thing working against you is the infinity maxHeight modifier, assuming that you do not want some space between the image and the bottom of the view.
.edgesIgnoringSafeArea(.all)
You may just need to tell your ZStack to ignore safe area insets.
struct ContactsView: View {
var body: some View {
ZStack {
Image("bgImage")
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: .infinity).background(Color.red).edgesIgnoringSafeArea(.all)
}
}
If you need space between the bottom and the image, wrap the ZStack in a VStack and throw a Spacer in the bottom of the VStack.
struct ContactsView: View {
var body: some View {
VStack {
ZStack {
Image(systemName: "bgImage")
Text("Hello, World!")
}.frame(maxWidth: .infinity, maxHeight: 300).background(Color.red)
Spacer()
}.edgesIgnoringSafeArea(.all)
}
}
ZStack {
Image("background")
.resizable()
.scaledToFill()
.edgesIgnoringSafeArea(.all)
}
Add .edgesIgnoringSafeArea(.all)