Position of ScrollView not consistent in main view - swiftui

I have a scrollview with an HStack inside it that is sized according to how many hexagons are to be placed in it. There are three possible values - 25, 64 and 100. The scrollview is positioned correctly for 25 and 64 hexagons, but is shifted to the right for 100. How can I make sure that is centred correctly too?
Incidentally, I have already tried manually setting the position of the scrollview - it worked the same as without.
GeometryReader
{ scrollViewGeometry in
ScrollView([.vertical, .horizontal], showsIndicators: true)
{
Spacer()
HStack(spacing:-(max(minHexagonSize, smallSide)))
{
ForEach(viewModel.hexagons){hexagon in
let hexagonFrame = getFrameFor(hexagon: hexagon, minWidth: smallSide)
let colorArray = getColorsFor(hexagon: hexagon)
ZStack()
{
let thisIndex = hexagon.id
HexagonView(strokeColor: colorArray[0]).onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeInOut(duration: 0.2))
if hexagon.isSelected
{
Circle().fill(playerColorArray[hexagon.playerIndex]).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
Circle().stroke(Color.black).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
}
else
{
Text(verbatim: String(thisIndex+1)).font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.onTapGesture(perform: {
Sounds.playSounds(soundName: "pop")
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeIn(duration: 0.5))
}
if hexagon.isSelected && hexagon.pressed
{
Text(String(viewModel.currentScore))
.foregroundColor(Color.black)
.font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.transition(AnyTransition.asymmetric(insertion: .identity, removal: AnyTransition.move(edge: .top).combined(with: .opacity).animation(.easeIn(duration: 1.0))))
.animation(.easeIn(duration: 1.0))
.zIndex(hexagon.isSelected ? 1 : 0)
}
}
.position(x: hexagonFrame.origin.x - (scrollViewGeometry.size.width/2), y: scrollViewGeometry.size.height/2 - hexagonFrame.origin.y)
.frame(width: hexagonFrame.size.width, height: hexagonFrame.size.height)
.zIndex(hexagon.isSelected ? 1 : 0)
}
}
.frame(width: (max(minHexagonSize, smallSide)) * (sqrt(CGFloat(viewModel.hexagons.count)) * 2), height: (max(minHexagonSize, smallSide) * (sqrt(CGFloat(viewModel.hexagons.count)) + 2)))
//.position(x: scrollViewGeometry.size.width, y: scrollViewGeometry.size.height)
}
.padding()
}

OK... so it turns out that the reason it was not centred, is because I was looking at the wrong part of the view. I still don't understand why it worked fine for the smaller scrollable areas in the scrollview, but not for the larger one!

Related

How does ScrollView work when wrapping a ZStack?

In the code below, firstScrollProxy does not work, while secondScrollViewProxy does. I don't understand why.
The only solution I found, was to give some id to the overlay, and scroll to that. However that causes other issues for my code, and I'd rather avoid such workarounds.
I played with fixedSize() for the ZStack items, but that didn't help either.
Laying out the items vertically has the same issue, while a VStack works.
The anchor is optional, but trying different anchors does reveal the fact that the scroll view behaves as if the width of the items are the same as the entire scrollable area.
import SwiftUI
struct ContentView: View {
var body: some View {
let numItems: Int = 100
let itemWidth = 60.0
let itemHeight = 100.0
VStack(spacing: 4) {
Spacer()
ScrollViewReader { firstScrollProxy in
ScrollView(.horizontal) {
ZStack {
ForEach(0..<numItems, id:\.self) { x in
Rectangle()
.fill(Color.purple)
.frame(width: itemWidth - 2, height: itemHeight)
.overlay {
Text("\(x)")
}
.position(x: Double(x) * itemWidth + itemWidth / 2.0, y: itemHeight / 2.0)
}
}
.frame(width: Double(numItems) * itemWidth, height: itemHeight)
}
.onTapGesture {
withAnimation {
firstScrollProxy.scrollTo(17, anchor: .center)
}
}
}
.padding(8)
.background(Color(white: 0.2))
Color.clear.frame(height: 10)
ScrollViewReader { secondScrollProxy in
ScrollView(.horizontal) {
HStack(spacing: 2) {
ForEach(0..<numItems, id:\.self) { x in
Rectangle()
.fill(Color.blue)
.frame(width: itemWidth - 2, height: itemHeight)
.overlay {
Text("\(x)")
}
}
}
.frame(height: itemHeight)
}
.onTapGesture {
withAnimation {
secondScrollProxy.scrollTo(17, anchor: .center)
}
}
}
.padding(8)
.background(Color(white: 0.25))
Spacer()
}
.background(.black)
.foregroundColor(.white)
}
}
ZStack doesn't know about the effect of the position modifier. ZStack just assumes all of its children are piled up on top of each other, since that's how it lays them out. So when the ZStack's parent ScrollView asks the ZStack for its size, the ZStack reports a size that is the maximum width and height of any of its children, without accounting for the side-by-side layout you have manually implemented.
Given Rob's answer, the solution is actually to use the anchor to calculate the the position to scroll to. E.g.
ZStack {
// ...
}
.id("zstack")
.onTapGesture {
.withAnimation {
let x = position_of_17 / width_of_zstack
firstScrollProxy.scrollTo("zstack", anchor: UnitPoint(x: x, y: 1.0))
}
}
}
I found a better workaround, if anyone should have this problem. Simply embed a HStack with a width equal to the ZStack in which you can add invisible elements with the ids and the location that you need to scroll to.
In my case this is acceptable, I only ever need to programmatically scroll to 1 element, and switching to an actual HStack view for my content would not make sense for my use case.
ZStack {
// Content
HStack(spacing: 0) {
Color.clear
.frame(width: targetX, height: 1)
Color.clear
.frame(width: itemWidth, height: 1)
.id("target")
Color.clear
.frame(width: totalWidth - targetX - itemWidth, height: 1)
}
}
func scrollToTarget() {
scrollProxy.scrollTo("target", anchor: anchor)
}

