How to solve this top bar problem on SwiftUI, elegantly? - swiftui

I am trying to use the whole iPhone area for my app.
I have this HStack at the top, used to create a custom toolbar.
var body: some View {
VStack (spacing:0) {
MyTopbar()
// other controls
Spacer()
}
.edgesIgnoringSafeArea(.top)
This appears like this on new devices with a notch and old devices without a notch. The notch cuts my menu.
I can solve that by adding a spacer with a frame height before MyTopbar() on the vertical stack but first of all this seems to be a very awful solution. First I have to guess a height for that spacer. Then I have to detect if the device has a notch or not (?).
Is there a better way?

You can think of it as layers (content that respects safe area and content that doesn't).
Something like this perhaps:
struct ContentView: View {
var body: some View {
ZStack {
Color.blue.ignoresSafeArea() // Whatever view fills the whole screen
VStack (spacing:0) {
MyTopbar()
// other controls
Spacer()
}
}
}
}

A possible solution to add clear color with safe area height. No need for much calculation.
var body: some View {
VStack (spacing:0) {
Color.clear.frame(height: Color.clear.frame(height: UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0)
MyTopbar()
// other controls
Spacer()
}
.edgesIgnoringSafeArea(.top)

Related

SwiftUI ScrollView horizontal scroll lag on macOS

Faced with a very strange ScrollView behavior on macOS. The content freezes under the mouse during horizontal scrolling. But it is worth taking the mouse away from the window and the content scrolls normally.
This happens when I try to use a vertical scroll inside a horizontal one:
struct ScrollTestView: View {
var body: some View {
ScrollView(.horizontal) {
ScrollView(.vertical) {
VStack {
ForEach(0..<20, id: \.self) { row in
HStack {
ForEach(0..<20, id: \.self) { item in
Text("\(item)")
.font(.title)
.padding()
.background {
Color.gray
}
}
}
}
}
}
}
}
}
Yes, I know that I can use the same ScrollView for both axes simultaneously, but I need solution with two ScrollViews because of desired UX.
This solution is perfectly works on iOS, but I have this strange behavior on macOS.
Also if you swap a horizontal and a vertical ScrollView in the exact same code, everything works just fine:
struct ScrollTestView: View {
var body: some View {
ScrollView(.vertical) {
ScrollView(.horizontal) {
// ...
}
}
}
}
Looks like this is a SwiftUI bug, but I am not sure, maybe I am missing something?
Any ideas?

SwiftUI offset items in ScrollView under another view

I have a VStack with some content at the top of my app, then a ScrollView on the bottom, with these views being seperated with a Divider. Is there any way to offset the scrollView such that it starts slightly tucked under the Divider and the top view?
Here is an example image of what I want:
The numbers are in a ScrollView and the top content is simply Color.white in this example.
If I apply a simply y offset, though, I get this:
The number is vertically shifted up, but not "tucked" under.
Is there an easy way to get the "tucked" result? I'm sure I could use a ZStack or something, but that seems like a lot of work, especially because I don't know how large the top content will be.
Example Code:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
Color.white.frame(height: 100)
Divider()
ScrollView {
ForEach(0..<20) { number in
Text("\(number)")
}
}
.offset(y: -8)
}
}
}
I assume you just need padding for scroll view, like
ScrollView {
ForEach(0..<20) { number in
Text("\(number)")
}
}
.padding(.top, -8) // << here !!
.clipped()

SwiftUI - How to size a view relative to its parent when the parent is inside a scrollview?

I'm trying to dynamically size some views which end up being placed inside of a scrollview. Here is the simplest sample code I can think of:
struct RootView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
// More views above
HStack(spacing: 16) {
MyView()
MyView()
}
.padding([.leading, .trailing], 16)
// More views below
}
}
}
}
struct MyView: View {
var body: some View {
VStack(alignment: .leading, spacing: 24) {
Image("myImage")
.resizable()
.scaledToFill()
VStack(alignment: .leading, spacing: 0) {
Text("Text")
OtherView()
}
}
}
}
EDIT: I think really the main issue I'm having is regarding how to dynamically size each MyView inside of the HStack.
If I wanted the Image in MyView to be sized to fill its width and grow vertically to maintain its aspect ratio, and then also size each MyView in RootView to be 40% of RootView's width, what is the best way to accomplish this? I've tried using GeometryReader but when it's nested inside the ScrollView, it causes the view its used in to collapse in on itself. If I use it outside of the ScrollView, I'm effectively always going to be getting the screen width (in this application) which isn't always what I need. On top of that, imagine that MyView is nested deeper in the view hierarchy and not called directly from RootView, but rather one of its child views. Or better yet, imagine that we don't know that RootView doesn't know its rendering a MyView if the view is determined at runtime.
To give a little context to anyone who is interested in some backstory, the app I'm trying to build is very modular in nature. The idea is that we really only have one "container view" struct that determines which views to render at runtime. We basically have a ScrollView in this container view and then any number of subviews. I'm really struggling with why it seems so difficult to set a view's content dimensions relative to its parent, any assistance would be hugely appreciated.
The best way I can think of is using a GeometryReader view. Here is an example.
GeometryReader { geometry in
RoundedRect(cornerRadius: 5).frame(width: geometry.size.width * 0.8, height: geometry.size.height * 0.8)
}
Typically I use the GeometryReader as a "Root" view and scale everything off of it, however you can place it inside of another view or even as an overlay to get the parent view size. For example.
VStack {
GeometryReader { geometry in
//Do something with geometry here.
}
}
Check it out here.
If I understood your goal correctly it is just needed to make images resizable (that makes them fill available space taking into account aspect ratio), like
VStack(alignment: .leading, spacing: 24) {
Image("myImage")
.resizable() // << here !!
.aspectRatio(contentMode: .fill)

LazyVGrid onTapGesture navigate to next screen swiftUI

I am quite new to swiftUI. I have created a grid view on tapping on which I want to go to next screen. But somehow I am not able to manage to push to next screen. I am doing like this:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(viewModel.pokemon) { pokemon in
PokemonCell(pokemon: pokemon, viewModel: viewModel)
.onTapGesture {
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
Text(pokemon.name)
}
}
}
}
}
.navigationTitle("Pokedex")
}
}
Upon doing like this, I am getting a warning stating
Result of 'NavigationLink<Label, Destination>' initializer is unused
Can someone please guide me, how to do this?
.onTapGesture adds an action to perform when the view recognizes a tap gesture. In your case you don't need to use .onTapGesture. If you want to go to another view when cell is tapped you need to write NavigationLink as below.
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
PokemonCell(pokemon: pokemon, viewModel: viewModel)
}
If you want to use .onTapGesture, another approach is creating #State for your tapped cell's pokemon and using NavigationLink's isActive binding. So when user tap the cell it will change the #State and toggle the isActive in .onTapGesture. You may need to add another Stack (ZStack etc.) for this.
NavigationView {
ZStack {
NavigationLink("", destination: PokemonDetailView(pokemon: pokemon), isActive: $isNavigationActive).hidden()
ScrollView {
// ...

Animated transitions and contents within ScrollView in SwiftUI

I'm not quite a SwiftUI veteran but I've shipped a couple of apps of moderate complexity. Still, I can't claim that I fully understand it and I'm hoping someone with deeper knowledge could shed some light on this issue:
I have some content that I want to toggle on and off, not unlike .sheet(), but I want more control over it. Here is some "reconstructed" code but it should be able capture the essence:
struct ContentView: View {
#State private var isShown = false
var body: some View {
GeometryReader { g in
VStack {
ZStack(alignment: .top) {
// This element "holds" the size
// while the content is hidden
Color.clear
// Content to be toggled
if self.isShown {
ScrollView {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.frame(width: g.size.width) // This is a "work-around"
} // ScrollView
.transition(.move(edge: .bottom))
.animation(.easeOut)
}
} // ZStack
// Button to show / hide the content
Button(action: {
self.isShown.toggle()
}) {
Text(self.isShown ? "Hide" : "Show")
}
} // VStack
} // GeometryReader
}
}
What it does is, it toggles on and off some content block (represented here by a Rectangle within a ScrollView). When that happens, the content view in transitioned by moving in from the bottom with some animation. The opposite happens when the button is tapped again.
This particular piece of code works as intended but only because of this line:
.frame(width: g.size.width) // This is a "work-around"
Which, in turn, requires an extra GeometryReader, otherwise, the width of the content is animated, producing an unwanted effect (another "fix" I've discovered is using the .fixedSize() modifier but, to produce reasonable effects, it requires content that assumes its own width like Text)
My question to the wise is: is it possible to nicely transition in content encapsulated within a ScrollView without using such "fixes"? Alternatively, is there a more elegant fix for that?
A quick addition to the question following #Asperi's answer: contents should remain animatable.
You are my only hope,
–Baglan
Here is a solution (updated body w/o GeometryReader). Tested with Xcode 11.4 / iOS 13.4
var body: some View {
VStack {
ZStack(alignment: .top) {
// This element "holds" the size
// while the content is hidden
Color.clear
// Content to be toggled
if self.isShown {
ScrollView {
Rectangle()
.aspectRatio(1, contentMode: .fit)
.animation(nil) // << here !!
} // ScrollView
.transition(.move(edge: .bottom))
.animation(.easeOut)
}
} // ZStack
// Button to show / hide the content
Button(action: {
self.isShown.toggle()
}) {
Text(self.isShown ? "Hide" : "Show")
}
} // VStack
}