SwiftUI - offset is wrong - swiftui

I came across this weird thing where I overlay two diffrent views. These two views contain a ForEach loop where it creates a Rectangle element. The offset of this rectangle is determined by the index of the ForEach loop * a set height variable of 30. One of the view's rectangles have a height of 1 while he other view's rectangle has as a height of this height variable. This should create a rectangle with a border. But it doesn't, the two views don't match up?
I have no clue why this happens. Of course if I wanted a Rectangle with a border I could do that but that's not my point. Im genuinely curious as why this happens. Code is below.
First view:
import SwiftUI
struct test: View {
var body: some View {
VStack(spacing: 0) {
ForEach(0 ..< 10) { i in
Rectangle()
.fill(Color.green)
.frame(height: 1)
.offset(y: CGFloat(height*i))
}
Spacer()
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
Second View:
import SwiftUI
struct test2: View {
var body: some View {
VStack(spacing: 0) {
ForEach(0 ..< 5) { i in
Rectangle()
.fill(Color.red)
.frame(height: CGFloat(height))
.offset(y: CGFloat(height*i))
}
Spacer()
}
}
}
struct test2_Previews: PreviewProvider {
static var previews: some View {
test2()
}
}
ContentView:
import SwiftUI
public var height = 30
struct ContentView: View {
var body: some View {
test2()
.overlay(test())
}
}
Result:

use this in test:
.offset(y: CGFloat((height*i)-i))

Say both test and test2 starts at y = 0.
Your first green rectangle locates between y=0 and y=1, and offset by 30
Your second green rectangle locates between y=31 and y=32
Third between y=62 and y=63
⋮
Your first red rectangle locates between y=0 and y=30, and offset by 30
Your second red rectangle locates between y=60 and y=90
⋮
Now you see the problem, then don't match.
To solve it, you could change the offsets in test:
struct test : View {
⋮
Rectangle()
.offset(y: CGFloat((height - 1) * i))
⋮
}

Related

SwiftUI chessBoard view with random field colors - unable to compile

I have a super simple chessboard structure in SwiftUI and wanted to give each field a random color, but Xcode cannot compile it.
I can provide a single color, or 2 colors with the following function:
let color = (row + column) % 2 == 0 ? Color.green : Color.blue and using fill(color) but random like shown below does not work giving me the warning that it could not compile in time.
I also tried to check the ID and provide a color from another array, but even this does not work.
Is my structure completely off?
import SwiftUI
struct ContentView: View {
#State var lastTappedSquareID: Int?
var body: some View {
VStack {
ChessBoard(squareSize: 50, lastTappedSquareID: $lastTappedSquareID)
if let squareID = lastTappedSquareID {
Text("Last Tapped Square ID: \(squareID)")
}
}
}
}
struct ChessBoard: View {
let rows : Int = 8
let columns : Int = 8
let squareSize: CGFloat
#Binding var lastTappedSquareID: Int?
var body: some View {
VStack(spacing: 0) {
ForEach(0..<rows) { row in
HStack(spacing: 0) {
ForEach(0..<columns) { column in
let id = Int(row * columns + column + 1)
Rectangle()
.fill([Color.green, Color.blue, Color.brown, Color.darkGray].randomElement()!)
.frame(width: squareSize, height: squareSize)
.id(id)
.border(id == lastTappedSquareID ? .red : .clear, width: 3)
.onTapGesture {
lastTappedSquareID = id
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It's having trouble with your fill line. I've split it into a separate function. Note also that darkGray isn't available on Color -- I've changed it to orange
func randomRectangleColor() -> Color {
[Color.green, Color.blue, Color.brown, Color.orange].randomElement()!
}
.fill(randomRectangleColor())
A basic/general strategy for the error you encountered is to comment out sections of the code until you find the culprit -- that's what I did to identify the problem line (your code compiles without the fill line)

Swiftui ScrollView is affected unexpectedly by scaleEffect modifier

I have some views with many details with fixed sizes, and am trying to use scaleEffect() to reduce them proportionally to fit better smaller devices. However, when using scaleEffect() on a ScrollView, I noticed that it has a larger effect than expected on the axis of the ScrollView. Small example below:
import SwiftUI
struct FancyItemView: View {
var body: some View {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
}
struct ItemDisplayView: View {
var sizeAdjustment: Double
var body: some View {
ScrollView(.horizontal){
FancyItemView()
}
.background(.blue)
.scaleEffect(sizeAdjustment)
.frame(width: 150 * sizeAdjustment, height: 100 * sizeAdjustment)
.border(.black)
}
}
struct ContentView: View {
var body: some View {
VStack{
ItemDisplayView(sizeAdjustment: 1)
ItemDisplayView(sizeAdjustment: 0.8)
ItemDisplayView(sizeAdjustment: 1.2)
}
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Screenshot of the resulting view: https://i.stack.imgur.com/POvjw.png
In this example I am using only one item view, but in my real code the ScrollView contains titles and grids of items. I may be able to work around this issue by applying scaleEffect to the other views around ScrollView and not applying to it, but that would make the code much more confusing. So I am wondering if there is anything I am missing to make scaleEffect work properly with ScrollView.
Thanks
I don´t think .scaleEffect is the propper tool here. It is more for visual presentation/animation than for laying out views. Get rid of the .scaleEffect modifier and pass your scale var through to your Controll and style it appropriatly.
struct FancyItemView: View {
var sizeAdjustment: Double
var body: some View {
Rectangle()
.fill(.red)
.frame(width: 100 * sizeAdjustment, height: 100 * sizeAdjustment)
}
}
struct ItemDisplayView: View {
var sizeAdjustment: Double
var body: some View {
ScrollView(.horizontal){
FancyItemView(sizeAdjustment: sizeAdjustment) // pass the multiplier to the ChildView
}
.background(.blue)
// .scaleEffect(sizeAdjustment) // remove this
// .frame(width: 150 * sizeAdjustment, height: 100 * sizeAdjustment) // you probably don´t want this either
// or at least get rid of the multiplier
.border(.black)
}
}

Why is Text onTapGesture not working for widened frame in SwiftUI?

I have this testing file that is not working how I expect it to.
import SwiftUI
struct SwiftUIView: View {
#State var boolTest = false
var nums = ["1","2","3","4","5","6","7"]
var body: some View {
VStack {
ForEach(nums, id: \.self) { num in
Text("\(num)")
.frame(width: 400)
.font(.system(size: 70))
.foregroundColor(boolTest ? .red : .green)
.onTapGesture {
boolTest.toggle()
}
}
}
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
When I tap on a number, the foreground color changes as expected. However, I want to be able to tap on the areas left and right of the Text("\(num)") so I expanded the frame modifier to test. However the color only changes when I tap directly on the number or text.
How do I tap on the space left or right of the number and have it change colors instead of it doing nothing?
The system treats the "invisible" (ie doesn't have visible drawn content) part of the view as unresponsive unless you set a contentShape on it.
//...
.frame(width: 400)
.contentShape(Rectangle())
//...

Scrollview doesn't scroll past views with an offset

To solve a much more complicated problem, I created the following simple test project:
struct ContentView: View {
var body: some View {
ScrollView {
ZStack {
ForEach(0..<50) { index in
Text("Test \(index)")
.offset(x: 0, y: CGFloat(index * 20))
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This draws 50 Text views inside a ZStack, each one with a larger y offset, so that they are drawn down past the visible part of the screen:
The whole thing is wrapped inside of a ScrollView, so I expect that I should be able to scroll down to the last view.
However, it doesn't scroll past Test 26.
How can I arrange views inside of a ZStack by assigning offsets and update the ScrollView's contentSize?
The content size is calculated by the size of the view inside the ScrollView. So that only thing we can do is to change that view size.
By default VStack size is the total size (actual view sizes) of views inside it.
As well as we can use frame() to change the size in this case. check apple document
Since VStack arrange view in the middle we can align it to the .top.
struct ContentView: View {
var body: some View {
ScrollView() {
VStack {
ForEach(0..<50) { index in
Text("Test \(index)")
.offset(x: 0, y: CGFloat(index * 20))
}
}
.frame( height: 2000, alignment: .top) //<=here
.border(Color.black)
}
}
}
If I understood your intention correctly then you don't need offset in this scenario at all (SwiftUI works differently)
ScrollView {
VStack {
ForEach(0..<50) { index in
Text("Test \(index)") // << use .padding if needed
}
}
}
Note: The offset does not change view layout, view remains in the same place where it was created, but rendered in place of offset; frames of other views also not affected. Just be aware.

SwiftUI ScrollView does not center content when content fits scrollview bounds

I'm trying to have the content inside a ScrollView be centered when that content is small enough to not require scrolling, but instead it aligns to the top. Is this a bug or I'm missing adding something? Using Xcode 11.4 (11E146)
#State private var count : Int = 100
var body : some View {
// VStack {
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(alignment: .center)
}
.border(Color.blue)
.padding(8)
// }
}
Credit goes to #Thaniel for finding the solution. My intention here is to more fully explain what is happening behind the scenes to demystify SwiftUI and explain why the solution works.
Solution
Wrap the ScrollView inside a GeometryReader so that you can set the minimum height (or width if the scroll view is horizontal) of the scrollable content to match the height of the ScrollView. This will make it so that the dimensions of the scrollable area are never smaller than the dimensions of the ScrollView. You can also declare a static dimension and use it to set the height of both the ScrollView and its content.
Dynamic Height
#State private var count : Int = 5
var body: some View {
// use GeometryReader to dynamically get the ScrollView height
GeometryReader { geometry in
ScrollView {
VStack(alignment: .leading) {
ForEach(0...self.count, id: \.self) { num in
Text("entry: \(num)")
}
}
.padding(10)
// border is drawn before the height is changed
.border(Color.red)
// match the content height with the ScrollView height and let the VStack center the content
.frame(minHeight: geometry.size.height)
}
.border(Color.blue)
}
}
Static Height
#State private var count : Int = 5
// set a static height
private let scrollViewHeight: CGFloat = 800
var body: some View {
ScrollView {
VStack(alignment: .leading) {
ForEach(0...self.count, id: \.self) { num in
Text("entry: \(num)")
}
}
.padding(10)
// border is drawn before the height is changed
.border(Color.red)
// match the content height with the ScrollView height and let the VStack center the content
.frame(minHeight: scrollViewHeight)
}
.border(Color.blue)
}
The bounds of the content appear to be smaller than the ScrollView as shown by the red border. This happens because the frame is set after the border is drawn. It also illustrates the fact that the default size of the content is smaller than the ScrollView.
Why Does it Work?
ScrollView
First, let's understand how SwiftUI's ScrollView works.
ScrollView wraps it's content in a child element called ScrollViewContentContainer.
ScrollViewContentContainer is always aligned to the top or leading edge of the ScrollView depending on whether it is scrollable along the vertical or horizontal axis or both.
ScrollViewContentContainer sizes itself according to the ScrollView content.
When the content is smaller than the ScrollView, ScrollViewContentContainer pushes it to the top or leading edge.
Center Align
Here's why the content gets centered.
The solution relies on forcing the ScrollViewContentContainer to have the same width and height as its parent ScrollView.
GeometryReader can be used to dynamically get the height of the ScrollView or a static dimension can be declared so that both the ScrollView and its content can use the same parameter to set their horizontal or vertical dimension.
Using the .frame(minWidth:,minHeight:) method on the ScrollView content ensures that it is never smaller than the ScrollView.
Using a VStack or HStack allows the content to be centered.
Because only the minimum height is set, the content can still grow larger than the ScrollView if needed, and ScrollViewContentContainer retains its default behavior of aligning to the top or leading edge.
You observe just normal ScrollView behaviour. Here is a demo of possible approach to achieve your goal.
// view pref to detect internal content height
struct ViewHeightKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}
// extension for modifier to detect view height
extension ViewHeightKey: ViewModifier {
func body(content: Content) -> some View {
return content.background(GeometryReader { proxy in
Color.clear.preference(key: Self.self, value: proxy.size.height)
})
}
}
// Modified your view for demo
struct TestAdjustedScrollView: View {
#State private var count : Int = 100
#State private var myHeight: CGFloat? = nil
var body : some View {
GeometryReader { gp in
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...self.count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(alignment: .center)
.modifier(ViewHeightKey()) // read content view height !!
}
.onPreferenceChange(ViewHeightKey.self) {
// handle content view height
self.myHeight = $0 < gp.size.height ? $0 : gp.size.height
}
.frame(height: self.myHeight) // align own height with content
.border(Color.blue)
.padding(8)
}
}
}
The frame(alignment: .center) modifier you’ve added doesn’t work since what it does is wrapping your view in a new view of exactly the same size. Because of that the alignment doesn’t do anything as there is no additional room for the view do be repositioned.
One potential solution for your problem would be to wrap the whole ScrollView in a GeometryReader to read available height. Then use that height to specify that the children should not be smaller than it. This will then make your view centered inside of ScrollView.
struct ContentView: View {
#State private var count : Int = 100
var body : some View {
GeometryReader { geometry in
ScrollView {
VStack {
Button(action: {
if self.count > 99 {
self.count = 5
} else {
self.count = 100
}
}) {
Text("CLICK")
}
ForEach(0...self.count, id: \.self) { no in
Text("entry: \(no)")
}
}
.padding(8)
.border(Color.red)
.frame(minHeight: geometry.size.height) // Here we are setting minimum height for the content
}
.border(Color.blue)
}
}
}
For me, GeometryReader aligned things to the top no matter what. I solved it with adding two extra Spacers (my code is based on this answer):
GeometryReader { metrics in
ScrollView {
VStack(spacing: 0) {
Spacer()
// your content goes here
Spacer()
}
.frame(minHeight: metrics.size.height)
}
}