pin view to bottom of screen and ignore bottom safe area - swiftui

Layout question - how do I setup for a view block to expand over the bottom safe area? I've looked through various sources for ignoresSafeAreas() but can't achieve quite the result I'm looking for.
I want, later, to be able to expand this view upwards but start it short. If that makes sense.
var body: some View {
VStack{
Spacer()
HStack(alignment: .center) {
Text ("Expand to fill bottom safe area ...?")
.foregroundColor(.white)
}
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 50, maxHeight: 100)
.background(Color.red)
.ignoresSafeArea()
}
}

Option 1
Put ignoresSafeArea inside background. This will let the red color extend over to the device edges, but the HStack's position will stay the same.
struct ContentView: View {
var body: some View {
VStack{
Spacer()
HStack(alignment: .center) {
Text ("Expand to fill bottom safe area ...?")
.foregroundColor(.white)
}
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 50, maxHeight: 100)
.background(Color.red.ignoresSafeArea()) /// inside `background`
}
}
}
Option 2
Put ignoresSafeArea on the VStack, and everything will ignore the safe area.
struct ContentView: View {
var body: some View {
VStack{
Spacer()
HStack(alignment: .center) {
Text ("Expand to fill bottom safe area ...?")
.foregroundColor(.white)
}
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 50, maxHeight: 100)
.background(Color.red)
}
.ignoresSafeArea() /// attached to `VStack`
}
}
Result:
Option 1
Option 2

Related

SwiftUI: Resize spacers in VStack on smaller devices in a certain way

I am trying to optimize a UI with VStacks and spacers in between. The UI is ideally designed for the iPhone 13 Pro screen size (left). For smaller devices, the intention is that the spacers will shrink in a certain way that the UI still looks appealing.
I tried to achieve this by using frames for the Spacers with minHeight, idealHeight and maxHeight. The intended layout appears on the iPhone 13 Pro, but on a smaller device like the iPhone SE the spacers don't scale down to the minWidth. How can I fix that?
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
// Top Space
Spacer()
.frame(minHeight: 16, idealHeight: 32, maxHeight: .infinity)
.fixedSize()
// VStack 1
VStack(spacing: 0) {
// Image placeholder
Rectangle()
.fill(Color.red)
.frame(height: 175)
Spacer()
.frame(minHeight: 15, idealHeight: 62, maxHeight: .infinity)
.fixedSize()
Text("Abc")
.frame(height: 100)
}
.background(Color.gray)
// Middle Space
Spacer()
.frame(minHeight: 22, idealHeight: 100, maxHeight: .infinity)
.fixedSize()
// VStack 2
VStack(spacing: 0) {
// Image placeholder
Rectangle()
.fill(Color.red)
.frame(height: 100)
Spacer()
.frame(minHeight: 15, idealHeight: 35, maxHeight: .infinity)
.fixedSize()
// Image placeholder
Rectangle()
.fill(Color.red)
.frame(height: 195)
}
.background(Color.gray)
// Bottom Space
Spacer()
.frame(minHeight: 16, idealHeight: 45, maxHeight: .infinity)
.fixedSize()
}
.edgesIgnoringSafeArea(.all)
}
}
I'd suggest to wrap everything in another VStack and use it's spacing.
You can read out the UIScreen bounds in the init of the view and compare it to the bounds of all devices.
struct SpacerTest: View {
var spacing: CGFloat
init() {
let screenHeight = UIScreen.main.bounds.size.height
if screenHeight < 500 {
self.spacing = 20
} else if screenHeight < 600 {
self.spacing = 50
} else {
self.spacing = 100
}
}
var body: some View {
VStack(spacing: spacing) {
Text("Spacing Test")
Text("Spacing Test")
}
}
}

Make a View the same size as another View which has a dynamic size in SwiftUI

I want to achieve this (For the screenshots I used constant heights, just to visualize what I am looking for):
The two Views containing text should together have the same height as the (blue) Image View. The right and left side should also be of the same width. The important part: The Image View can have different aspect ratios, so I can't set a fixed height for the parent View. I tried to use a GeometryReader, but without success, because when I use geometry.size.height as height, it takes up the whole screen height.
This is my code:
import SwiftUI
struct TestScreen: View {
var body: some View {
VStack {
GeometryReader { geometry in
HStack {
Image("blue-test-image")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
VStack {
Text("Some Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
Text("Other Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
.frame(maxWidth: .infinity, maxHeight: geometry.size.height /* should instead be the height of the Image View */)
}
}
Spacer()
}
.padding()
}
}
This is a screenshot of what my code leads to:
Any help would be appreciated, thanks!
As #Asperi pointed out, this solves the problem: stackoverflow.com/a/62451599/12299030
This is how I solved it in this case:
import SwiftUI
struct TestScreen: View {
#State private var imageHeight = CGFloat.zero
var body: some View {
VStack {
HStack {
Image("blue-test-image")
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity)
.background(GeometryReader {
Color.clear.preference(
key: ViewHeightKeyTestScreen.self,
value: $0.frame(in: .local).size.height
)
})
VStack {
Text("Some Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
Text("Other Text")
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray)
}
.frame(maxWidth: .infinity, minHeight: self.imageHeight, maxHeight: self.imageHeight)
}
.onPreferenceChange(ViewHeightKeyTestScreen.self) {
self.imageHeight = $0
}
Spacer()
}
.padding()
}
}
struct ViewHeightKeyTestScreen: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value += nextValue()
}
}
struct TestScreen_Previews: PreviewProvider {
static var previews: some View {
TestScreen()
}
}

Two buttons in swift UI on watchOS