At SwiftUI how do you assign different actions if buttons are created in a loop

How can you assign different actions if you are creating buttons in a loop when using SwiftUI.
I was using sender tag while using UIView.
At following code, every buttons calls the same function as usual.
How can I make them call different action in case we are using loops to create buttons.
var buttonNames = ["OK", "NOPE"]
var body: some View {
let width = UIScreen.main.bounds.width / CGFloat(buttonNames.count)
ScrollView (.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(0..<buttonNames.count, id: \.self) { index in
Button(buttonNames[index]) {
buttonTouched()
}.frame(width: width)
.background(Color.gray)
}
}
.frame(width: UIScreen.main.bounds.width, height: 45, alignment: .center)
.foregroundColor(.white)
.background(Color.gray)
}
.position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height - 45)
}
After the comment I did it like this and it makes the trick.
var buttonNames = ["OK", "NOPE"]
#State var touchedButton = 0
func buttonTouched(){
print("Button tapped! \(touchedButton)")
}
var body: some View {
let width = UIScreen.main.bounds.width / CGFloat(buttonNames.count)
ScrollView (.horizontal, showsIndicators: false) {
LazyHStack {
ForEach(0..<buttonNames.count, id: \.self) {
index in
Button(action:{
touchedButton = index
buttonTouched()
}
){
Text(buttonNames[index])
}
.frame(width: width)
}
}
.frame(width: UIScreen.main.bounds.width, height: 45, alignment: .center)
.foregroundColor(.white)
.background(Color.gray)
} .position(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height - 45)
}

How to move View in HStack to the top of the other views

I have a set of subviews that are arranged within an HStack. I have a transition that is run whenever a view is selected successfully, but it is occluded by neighbouring views "above" it.
I found this article here, but I can't see how to integrate it into my solution:
How to bring a view on top of VStack containing ZStacks
Is there a way of getting the selected view to pop on top of the others?
HStack(spacing: -75)
{
ForEach(viewModel.hexagons){hexagon in
let hexagonFrame = getFrameFor(hexagon: hexagon, minWidth: smallSide)
let colorArray = getColorsFor(hexagon: hexagon)
ZStack()
{
let thisIndex = hexagon.id
HexagonView(strokeColor: colorArray[0]).onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeInOut(duration: 0.2))
if hexagon.isSelected
{
Circle().fill(playerColorArray[hexagon.playerIndex]).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
Circle().stroke(Color.black).frame(width: hexagonFrame.width * fontScalingFactor, height: hexagonFrame.height * fontScalingFactor, alignment: .center)
}
else
{
Text(verbatim: String(thisIndex+1)).font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.onTapGesture(perform: {
viewModel.chooseHexagon(hexagon: hexagon)
})
.scaleEffect(hexagon.pressed ? 1.2 : 1.0)
.animation(Animation.easeIn(duration: 0.5))
}
This is the view that animates up from the haxagon that has been selected
if hexagon.isSelected && hexagon.pressed
{
Text(String(viewModel.currentScore))
.foregroundColor(Color.gray)
.font(Font.system(size: hexagonFrame.size.width * fontScalingFactor)).foregroundColor(colorArray[1])
.transition(AnyTransition.asymmetric(insertion: .identity, removal: AnyTransition.move(edge: .top)))
.animation(.easeIn(duration: 2.0))
.zIndex(.greatestFiniteMagnitude)
}
}
.position(x: hexagonFrame.origin.x + hexagonFrame.size.width / 3 - (scrollViewGeometry.size.width/2), y: (scrollViewGeometry.size.height/2) - hexagonFrame.origin.y)
.frame(width: hexagonFrame.size.width, height: hexagonFrame.size.height, alignment:.center)
}
}.frame(width: (max(75, smallSide) * 8), height: (max(75, smallSide) * 5))

How to determine where to zoom and pan on an image in SwiftUI?

