I'm trying to implement a view that displays error message for my whole app.
I want this view to always be above every other view, but I also use sheets in my app and in that case the error message is hidden behind the sheet, since the sheet is displayed above every other view.
Here is a View to reproduce my situation:
struct AppView: View {
#State var isPresentingSheet = false
var body: some View {
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.sheet(isPresented: $isPresentingSheet) {
Text("Im above everything else")
}
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.2))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
}
}
I want to know if it's possible to display a view above a sheet, but to me it looks like the sheet is in a completely different window?
But maybe it's possible to create a custom sheet that moves in from the top and is displayed above other native sheets?
If anybody is interested, I have created a custom bottom sheet with simple controls and snap functionality.
The bottom sheet has a zIndex of 1 so you can easily place views above it with a greater zIndex.
You can find it here: AlternativeSheet.
Here is the code to achieve the view of the image above.
import AlternativeSheet
...
ZStack {
VStack {
Button("Toggle sheet") {
isPresentingSheet.toggle()
}
}
.alternativeSheet(isPresented: $isPresentingSheet, snaps: [0.95]) {
Text("Im above everything else")
}
.isDraggable()
.dampenDrag()
VStack {
HStack {
Image(systemName: "xclose")
Text("I want to be even above the sheet")
}
.foregroundColor(Color.red)
.frame(minWidth: 0, maxWidth: .infinity)
.padding(Padding.l)
.background(Color.red.opacity(0.3))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.red),
alignment: .bottom
)
Spacer()
}
}
Related
Thanks to a previous question I asked, I am using Color.black.overlay and .clipped() to show an image with letterbox borders above and below it.
But when I attempt to put a button on the top border, it can't be tapped on (I assume because the image (unclipped) is in that space, and is intercepting the tap gesture).
Here is what the layout looks like:
Here is the code:
var body: some View {
ZStack {
Color.black
VStack {
topBorder
imageMiddle
bottomBorder
}
}
.ignoresSafeArea()
}
var topBorder: some View {
return Group {
ZStack {
Rectangle()
.fill(.green)
.frame(minHeight: borderHeight, maxHeight: borderHeight)
Button {
print("tap")
} label: {
Image(systemName: "hand.tap.fill")
.foregroundColor(.black)
}
}
}
}
var bottomBorder: some View {
return Group {
Rectangle()
.fill(.green)
.frame(minHeight: borderHeight, maxHeight: borderHeight)
}
}
var imageMiddle: some View {
return Group {
Color.black.overlay(
Image("cat")
.scaledToFill()
)
.clipped()
}
}
How can I expose that button to a user's tap?
Adding .allowsHitTesting(false) to your image view will fix it. However, it seems like the wrong approach.
VStack {
topBorder
imageMiddle
.allowsHitTesting(false) // <- This will fix your problem.
bottomBorder
}
I would recommend using another approach to add your borders on top of the image instead. Something like this:
ZStack {
imageMiddle
VStack {
topBorder
.overlay(alignment: .bottom) {
Rectangle().frame(minHeight: 0, maxHeight: 10)
}
Spacer()
bottomBorder
.overlay(alignment: .top) {
Rectangle().frame(minHeight: 0, maxHeight: 10)
}
}
}
.ignoresSafeArea()
I have a view where the basic structure is as follows (this is just a bare representation of my actual view obviously):
var body: some View {
NavigationView {
VStack {
// Logo and postcode search
VStack {
Text("Logo")
.background(Color.red)
Text("Title")
.background(Color.blue)
}
.fixedSize(horizontal: false, vertical: true)
.padding()
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
Text("Selector")
.background(Color.yellow)
Text("Browse")
.frame(maxHeight: .infinity)
.background(Color.green)
}
.navigationBarHidden(true)
}
}
.background(Color.red)
}
}
What I am trying to do is have the "Browse" section fill all the available space from its starting position to the bottom of the screen. However, here is how it currently looks:
If I set a concrete height to the 'browse' text (e.g. frame(height: 400)) it increases the height accordingly. However, given that the view is within the scroll view which we can see in turn is the full height of the screen, I thought that setting the maxHeight property to .infinity would have the desired effect, but clearly not. What am I doing wrong here?
We need to calculate that manually, because ScrollView requires finite intrinsic content size.
Here is a possible approach. Tested with Xcode 13.4 / iOS 15.5
GeometryReader { gp in
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading) {
Text("Selector")
.background(Color.yellow)
Text("Browse")
.frame(minHeight: height, alignment: .top)
.background(GeometryReader {
Color.clear.preference(key: ViewOffsetKey.self,
value: $0.frame(in: .named("parent")).origin.y)
})
.background(Color.green)
}
.navigationBarHidden(true)
}
.coordinateSpace(name: "parent")
.onPreferenceChange(ViewOffsetKey.self) {
height = gp.size.height - $0
}
.frame(maxWidth: .infinity)
}
Complete code in project
Edit: Substitute your "system name:" of choice. "pencil.circle" works fine. "edit" is not a valid SF Symbol.
(I've simplified my code so you can cut and paste. That's why you see .frame, resizable, etc. where much simpler code might your first instinct.)
I have created a view which is a vertical list of row items (table view).
Each row item has a horizontal view with two images inside it.
The images take up too much space and do not fit correctly on the screen:
import SwiftUI
#main
struct StackOverflowDemoApp: App {
var body: some Scene {
WindowGroup {
TandemView()
}
}
}
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(height: 80)
.aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(width: 200, height: 80)
}
.padding()
.fixedSize()
}
}
struct TandemView_Previews: PreviewProvider {
static var previews: some View {
TandemView()
}
}
The above is the closest I can get to the desired layout (it just needs to fit horizontally). I experimented with GeometryReader but that did not produce desired results.
Here are some things I tried:
The code as provided
NoConstraintsOnPencilOrHStack
NoConstraintsOnTandemView
NoConstraintsOnImageInPaddedViewButWithFrameConstraint
I am trying to get a row view which consists of two Images (my actual source consists of UIImage objects) that fits within the width of the screen.
Edit:
After Accepting cedricbahirwe's spot-on response, I was able to simplify the code further. New results:
I added at the top level
TandemView()
.padding(.horizontal)
I removed:
// Spacer()
at the end of PaddedImageView
updated TandemView -- changed both frames and removed 3 lines:
struct TandemView: View {
var body: some View {
HStack {
Spacer()
Image(systemName: "pencil")
.resizable()
.background(Color.orange)
.frame(width: 80, height: 80)
// .aspectRatio(1, contentMode: .fill)
PaddedImageView()
.frame(height: 80)
}
// .padding()
// .fixedSize()
}
}
This is happening because of the layout of PaddedImageView View, you can actually remove the Spacer since it is not needed there.
So change
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
Spacer()
}
}
to
struct PaddedImageView: View {
let color: Color = .red
var body: some View {
ZStack {
color
Image(systemName: "edit")
.resizable()
.padding()
}
}
}
Note:
SwiftUI Engine infers the layout of your view from the implementation of the body property. It's recommended to have one Parent View inside the body property.
I have content in my ScrollView that is sometimes short and other times longer than the screen. I'm trying to stretch the content to the bottom if it is too short, but no matter what I do the content stays short:
This is what the code looks like:
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Header")
.padding()
VStack {
Text("Content")
Image(systemName: "bell")
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
}
}
.background(Color.yellow.ignoresSafeArea())
}
}
I would've expected the Spacer() in my VStack to stretch it to the end. Or the maxHeight: .infinity to help me out as well, but the content always stays short even tho the ScrollView is the screen height (because of the yellow background on it). This becomes more of a problem on iPad when the one screen shows the big gap. How can this be solved for various screen height?
Using #CenkBilgen's idea of using an overlay + GeometryReader, you could do the following:
struct ContentView: View {
var body: some View {
Color.clear
.overlay(GeometryReader { geo in
ScrollView {
VStack {
ForEach(1..<10) {
Text("\($0)").padding()
}
}
HStack { Spacer() }
}
.clipped()
.frame(width: geo.size.width, height: geo.size.height)
})
}
}
This is an example of what I am trying to do.Link to image of design to implement. I am unsure how to position a button and text at the top of the screen in this manner coding in swiftui. Alternatively I thought I could use the navigation bar inline and customise that but I am unsure.
var body: some View {
VStack (alignment: .trailing) {
HStack(spacing:10) {
Button(action: {
}) {
Image(systemName: "line.horizontal.3")
.font(.headline)
}
Text("WeCollab")
.foregroundColor(.white)
.font(.title)
//padding(.leading,40)
Spacer()
}
.padding(.top,UIApplication.shared.windows.first?.safeAreaInsets.top)
.background(customPurpleColour)
Spacer()
}
.edgesIgnoringSafeArea(.top)
}
}
Seems like you're on the right track. In order to keep the title centered, it seemed easier to make a ZStack. The menu button gets it's own .leading-aligned VStack and then the title goes on top of that.
The edgesIgnoringSafeArea and padding can be simplified so that you don't have to use the screen size safe areas.
struct ContentView: View {
var body: some View {
VStack {
ZStack {
VStack {
Button(action: { }) {
Image(systemName: "line.horizontal.3")
.font(.headline)
}.padding(.leading)
}.frame(maxWidth: .infinity, alignment: .leading)
Text("WeCollab")
.foregroundColor(.white)
.font(.title)
}
.background(Color.purple.edgesIgnoringSafeArea(.top))
Spacer() //other content goes here
}
}
}