I have a view which I would like to use as a List item on WatchOS.
The view has a ZStack which has one VStacks in it and HStack on top of it. The top HStack has two buttons in it, each taking half of the list item. The idea is to have two transparent buttons on top of the text in the list item with two different actions based on which one is tapped.
When I tap one of the buttons in the list. Both of the action callbacks get triggered. I found this post where the Button style had to be changed to BorderlessButtonStyle to have the callback work but this is not available on watchOS.
Another solution was to use .onTapGesture {} on the button which works for me when the button has some color(check image below for reference).
But when I set the color of the buttons to clear and it looks as I want it, the .onTapGesture {} is not called anymore and only the action callback fires.
Is it even possible to make this work with SwiftUI and using List?
Here is my view code.
var body: some View {
ZStack(content: {
VStack(alignment: .leading, spacing: nil, content: {
Text(goal.title).foregroundColor(.red).padding(.leading, 8)
.padding(.trailing, 8)
.padding(.top, 4)
HStack(alignment: .center,content: {
Text("0").padding(.top,8).padding(.trailing, 16).padding(.bottom, 4).font(.title2)
}).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0,maxHeight: 35,alignment: .trailing)
}).listRowPlatterColor(Color.blue).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, alignment: .leading).background(Color(UIColor.goalBackroundColor)).cornerRadius(10)
HStack(alignment: .center, spacing: nil, content: {
Button(action: {
//print("\(goal.id) -1")
print("huehue")
}){
Text("").frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
}.background(Color.clear).onTapGesture {
print("\(goal.id) -1")
}
Button(action: {print("hue")}){
Text("").frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
}.background(Color.clear).onTapGesture {
print("\(goal.id) +1")
}
}).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0,maxHeight: .infinity,alignment: .center)
})
}

Vertically centering text in Swift UI

I want the "$0.00" to be in the middle of the screen but I can't figure out how to do it.
This is my code:
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .center) {
Text(String(format: "$%.2f", (dolaresVM.dolares.last?.v)!))
.font(.largeTitle)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}.navigationBarTitle("Test")
.onAppear(perform: self.dolaresVM.fetchDolares)
}
}
What am I doing wrong?
ScrollView has infinite inner space for its children. The VStack can't take all of this space. So VStack's height is defined by its content (in our case - Text).
Without ScrollView it will work like you want:
var body: some View {
NavigationView {
VStack(alignment: .center) {
Text(String(format: "$%.2f", 0)).font(.largeTitle)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("Test")
}
}
Providing idealHeight for the VStack can be helpful as well. You can use GeometryReader to get the 'outer' height of the ScrollView:
NavigationView {
GeometryReader { geometry in
ScrollView {
VStack(alignment: .center) {
Text(String(format: "$%.2f", 0)).font(.largeTitle)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: geometry.size.height, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
}.navigationBarTitle("Test")
}
}
As we discussed above, you don't need ScrollView so can write .navigationBarTitle("Test") inside NavigationView. So that NavigationBarTitle and Text("$0.00") both will be display on your screen.
Here i put static value of Text, you can replace it with dynamic value which you are setting up from your Model.
struct ContentView: View {
var body: some View {
NavigationView {
VStack(alignment: .center) {
Text("$0.00")
.font(.largeTitle)
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.edgesIgnoringSafeArea(.all)
.navigationBarTitle("Test")
}
}
}

How to make width of view equal to superview in SwiftUI

I have Button
struct ContentView : View {
var body: some View {
HStack {
Button(action: {}) {
Text("MyButton")
.color(.white)
.font(Font.system(size: 17))
}
.frame(height: 56)
.background(Color.red, cornerRadius: 0)
}
}
}
But I want to pin it to supreview's edges (trailing to superview's trailing and leading). Like this:
HStack doesn't help me, and it's expecting.
Fixed frame or width equals UIScree.size are not flexible solutions.
You need to use .frame(minWidth: 0, maxWidth: .infinity) modifier
Add the next code
Button(action: tap) {
Text("Button")
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.red)
}
.padding(.horizontal, 20)
Padding modifiers will allow you to have some space from the edge.
Keep in mind that the order of modifiers is essential. Because modifiers are functions that are wrapping the view below (they do not change properties of views)
You can use GeometryReader: https://developer.apple.com/documentation/swiftui/geometryreader
According to Apple:
This view returns a flexible preferred size to its parent layout.
It is a flexible solution, as it changes according to the parent layout changes.
Here's a pure SwiftUI solution where you want to fill the width of the parent but constrain the height to an arbitrary aspect ratio (say you want square images):
Image("myImage")
.resizable()
.scaledToFill()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(16.0 / 9.0, contentMode: .fit)
Just replace 16.0 / 9.0 with whatever aspect ratio you want.
I spent about a day trying to figure this out because I didn't want to use GeometryReader or UIScreen.main.bounds (which isn't cross-platform)
Edit: Found an even simpler way.
Simple adding following to your code will make the button extend edge to edge. (You can add padding as well if you want)
.frame(minWidth: 0, maxWidth: .infinity)
The entire Button code will look like this:
struct ContentView : View {
var body: some View {
HStack {
Button(action: {}) {
Text("MyButton")
.color(.white)
.font(Font.system(size: 17))
}
.frame(minWidth: 0, maxWidth: .infinity)
.padding(.top, 8)
.padding(.bottom, 8)
.background(Color.red, cornerRadius: 0)
}
}
}
I found one solution:
var body: some View {
Button(action: {}) {
HStack {
Spacer()
Text("MyButton")
.color(.white)
.font(Font.system(size: 17))
Spacer()
}
.frame(height: 56)
.background(Color.red, cornerRadius: 0)
}.padding(20)
}
But I'm not sure that it is the best. May be someone will find more elegant solution/