When developing with SwiftUI, I am using an Image() and would like to be able to not only zoom in on it, which I am currently doing, but would like to zoom in on different areas of it and to pan across to different areas. It seems I can only zoom in on it from one area as the origin of the zoom (center) but would like to zoom from where on the image the gesture started from to so speak. Here is the code so far:
ScrollView([.horizontal, .vertical]) {
Image(uiImage: UIImage(data: self.document.image)!)
.resizable()
.scaledToFit()
.padding()
.cornerRadius(8)
.scaleEffect(self.scale, anchor: .center)
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
.gesture(MagnificationGesture()
.onChanged { val in
let delta = (val / self.lastScale)
self.lastScale = val
self.scale = (self.scale * delta)
}
.onEnded { val in
self.lastScale = 1.0
}
)
.onTapGesture(count: 2) {
self.scale = 1.0
self.lastScale = 1.0
}
}
.frame(width: geo.size.width - 50, height: geo.size.height - 90, alignment: .center)
.clipped()
Any help on this would be much appreciated.
UPDATED Code seems to do the trick for me:
Image(uiImage: UIImage(data: self.document.image)!)
.resizable()
.scaledToFill()
.padding()
.cornerRadius(10)
.scaleEffect(self.finalAmount + self.currentAmount)
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
.frame(width: geo.size.width, height: geo.size.height, alignment: .center)
.clipped()
.scaleEffect(1.15)
.sheet(isPresented: self.$isSheetActive) {
ShareSheet(activityItems: [self.MakeImageFileToShare(uiimage: UIImage(data: self.document.image)!)])
}
.gesture(MagnificationGesture()
.onChanged { value in
if self.finalAmount + value - 1 >= 1 {
self.currentAmount = value - 1
}
}
.onEnded { amount in
self.finalAmount += self.currentAmount
self.currentAmount = 0
}
.simultaneously(with: DragGesture()
.onChanged({ value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
})
.onEnded ({ value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.newPosition = self.currentPosition
})))

SwiftUI: What is the Proper Logic Statement to Prevent Views From Disappearing While Using a Custom Slider

My SwiftUI Views are disappearing when I am using a custom made slider. I believe it is because the frame values of the Views are becoming negative; however, I feel that I have coded the logic correctly to prevent this from happening (but something is wrong).
I have tried several different if statements to prevent the values from becoming negative but am having no luck.
import SwiftUI
struct ContentView: View {
#State var scale: CGFloat = 1.0
#State private var dragged = CGSize.zero
#State private var accumulated = CGSize.zero
var body: some View {
ZStack {
Rectangle()
.stroke()
.frame(width: 300, height: 5, alignment: .leading)
.position(x: 180, y: 30)
Rectangle()
.frame(width: 0 + dragged.width, height: 5, alignment: .leading)
.position(x: 30 + (0.5 * dragged.width), y: 30)
Circle()
.foregroundColor(.red)
.frame(width: 20, height: 20).border(Color.red)
.position(x: 30, y: 30)
.offset(x: self.dragged.width)
.gesture(DragGesture()
.onChanged{ value in
//My IF-Statement
if value.translation.width > 0 {
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width,
height: value.translation.height + self.accumulated.height
)
}
}
.onEnded{ value in
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width,
height: value.translation.height + self.accumulated.height)
self.accumulated = self.dragged
}
)
VStack{
Text("\(dragged.width)")
Text("\(30 + (0.5 * dragged.width))")
}
}
}
}
The error occurs when I slide the red circle too far to the left very quickly. I am expecting the IF statement to stop the circle before it makes the variable negative (if my assumption is correct about what is making it disappear).
Limit to the right
You don't need an if statement, but rather a maximum the width can go. One way to do it to use min(maxValue, calculated) to enforce the width to not go beyond maxValue.
Limit to the left
Your drag starts at the red dot, but dragging is not limited to your drawn scale, but you can rather drag to the left of it since the screen continues until the screen edge, which makes drags to the left go negative.
To avoid it you can check whether the result is negative and if so, just use 0 instead of the negative value, like below.
import SwiftUI
struct SliderView: View {
#State var scale: CGFloat = 1.0
#State private var dragged = CGSize.zero
#State private var accumulated = CGSize.zero
let maxWidth: CGFloat = 300
var body: some View {
ZStack {
Rectangle()
.stroke()
.frame(width: maxWidth, height: 5, alignment: .leading)
.position(x: 180, y: 30)
Rectangle()
.frame(width: 0 + dragged.width, height: 5, alignment: .leading)
.position(x: 30 + (0.5 * dragged.width), y: 30)
Circle()
.foregroundColor(.red)
.frame(width: 20, height: 20).border(Color.red)
.position(x: 30, y: 30)
.offset(x: self.dragged.width)
.gesture(DragGesture()
.onChanged{ value in
print("on changed called. width: \(value.translation.width)")
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width > 0 ?
min(self.maxWidth, value.translation.width + self.accumulated.width) : 0,
height: value.translation.height + self.accumulated.height
)
}
.onEnded{ value in
self.dragged = CGSize(
width: value.translation.width + self.accumulated.width > 0 ?
min(self.maxWidth, value.translation.width + self.accumulated.width) : 0,
height: value.translation.height + self.accumulated.height)
self.accumulated = self.dragged
}
)
VStack{
Text("\(dragged.width)")
Text("\(30 + (0.5 * dragged.width))")
}
}
}
}