SwiftUI ScrollView and alignment - swiftui

Okay, I know SwiftUI is a shift in thinking, especially coming from a world of HTML and css. But I've spent like 4 days trying to get something to work that I feel should be pretty easy and just can't so please help!
I have an app where one screen is a table of results, dynamic data that could be one or two rows/columns but could also be hundreds. So I want to be able to scroll around in cases where the table is huge.
I've replicated my setup and reproduced my problems in a Swift playground like so
import Foundation
import UIKit
import PlaygroundSupport
import SwiftUI
struct ContentView : View {
var cellSize: CGFloat = 50
var numRows: Int = 3
var numCols: Int = 3
var body : some View {
ZStack {
ScrollView([.horizontal,.vertical]) {
HStack( spacing: 0) {
VStack ( spacing: 0) {
ForEach(0 ..< numRows, id: \.self) { row in
Text("row " + row.description)
.frame( height: self.cellSize )
}
}
ForEach(0 ..< self.numCols, id: \.self) { col in
VStack( spacing: 0) {
ForEach(0 ..< self.numRows, id: \.self) { row in
Rectangle()
.stroke(Color.blue)
.frame( width: self.cellSize, height: self.cellSize )
}
}
}
}
.frame( alignment: .topLeading)
}
}
}
}
let viewController = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = viewController
Here's what I get when the grid is 3x3
I know things like to center by default in SwiftUI, but why isn't that .frame( alignment: .topLeading) on the HStack causing the table to be aligned to the upper left corner of the screen?
Then even worse, once that table gets large, here's what I get:
Still not aligned to the top left, which would make sense as a starting point.
When I scroll left, I can't even get to the point where I can see my header column
The view bounces me away from the edges when I get close. Like I can get to the point where I can see the top edge of the table, but it bounces me back right away.
A ton of whitespace to the right, which probably correlates to me not seeing my header columns on the left.
What am I doing wrong here? I'm exhausted trying all different frame and alignment options on various Views in here.

Related

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()

How do you keep 'fullScreenISPresented' from dismissing itself when the screen rotates?

I have a LazyVGrid with a layout count: 2 when in portrait, and court: 3 when in landscape, in a scrollview. I use a ternary to change the count. Problem is when I scroll down than select a cell, when the model slides up and I rotate, the view dismisses by itself. I also notice the scroll seems to be in a totally different location.
Do I need to build this differently? Funny thing is it only happens at certain places down in the scrollview. Its not consistent. Sometimes it works fine then as I continue to scroll down it'll start to happen.
If I don't change the layout count in portrait or landscape, it works fine. It seems change the count causes this.
struct Feed_View: View {
#EnvironmentObject var viewModel : Post_View_Model
#Environment(\.verticalSizeClass) var sizeClass
#FocusState private var isFocused: Bool
var body: some View {
Color("BGColor").ignoresSafeArea()
ZStack {
VStack (alignment: .center, spacing: 0) {
//MARK: - NAVIGATION BAR
NavBar_View() // Top Navigation bar
.frame(maxHeight: 40, alignment: .center)
//MARK: - SCROLL VIEW
ScrollView (.vertical, showsIndicators: false) {
//MARK: - FEED FILL
let layout = Array(repeating: GridItem(.flexible(), spacing: 10), count: sizeClass == .compact ? 3 : 2)
LazyVGrid(columns: layout, spacing: 10) {
ForEach (viewModel.posts, id: \.self) { posts in
Feed_Cell(postModel: posts)
} //LOOP
} //LAZYV
.padding(.horizontal, 10).padding(.vertical, 10)
} //SCROLL
} //V
} //Z
}
}

LazyVGrid where every item takes only the place it needs

I just wondered how you could make a LazyVGrid where every item takes only the place it needs, not less and not more.
I know about .flexible() but the problem is: My Items are different sized, that means I don't know how many of them will fit in a row.
Do you got any ideas?
Thanks for your help!
Boothosh
EDIT:
LazyVGrid(columns: [GridItem(.flexible())]) {
Color.blue
.frame(width: 200, height: 200)
Color.blue
.frame(width: 100, height: 100)
Color.blue
.frame(width: 100, height: 100)
}
This is a example what Im talking about. I want to achieve that this items are placed with evenly space around them. (Not below each other, like they are now)
You just need to specify what you want by using variables.
Try this :
struct ContentView: View {
let data = (1...100).map { "Item \($0)" }
let columns = [
// The number of grid Items here represent the number of columns you will
//see. so you can specify how many items in a row thee are .
// 2 grid Items = 2 items in a row
GridItem(.flexible()),
GridItem(.flexible()),
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(data, id: \.self) { item in
Text(item)
}
}
.padding(.horizontal)
}
.frame(maxHeight: 300)
}
}

