I am trying to use a scroll view for scrollable content, but whenever I nest my views inside the Scroll View, I have noticed that the views from my stacks vanish back into the view hierarchy and nothing remains visible on the screen. I have also seen that whenever I am using a ScrollView, it adds another Hosting View Controller and I don't know if this is the normal behaviour.
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(bookLibrary.indices, id: \.self) { index in
HStack {
ForEach(self.bookLibrary[index], id: \.self) { book in
BookView(book: book)
}
}
}
}
}
}
}
Getting this view hierarchy. You can also see that the HostingScrollView has a width of 0.
If you don't want to use GeometryReader just insert zero height view with correct width like this
var body: some View {
ScrollView {
VStack {
Color.clear
.frame(maxWidth: .infinity, maxHeight: 0)
ForEach(...) { each in
...
}
}
}
}
While not a perfect solution, you can use GeometryReader to set the scroll view's frame to the same width as its superview.
NavigationView {
GeometryReader {geometry in
ScrollView(.vertical) {
// TODO: Add content
}
.frame(width: geometry.size.width)
}
}
This workaround was inspired by Rob Mayoff's answer on another question.
Related
I have a view that is a TabView with the style PageTabViewStyle(indexDisplayMode: .always which looks great for my use case however the page indicator is bumping right up to the safe area near the bottom of the screen and it looks bad. I'd like to move the page indicator up to some n value. here's a sample of my view to reproducing it. If this view were built on any device without a Home Button, it will ride on top of the home indicator line.
var body: some View {
ZStack {
TabView(selection: $homeVM.selectedPageIndex) {
// Any number of views here.
}
.frame(width: UIScreen.main.bounds.width)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .never))
}.edgesIgnoringSafeArea(.all)
}
I attempted to add the padding to the ZStack, which does work but then my TabView is cut off on the bottom, which means my cells disappear prematurely.
Here's an image for what I'm trying to fix. Notice the page indicator sits on the home bar indicator. I need the indicators pushed up, without pushing up the background ScrollView
Update #1
This view is being presented by a base view that I use to handle my navigation stack. The view is as follows. The important thing to note here is the .ignoresSafeArea() that I have on this view. I did that because it's a containing view for my eventual TabView to be presented from. Interestingly if I remove this modifier the indicators move up to a more manageable position, but then my form becomes clipped at the top and bottom of the device when scrolling, and that's not ideal.
struct BaseLaunchView: View {
#StateObject var baseNavVM = BaseLaunchViewModel()
#State var shouldLogin = false
#State var shouldRegister = false
var body: some View {
VStack {
switch baseNavVM.loggedIn {
case true:
HomeView()
default:
NavigationView {
VStack{
Spacer()
VStack(spacing: 30) {
VStack {
Text("Stello")
.font(Fonts.title)
Text("Life Groups")
.font(Fonts.body)
}
StelloDivider()
Text("Connect with like minded people, to fellowship and find your home.")
.font(Fonts.subheading)
.multilineTextAlignment(.center)
}
Spacer()
NavigationLink(destination: RegisterOptionsView(isLoggingIn: true), isActive: $shouldLogin) {
Button(action: {
shouldLogin.toggle()
}, label: {
Text("Login")
.font(Fonts.button)
}).buttonStyle(StelloFillButtonStyle())
}
NavigationLink(destination: RegisterOptionsView(isLoggingIn: false), isActive: $shouldRegister) {
Button(action: {
shouldRegister.toggle()
}, label: {
Text("Register")
.font(Fonts.button)
}).buttonStyle(StelloHollowButtonStyle())
}
}
}.accentColor(.black)
}
}
.padding()
.environmentObject(baseNavVM)
.ignoresSafeArea()
}
}
I'm building an SwiftUI app with a dropdown menu with a vertical ScrollView within another vertical ScrollView. However, the dropdown menu one (the nested one) won't scroll. I would like to give it priority somehow. It seems like a simple problem, but I have scoured the internet but cannot find an adequate solution. Here is the basic code for the problem (the code is cleaner in the app but copy and pasting particular snippets did not work very well):
ScrollView{
VStack{
(other stuff)
DropdownSelector()
(other stuff)
}
}
struct DropdownSelector(){
ScrollView{
VStack(alignment: .leading, spacing: 0) {
ForEach(self.options, id: \.self) { option in
(do things with the option)
}
}
}
Creating nested ScrollViews in the first place is probably a bad idea. Nonetheless, there is a solution.
Because with ScrollView it scrolls as much as the content height, this is a problem when they are nested. This is because the inner ScrollView isn't limited in height (because the outer ScrollView height just changes), so it acts as if it wasn't there at all.
Here is a minimal example demonstrating the problem, just for comparison:
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Top view")
DropdownSelector()
Text("Bottom view")
}
}
}
}
struct DropdownSelector: View {
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(0 ..< 10) { i in
Text("Item: \(i)")
}
}
}
}
}
To fix it, limit the height of the inner scroll view. Add this after DropdownSelector():
.frame(height: 100)
I'm trying to build a UI (SwiftUI / iOS) out of a number of custom views.
All those custom views have a defined aspect ratio or ratio for their frame.
Here's a simplified version of such a custom view:
struct TestView: View {
var body: some View {
GeometryReader { geometry in
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(height: geometry.size.width / 3)
}
}
}
My ContentView currently looks like that:
struct TestContentView: View {
var body: some View {
GeometryReader {geomerty in
VStack {
TestView()
TestView()
}
}
}
}
I would like to have the two rectangles to be positioned right below each other (at the top of the screen). So without any space between them. So a bit like an old-fashioned UITableView with only to rows.
But whatever I try, I only get one of two results:
They are equally spread out over the screen (vertically)
They overlap (= the view on the top only gets a vertical size of 20
The only solution I've found so far is to define the frame size of the sub-views also in the TestContentView(). But that seems to be quite un-SwiftUI.
Thanks!
Remove the GeometryReader from your content view, since it isn't doing anything
You said that your TestView has a defined aspect ratio, but, in fact, it doesn't -- it just has a defined width. If you do define an aspect ratio, it starts working as expected:
struct TestView: View {
var body: some View {
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.aspectRatio(3, contentMode: .fit)
}
}
struct TestContentView: View {
var body: some View {
VStack(spacing: 0) {
TestView()
TestView()
Spacer()
}
}
}
I've got a simple view:
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(0..<2) { _ in
CardVew(for: cardData)
}
}
}
.navigationBarTitle("Testing", displayMode: .automatic)
}
}
But you can replace the CardView with anything - the glitch presists. Glitch video
Is there a way to fix it?
Xcode 12.0.1, Swift 5
Setting the top padding to 1 is breaking at least 2 major things:
The scroll view does not extend under NavigationView and TabView - this making it loose the beautiful blur effect of the content that scrolls under the bars.
Setting background on the scroll view will cause Large Title NavigationView to stop collapsing.
I've encountered these issues when i had to change the background color on all screens of the app i was working on.
So i did a little bit more digging and experimenting and managed to figure out a pretty nice solution to the problem.
Here is the raw solution:
We wrap the ScrollView into 2 geometry readers.
The top one is respecting the safe area - we need this one in order to read the safe area insets
The second is going full screen.
We put the scroll view into the second geometry reader - making it size to full screen.
Then we add the content using VStack, by applying safe area paddings.
At the end - we have scroll view that does not flicker and accepts background without breaking the large title of the navigation bar.
struct ContentView: View {
var body: some View {
NavigationView {
GeometryReader { geometryWithSafeArea in
GeometryReader { geometry in
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)
ForEach(0..<5) { i in
Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)
Spacer()
}
Color.red.frame(width: 100, height: 100, alignment: .center)
}
.padding(.top, geometryWithSafeArea.safeAreaInsets.top)
.padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
.padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
.padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
}
.background(Color.yellow)
}
.edgesIgnoringSafeArea(.all)
}
.navigationBarTitle(Text("Example"))
}
}
}
The elegant solution
Since the solution is clear now - lets create an elegant solution that can be reused and applied to any existing ScrollView by just replacing the padding fix.
We create an extension of ScrollView that declares the fixFlickering function.
The logic is basically we wrap the receiver into the geometry readers and wrap its content into the VStack with the safe area paddings - that's it.
The ScrollView is used, because the compiler incorrectly infers the Content of the nested scroll view as should being the same as the receiver. Declaring AnyView explicitly will make it accept the wrapped content.
There are 2 overloads:
the first one does not accept any arguments and you can just call it on any of your existing scroll views, eg. you can replace the .padding(.top, 1) with .fixFlickering() - thats it.
the second one accept a configurator closure, which is used to give you the chance to setup the nested scroll view. Thats needed because we don't use the receiver and just wrap it, but we create a new instance of ScrollView and use only the receiver's configuration and content. In this closure you can modify the provided ScrollView in any way you would like, eg. setting a background color.
extension ScrollView {
public func fixFlickering() -> some View {
return self.fixFlickering { (scrollView) in
return scrollView
}
}
public func fixFlickering<T: View>(#ViewBuilder configurator: #escaping (ScrollView<AnyView>) -> T) -> some View {
GeometryReader { geometryWithSafeArea in
GeometryReader { geometry in
configurator(
ScrollView<AnyView>(self.axes, showsIndicators: self.showsIndicators) {
AnyView(
VStack {
self.content
}
.padding(.top, geometryWithSafeArea.safeAreaInsets.top)
.padding(.bottom, geometryWithSafeArea.safeAreaInsets.bottom)
.padding(.leading, geometryWithSafeArea.safeAreaInsets.leading)
.padding(.trailing, geometryWithSafeArea.safeAreaInsets.trailing)
)
}
)
}
.edgesIgnoringSafeArea(.all)
}
}
}
Example 1
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)
ForEach(0..<5) { i in
Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)
Spacer()
}
Color.red.frame(width: 100, height: 100, alignment: .center)
}
}
.fixFlickering { scrollView in
scrollView
.background(Color.yellow)
}
.navigationBarTitle(Text("Example"))
}
}
}
Example 2
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
Color.red.frame(width: 100, height: 100, alignment: .center)
ForEach(0..<5) { i in
Text("\(i)")
.frame(maxWidth: .infinity)
.background(Color.green)
Spacer()
}
Color.red.frame(width: 100, height: 100, alignment: .center)
}
}
.fixFlickering()
.navigationBarTitle(Text("Example"))
}
}
}
Here's a workaround. Add .padding(.top, 1) to the ScrollView:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach(0..<2) { _ in
Color.blue.frame(width: 350, height: 200)
}
}
}
.padding(.top, 1)
.navigationBarTitle("Testing", displayMode: .automatic)
}
}
}
I simplified #KoCMoHaBTa's answer. Here it is:
extension ScrollView {
private typealias PaddedContent = ModifiedContent<Content, _PaddingLayout>
func fixFlickering() -> some View {
GeometryReader { geo in
ScrollView<PaddedContent>(axes, showsIndicators: showsIndicators) {
content.padding(geo.safeAreaInsets) as! PaddedContent
}
.edgesIgnoringSafeArea(.all)
}
}
}
Use like so:
struct ContentView: View {
var body: some View {
NavigationView {
ScrollView {
/* ... */
}
.fixFlickering()
}
}
}
while facing the same problem I did a bit of investigation.
The glitch seems to come from a combination of scrollview bouncing, and the speed of deceleration of the scrolling context.
For now I have managed to make the glitch disappear by settings the deceleration rate to fast. It seem to let swiftui better compute the layout while keeping the bounce animation active.
My work around is as simple as to set the following in the init of your view. The drawback its that this affects the speed of your scrolling deceleration.
init() {
UIScrollView.appearance().decelerationRate = .fast
}
A possible improvement would be to compute the size of the content being displayed and then switch on the fly the deceleration rate depending what would be needed.
I'd love to build a scrolling screen, so I wanted to embed it ScrollView. But I am not able to achieve it the view just shrinks to its compressed size. Let's say that I want the ScrollView to scroll vertically so I'd love the content to match scrollView's width. So I use such preview:
struct ScrollSubview_Previews : PreviewProvider {
static var previews: some View {
func textfield() -> some View {
TextField(.constant("Text")).background(Color.red)
}
return Group {
textfield()
ScrollView {
textfield()
}
}
}
}
But it ends with result like this:
A simple way for you, using frame(maxWidth: .infinity)
ScrollView(.vertical) {
VStack {
ForEach(0..<100) {
Text("Item \($0)")
}
}
.frame(maxWidth: .infinity)
}
Here is a trick: introduce a HStack that only has one Spacer:
return Group {
HStack {
Spacer()
}
textfield()
ScrollView {
textfield()
}
}
Actually you don't need GeometryReader anymore. ScrollView has been refactored in Xcode beta 3. Now you can declare that you have a .horizontal or .vertical ScrollView.
This makes the ScrollView behave like it should, like any normal View protocol.
Ex:
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach((1...10).reversed()) {
AnyViewYouWant(number: $0)
}
}
}
This will result in a view with the width of its parent scrolling horizontally. The height will be defined by the ScrollView's subviews height.
It's a known issue existing also in beta 2 - check the Apple's Xcode 11 release notes here ⬇️
https://developer.apple.com/documentation/xcode_release_notes/xcode_11_beta_2_release_notes.
The workaround mentioned by Apple themselves is to set fixed frame for ScrollView inside element. In that case I suggest to use GeometryReader for fix screen width, height is automatically fit to content.
For example if you need to fit the ScrollView's content to screen width you can something like:
return GeometryReader { geometry in
Group {
self.textfield()
ScrollView {
self.textfield()
.frame(width: geometry.size.width)
}
}
}
You can also done this using extra HStack and space.
ScrollView(.vertical) {
HStack {
VStack {
ForEach(0..<100) {
Text("Item \($0)")
}
}
Spacer()
}
}
Scroll view content will expand to available size.
GeometryReader { geo in
ScrollView(.vertical) {
YourView().frame(
minWidth: geo.size.width,
minHeight: geo.size.height
)
}
}
PreviewProvider is just about your Canvas (rendering view).
if you want to place text into ScrollView you should create your View
struct MyViewWithScroll : View {
var body: some View {
ScrollView {
Text("Placeholder")
}
}
}
And after that, render your MyViewWithScroll in Canvas (window with iPhone from the right side of code editor)
#if DEBUG
struct MyViewWithScroll_Previews : PreviewProvider {
static var previews: some View {
MyViewWithScroll()
}
}
#endif