How to horizontally align views with navigation bar items in SwiftUI - ios

How do you get views to align with the navigation bar items? This can be done in a UIKit app via view.layoutMarginsGuide.
Here’s an example:
var body: some View {
NavigationView {
VStack {
Text("Lorem ipsum nunc fermentum euismod.")
.background(Color.gray)
.padding() //FIXME
.navigationBarTitle("Title")
.navigationBarItems(leading: Button("Hello"){}, trailing: Button("World"){})
Spacer()
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) //make it fill the width
}
.navigationViewStyle(StackNavigationViewStyle())
}
This gives the following results on iPad 9.7" portrait and iPhone 11 Pro:

This is not a full answer, and it only addresses a small part of your question, but:
Interestingly, on iPhone, the text actually is aligned if you take kerning into account. This is a good example:
Generated by this body:
var body: some View {
NavigationView {
VStack(alignment: .leading) {
Text("Title.").background(Color.gray).padding(.leading)
Text("Hello.").background(Color.gray).padding(.leading)
Text("Lorem ipsum nunc fermentum euismod.").background(Color.gray).padding(.leading)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.navigationBarTitle("Title")
.navigationBarItems(leading: Button("Hello"){}, trailing: Button("World"){})
}
.navigationViewStyle(StackNavigationViewStyle())
}
If we zoom in, we can see the kerning in action:
It turns out that on iPhone––though apparently not on iPad––the text is actually aligned. Notice that the Hs are aligned and that the Ts are aligned:
This doesn't really answer your question because you still experience the issue on iPad. But it does at least narrow it down.

Related

SwiftUI Stack fill remaining height

I am a beginner in Swift and in iOS development. I am currently trying to create a sing in form with linear background on the top.
I've placed background view to gradient variable. Now I am trying to place my form after background on the top. But it doesn't fill the remaining space. I've tried using Spacer, but then form aligns to the bottom of the screen. How can I expand and align VStack with the form right after the gradient and make it fill the remaining height of the screen.
Here is my code
var gradient: some View {
ZStack(alignment: .top) {
LinearGradient(
stops: [
.init(color: .red, location: 0.4),
.init(color: .orange, location: 0.7),
.init(color: .yellow, location: 0.8)
],
startPoint: .leading,
endPoint: .trailing)
.frame(maxWidth: .infinity, maxHeight: 300, alignment: .top)
.mask(
RoundedRectangle(cornerRadius: 96)
.fill(Color.black)
.frame(width: .infinity, height: .infinity)
)
.blur(radius: 64)
}
.ignoresSafeArea()
.frame(height: .infinity, alignment: .top)
.offset(y: -100)
}
var body: some View {
VStack() {
gradient
Spacer()
VStack () {
VStack (spacing: 8) {
Text("Sign In")
.font(.title)
.fontWeight(.bold)
Text("Access to your account")
.font(.callout)
.foregroundColor(.secondary)
}
VStack (spacing: 18) {
TextField("Username", text: $username)
.modifier(TextFieldOutlined())
TextField("Password", text: $password)
.modifier(TextFieldOutlined())
}
.padding()
}
}
.frame(height: .infinity)
.padding(.bottom, 48)
.ignoresSafeArea()
}
Now it looks like this. With form Stack selected. It don't know why it is not filling the remaining height.
If I understand what you're looking for, you'd like the top portion of the screen to be a fixed size for the gradient, and then the form centered in the remaining part of the screen. You could do this by making the VStack expand using Spacers, modifying the body code to:
var body: some View {
VStack() {
gradient
VStack () {
// Add spacing above the form
Spacer()
VStack (spacing: 8) {
Text("Sign In")
.font(.title)
.fontWeight(.bold)
Text("Access to your account")
.font(.callout)
.foregroundColor(.secondary)
}
VStack (spacing: 18) {
TextField("Username", text: $username)
.modifier(TextFieldOutlined())
TextField("Password", text: $password)
.modifier(TextFieldOutlined())
}
.padding()
// Add spacing below the form
Spacer()
}
}
.padding(.bottom, 48)
.ignoresSafeArea()
}
Some of the padding, as currently written, will keep the form from being perfectly vertically centered, so you may want to tweak that.
Without your TextFieldOutlined() modifier, it looks like this in my preview (gradient is selected and outlined for emphasis):

Set the width of an Image to half the width of its HStack container view in SwiftUI?

So I just want to do something pretty simple which is have an Image take half the size of its container and a Vstack the other half.
Im currently doing this to force both views to take as much space as they can with maxWidth: .infinity, so they end up taking equal amounts of space:
struct ContentView: View {
var body: some View {
HStack {
Image("landscape")
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: 170)
VStack {
Text("Test")
Text("Test 2")
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: 170)
.background(Color.red)
.padding()
}
}
But it ends up in this poor result, sadly:
Any tips? I saw GeometryReader might come in handy here but seems to push my Views all the way to the top and end up with unwanted results.
you need to add a fixedSize() modifier to HStack and keep it's vertical to true.
var body: some View {
HStack {
Image("1")
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: 170)
VStack {
Text("Test")
Text("Test 2")
}
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: 170)
.background(Color.red)
.padding()
.fixedSize(horizontal: false, vertical: true)
}
you can read more about fixedsize() here
Now your HStack will be like this