How to make every item the same height in a LazyVGrid?

Edit This is a regression in iOS 15 beta. The code works as expected on iOS 14.5:
I have submitted a bug to Apple.
I have a dashboard-style screen in my SwiftUI app, where I am using a LazyVGrid with a single .adaptative column to layout my dashboard widgets, where widgets are laid out in wrapping rows.
It works as I want it to.
However, if a widget happens to be taller than others, I would like other widgets in the same row to grow vertically, so they end up having the same height as the tallest of the row.
This small bit of code illustrates my problem:
struct ContentView: View {
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
VStack {
Spacer()
Text("Hello")
}
.border(.red)
Text("Lorem ipsum")
.border(.blue)
}
.border(.green)
.padding(.horizontal, 100)
}
}
The result is:
I would like the red box (VStack containing Spacer + Hello) to be as tall as the blue box (lorem ipsum).
How could I accomplish that?
Please don't suggest using an HStack, as the above example is only to illustrate my problem with LazyVGrid. I do need to use the grid because I have quite a few children to layout, and the grid works great between phone and iPad form factors (adjusting the number of columns dynamically, exactly as I want it).
It looks like Apple begins (for unknown reason) to apply fixedSize for views in grid to make layout based on known intrinsic content sizes. The Spacer, Shape, Color, etc. do not have intrinsic size so we observe... that what's observed.
A possible approach to resolve this is perform calculations by ourselves (to find dynamically max height and apply it to all cells/views).
Here is a demo (with simple helper wrapper for cell). Tested with Xcode 13.2 / iOS 15.2
struct ContentView: View {
#State private var viewHeight = CGFloat.zero
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
GridCell(height: $viewHeight) {
VStack {
Spacer()
Text("Hello")
}
}.border(.red)
GridCell(height: $viewHeight) {
Text("Lorem ipsum asdfd")
}.border(.blue)
}
.onPreferenceChange(ViewHeightKey.self) {
self.viewHeight = max($0, viewHeight)
}
.border(.green)
.padding(.horizontal, 100)
}
}
struct GridCell<Content: View>: View {
#Binding var height: CGFloat
#ViewBuilder let content: () -> Content
var body: some View {
content()
.frame(minHeight: height)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
I had exactly same problem. My LazyVGrid looked great on iOS 14, but now its items have different heights.
I found a dirty workaround to force the items have same height:
In my case I have only several items in each LazyVGrid (So it won't cause too much performance drop), and it is easy for me to know which item has the largest height. So I made a ZStack and put a transparent highest item behind the actual item.
struct ContentView: View {
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
ZStack {
Text("Lorem ipsum") // This is the highest item.
.opacity(0) // Make it transparent.
Text("Hello")
}
.border(.red)
Text("Lorem ipsum")
.border(.blue)
}
.border(.green)
.padding(.horizontal, 100)
}
}
This workaround works in my case, but I don't recommend using it widely in your app especially when you have a lot of items in the grid.

Is there a way in SwiftUI to fulfill scrolling animations between 2 numbers

I'm looking for a similar way https://github.com/stokatyan/ScrollCounter in SwiftUI
This is a very rudimentary build of the same thing. I'm not sure if you're wanting to do it on a per-digit basis, however this should give you a solid foundation to work off of. The way that I'm handling it is by using a geometry reader. You should be able to easily implement this view by utilizing an HStack for extra digits/decimals. The next thing I would do would be to create an extension that handles returning the views based on the string representation of your numeric value. Then that string is passed as an array and views created for each index in the array, returning a digit flipping view. You'd then have properties that are having their state observed, and change as needed. You can also attach an .opacity(...) modifier to give it that faded in/out look, then multiply the opacity * n where n is the animation duration.
In this example you can simply tie your Digit value to the previewedNumber and it should take over from there.
struct TestView: View {
#State var previewedNumber = 0;
var body: some View {
ZStack(alignment:.bottomTrailing) {
GeometryReader { reader in
VStack {
ForEach((0...9).reversed(), id: \.self) { i in
Text("\(i)")
.font(.system(size: 100))
.fontWeight(.bold)
.frame(width: reader.size.width, height: reader.size.height)
.foregroundColor(Color.white)
.offset(y: reader.size.height * CGFloat(previewedNumber))
.animation(.linear(duration: 0.2))
}
}.frame(width: reader.size.width, height: reader.size.height, alignment: .bottom)
}
.background(Color.black)
Button(action: {
withAnimation {
previewedNumber += 1
if (previewedNumber > 9) {
previewedNumber = 0
}
}
}, label: {
Text("Go To Next")
}).padding()
}
}
}