This piece of code is kind of stress test for a ScrollView. While a ForEach loop with 3000 items is rendering acceptably fast, 30000 takes long, and 300000 forever (I stopped after 3 minutes).
However, there are situations where you have a lot of content to scroll, imagine a timeline with zoomable scales (decade / year / month / day). In this case, there may be days across 50 years to display, and the user must have the chance to zoom in and out in order to change the scale.
The question for me is therefore: what strategies are possible with SwiftUI in order to optimize caching, prefetching etc., or to find out which part is to be displayed next after the user scrolled, and to prevent the model from calculating widths?
To be more precise: The model knows all data to display. The ScrollView must be provided with a content view that has the width of all items to display. This width is my problem: how can it be determined in a way that makes sense, and without having all items to be available?
struct TestScrollView: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: true, content: {
HStack(alignment: .bottom, spacing: 1) {
// 3000 is working, 30000 not well, 300000 takes forever:
ForEach(1 ..< 30000) { index in
Rectangle()
.fill(Color.yellow)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 300, minHeight: 500, idealHeight: 500, maxHeight: 500, alignment: .bottom)
}
}
})
}
}
``
There was a trick old times for having horizontal UITableView that is using now for reversal filling (bottom to top) that you can use it here:
Use a vertical list
Rotate it 90˚
Rotate all items of it -90˚
now you have horizontal List
struct ContentView: View {
var body: some View {
List {
ForEach(1 ..< 30000) { index in
Text("\(index)")
.rotationEffect(.init(degrees: -90))
}
}
.rotationEffect(.init(degrees: 90))
.position(x: 10, y: 200) // Set to correct offset
}
}
Why are you using a Scrollview instead of a List? A List would reuse the cells which is much better for performance and memory management. With the list when a cell scrolls out of sight, it gets removed from the view which helps greatly with performance.
For more information about the difference in performance please see here
var body: some View
{
List
{
ForEach(1..< 30000) {
index in
Rectangle()
.fill(Color.yellow)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 300, minHeight: 500, idealHeight: 500, maxHeight: 500, alignment: .bottom)
}
}
}
Edit: Since I miss-read the original question because the scrollview is horizontal.
An option would be to wrap an UICollectionView into UIViewRepresentable see here for the dev talk and here for an example implementation this would let you display it as a horizontal view while getting the performance benefits of a list
Related
I'm trying to get views to take up as much room/space as possible based on the minimum requirements of other views in the layout, but I can't seem to get the exact result I want.
Consider this code...
struct MainView: View {
var body: some View {
VStack {
Color.red
HStack {
Color.green
Color.blue
.frame(maxWidth: 100)
}
.frame(maxHeight: 100)
}
.padding()
}
}
This produces the following output. However, this is only because HStack and Blue have their maxHeight and maxWidth values set respectively.
What I'm trying to do is have the opposite... I want the minimum size of blue to dictate the size of the others.
In this example, I want green to push blue as far to the right as it can until blue says 'I'm as small as I can be!' and then have green fill the rest horizontally.
Likewise, I want red to push the HStack down as far as it can go until the HStack says 'I can't get any shorter (also because of blue) and have red fill in the rest of the vertical space.
Now from the documentation, I thought it stated if you specify red.frame(maxHeight: .infinity) and green.frame(maxWidth: .infinity), it should work, but they seem to have no effect at all, let alone giving me the desired result.
struct MainView: View {
var body: some View {
VStack {
Color.red
.frame(maxHeight: .infinity)
HStack {
Color.green
.frame(maxWidth: .infinity)
Color.blue
.frame(minWidth: 100, minHeight: 100)
}
}
.padding()
}
}
That code produces this...
So what am I missing? How can I let the minimum size of blue dictate the rest of the layout?
Use the layoutPriority modifier with a value larger than zero to tell SwiftUI you want Color.red to get as much space as possible. From the documentation:
Views typically have a default priority of 0 which causes space to be apportioned evenly to all sibling views. Raising a view’s layout priority encourages the higher priority view to shrink later when the group is shrunk and stretch sooner when the group is stretched.
Here's your second attempt, with layoutPriority(1) attached to Color.red:
struct MainView: View {
var body: some View {
VStack {
Color.red
.layoutPriority(1)
HStack {
Color.green
Color.blue
.frame(minWidth: 100, minHeight: 100)
}
}
.padding()
}
}
Output:
I'd like to display a number of values as a continuous value from 0 to 1. I'd like them to grow from the bottom up, from 0 displaying no value, to 1 displaying a full height.
However, I'm unable to make it "grow from the bottom". I'm not sure what a better term for this is - it's a pretty simple vertical gauge, like a gas gauge in a car. I'm able to make it grow from the middle, but can't seem to find a way to make it grow from the bottom. I've played with mask and clipShape and overlay - but it must be possible to do this with just a simple View, and calculations on its height. I'd specifically like to able to show overlapping gauges, as the view below demonstrates.
My ContentView.swift is as follows:
import SwiftUI
struct ContentView: View {
// binding values with some defaults to show blue over red
#State var redPct: CGFloat = 0.75
#State var bluePct: CGFloat = 0.25
let DISP_PCT = 0.8 // quick hack - the top "gauge" takes this much so the sliders display below
var body: some View {
GeometryReader { geom in
VStack {
ZStack {
// neutral background
Rectangle()
.fill(Color.gray)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT)
// the first gauge value display
Rectangle()
.fill(Color.red)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * redPct)
// the second gauge value, on top of the first
Rectangle()
.fill(Color.blue)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * bluePct)
}
HStack {
Slider(value: self.$redPct, in: 0...1)
Text("Red: \(self.redPct, specifier: "%.2f")")
}
HStack {
Slider(value: self.$bluePct, in: 0...1)
Text("Red: \(self.bluePct, specifier: "%.2f")")
}
}
}
}
}
As you play with the sliders, the red/blue views grows "out" from the middle. I would like them to grow "up" from the bottom of its containing view.
I feel like this is poorly worded - if any clarification is needed, please don't hesitate to ask!
Any help would be greatly appreciated!
You can't have them all in the same stacks. The easiest way to do this is to have your gray rectangle be your case view, and then overlay the others on top in VStacks with Spacers like this:
// neutral background
Rectangle()
.fill(Color.gray)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT)
.overlay (
ZStack {
VStack {
Spacer()
// the first gauge value display
Rectangle()
.fill(Color.red)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * redPct)
}
VStack {
Spacer()
// the second gauge value, on top of the first
Rectangle()
.fill(Color.blue)
.frame(width: geom.size.width, height: geom.size.height * DISP_PCT * bluePct)
}
}
)
The overlay contains them, and the spacers push your rectangles down to the bottom of the stacks.
I'm still somewhat new to SwiftUI and I'm getting a weird case that I don't fully understand. Basically, I have a VStack that contains some Text Views but also has a background View. Ideally, I'd like the background to grow in width as much as it needs to up to a point. I figure that is what the minWidth and maxWidth are for in .frame()
I started with this and it seems to be working:
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message")
}
.background(
Rectangle()
.foregroundColor(Color.blue)
.frame(minWidth: 0,
maxWidth: 270)
)
}
}
So far so good, but when I make the text big enough that it would need to wrap, this is what I get.
So it seems that by putting the frame around the background only makes the min/max affect that background View.
If I then try to put the frame around the VStack, I get this:
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message")
}
.frame(minWidth: 0,
maxWidth: 270)
.background(
Rectangle()
.foregroundColor(Color.blue)
)
}
}
Even though I don't think I have something pushing it out, it still pushes out the the full maxWidth.
I've also tried moving the frame to the Text but that gives the same result.
What is the correct way to get a VStack with background to only grow with its contents up to a maxWidth?
Thank you!
Well I'm dumb, literally right after posting I remembered something about how the order of the modifiers on a View matter.
I put the frame after the background and it worked.
struct TestView: View {
var body: some View {
VStack {
Text("Title")
Text("Message abcdefghijklmnopqrstuvwxyz")
}
.background(
Rectangle()
.foregroundColor(Color.blue)
)
.frame(minWidth: 0,
maxWidth: 270)
}
}
I'll leave my question here just incase it somehow helps someone else someday.
This question already has an answer here:
SwiftUI: How to make entire shape recognize gestures when stroked?
(1 answer)
Closed 2 years ago.
I am using this down code for reading tap of user on Circle, the problem is here that it works on all part of frame 100X100 but I expect that it should work only on color filled Circle, how can I solve this issue?
struct ContentView: View {
var body: some View {
Circle()
.fill(Color.red)
.frame(width: 100, height: 100, alignment: .center)
.onTapGesture {
print("tap")
}
}
}
A nice little hack would be to add a background layer to the circle (it can't be 100% clear or it wouldn't render, so I made it opacity 0.0001 which looks clear) and then add another tapGesture onto that layer. The new gesture will take priority in the background area and we can just leave it without an action so nothing happens.
Circle()
.fill(Color.red)
.frame(width: 100, height: 100, alignment: .center)
.background(
Color.black.opacity(0.0001).onTapGesture { }
)
.onTapGesture {
print("tap")
}
I'm trying to figure out the basics of ScrollViews in SwiftUI.
I figured if I created a Text with a frame of the width of the screen and .infinite height, which I understood to mean "as large as the available space, e.g. safe area", and dropped it into a ScrollView with another Text companion, I'd get a screen-sized Text that could scroll horizontally to the companion Text.
struct ContentView: View {
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
GeometryReader { geometry in
VStack {
Text("crash me")
}
.frame(width: geometry.size.width,
height: .infinity,
alignment: .topLeading)
}
Text("crash me")
}
}
}
If I run this, it just crashes. What's so stupid about it?
Instead of:
.frame(height: .infinity)
You should use:
.frame(maxHeight: .infinity)
Setting the exact height to .infinity is not possible, but maxHeight means that the view will stretch its height to the maximum value possible. This would be the size of the screen, or any other limiting factor set by the parent.