Background in screens in TabView w/ PageTabViewStyle doesn't fill entire vertical space available

I want a TabView that uses a PageTabViewStyle with each individual screen having a different background colour that fills the entire vertical space available (i.e. extended into the safe areas).
TabView(selection: $selection) {
VStack {
Text("screen 1")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.green)
VStack {
Text("screen 2")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
}.edgesIgnoringSafeArea(.all)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
However, it looks like this:
And after I scroll to the bottom:
The white colour can be changed if I set a background colour for the TabView itself, which would be fine if each screen used the same background colour. Is there any way for individual screens within the TabView to have a unique background colour set that fills the entire screen?
Normally I wouldn't suggest GeometryReader, but this doesn't look like intended behavior on Apple's part. The key is expanding the background modifier color to a large enough height so that you can't see the white background behind the tab.
Thank you, Asperi, for sharing the link to a similar question for inspiration:
https://stackoverflow.com/a/62596307/12299030
struct ContentView: View {
#State private var selection: Int = 0
var body: some View {
GeometryReader { geo in
TabView(selection: $selection) {
VStack {
Text("Screen 1")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
EmptyView()
.frame(minHeight: 2 * geo.size.height)
.background(Color.green)
)
.tag(0)
VStack {
Text("Screen 2")
}.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
EmptyView()
.frame(minHeight: 2 * geo.size.height)
.background(Color.red)
)
.tag(1)
}.edgesIgnoringSafeArea(.all)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
}
Using an EmptyView combined with a set minimum height of 2 times the current height ensures that the user can scroll up and down, reaching the bounce-back distance, and not see a white background. Put any background you'd like on this view.
You may be able to swap out EmptyView with whatever you'd like, but that was the first thing that came to mind. I added .tag() and selection so that ContentView can be ran on its own in case you'd like to experiment.

alignmentGuide() not overriding child views

I initially align my child views to the .top by giving alignment: .top to my parent HStack.
I then want to have one child to the left (.leading) and the other child to the right (.trailing).
However this is not working, they are both in the center (screenshot below):
import SwiftUI
struct TestView: View {
var body: some View {
HStack{
Text("First")
.alignmentGuide(.leading) { d in d[.leading] }
Text("Second")
.alignmentGuide(.trailing) { d in d[.trailing] }
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
.padding(.top, 50)
}
}
Any idea why?
I then want to have one child to the left (.leading) and the other child to the right (.trailing).
It can be achieved in much simpler way
HStack{
Text("First")
Spacer()
Text("Second")
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)

VStack inside ScrollView bottom alignment in SwiftUI

Currently building a chat application and I need new messages to appear at the bottom of the screen. I also need to have messages aligned to the bottom. However, using VStack inside ScrollView features top alignment by default.
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity, minHeight:0, maxHeight: .infinity, alignment: Alignment.topLeading)
ForEach(notList, id: \.self) { not in
NotRow(not: not)
}
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity, minHeight:0, alignment: Alignment.topLeading)
}
What should I do to fix this?
A bit late to answer that one but it might help someone else. You could use a good old double rotation trick to achieve the result you want. Other solutions does not work because the maximum size inside a scroll view is undefined as it depends on its child views to compute its own content size.
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity, minHeight:0, maxHeight: .infinity, alignment: Alignment.topLeading)
ForEach(notList, id: \.self) { not in
NotRow(not: not)
}
}
.padding()
.rotationEffect(Angle(degrees: 180))
}
.rotationEffect(Angle(degrees: 180))
ScrollView does not propose the same height to its children (VStack in this case) as it was proposed to it. It just asks for minimal size. The solution is to make its direct child respond with the minimum height which was proposed to ScrollView itself. We can achieve this by reading the proposed height via GeometryReader and setting that as minimum height of the child.
GeometryReader { reader in
ScrollView {
VStack {
/// Spacer() or Color.red will now behave as expected
}
.frame(minHeight: reader.size.height)
}
}
I think you're missing a maxHeight in VStack
VStack(alignment: .leading) {
Spacer().frame(maxWidth: .infinity)
// content here
}
.frame(maxHeight: .infinity) // <- this
If you remove Spacer() and put it after the closing braces of your ForEach, it will fix it. Here is how your updated code will look like:
ScrollView {
VStack(alignment: .leading, spacing: 16) {
.frame(minWidth: 0, maxWidth: .infinity, minHeight:0, maxHeight: .infinity, alignment: Alignment.topLeading)
ForEach(notList, id: \.self) { not in
NotRow(not: not)
}
Spacer()
}
.padding()
.frame(minWidth: 0, maxWidth: .infinity, minHeight:0, alignment: Alignment.topLeading)
}
Try changing all of your Alignment.topLeadings to Alignment.bottomLeading

Resources