SwiftUI horizontal ScrollView wanting to auto centre item - swift

I’m very new to SwiftUI and trying to figure out the best way to handle this.
I have set up a card view and that I have in a horizontal ScrollView. I am trying to get it to snap or align to the next card so it is always central. Think App Store apps or games tab in the header, I want it to behave like this.
I have searched around a lot for this but cant seem to find a solution, at least in SwiftUI. I implemented a similar thing in a UIKit app but I’m looking for a native SwiftUI solution if at all possible. This is what I implemented previously https://github.com/BenEmdon/CenteredCollectionView Though I am trying to seek a lighter and native SwiftUI way of doing this if at all possible as I always felt this was a little heavy for what I need.
The code I have so far is as follows:
ScrollView(.horizontal,
content: {
HStack {
CardView(card: cardDetails)
CardView(card: cardDetails)
CardView(card: cardDetails)
CardView(card: cardDetails)
}
.padding(.leading, 10)
})
.frame(height: 250)
struct CardView: View {
let card: Card
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 25, style: .continuous)
.fill(Color.white)
VStack {
Text(card.prompt)
.font(.largeTitle)
.foregroundColor(.black)
Text(card.answer)
.font(.title)
.foregroundColor(.gray)
}
.padding(20)
.multilineTextAlignment(.center)
}
.frame(width: 450, height: 250)
}
}
Any input will be greatly appreciated, thank you in advance for your help.
Here is an image from the App Store, the header section is similar to what I want to achieve, when you scroll through these it scrolls on a per item basis and almost snaps to the next item rather than a free scroll:

Related

How do i set full height on a component in a scroll view?

I am a web developer trying to learn SwiftUI and have some questions regarding about my issue.
So my code architecture is as follows:
ContentView.swift
ScrollView(.vertical){
Foreach(users){
user in UserCard()
}
}
UserCard.swift
VStack(){
Image()
Text("long text")
}
How do I make my UserCard to always the height of my iphone? As of right now, my UserCard sometimes overflow or cuts off too quickly depending on my Text length.
Any help would be appreciated. And any tips for a SwiftUI beginner would be great!
Thank you in advanced.
If you want it to be full screen and ignoring safe area you can set the UserCard's height to screen height like this:
VStack(){
Image()
Text("long text")
}
.frame(height: UIScreen.main.bounds.height)
And if you want it to respect safe area you could use a GeometryReader:
GeometryReader { geometry in
ScrollView {
Color.red
.frame(height: geometry.size.height)
Color.blue
.frame(height: geometry.size.height)
Color.yellow
.frame(height: geometry.size.height)
}
}
one Question -> one Answer
for second part of your Question need new Question with own Topic.
ZStack()
{
VStack()
{
Image(systemName: "star")
Text("long text")
}
}
.ignoresSafeArea()

SwiftUI Finding Solution for Button overlay on Image

I overlayed a button/text on the image, but the button didnt respond. Is there any solution?
It seems like the hidden navigation bar view is blocking the button or the image is blocking the touch gesture. How should I fix this?
struct Tutorial: View {
let dataModel = DataModel()
#State var messageIndex = 0
var body: some View{
ZStack{
Image("Tutorial UI")
.resizable()
.scaledToFill()
.overlay(
VStack(spacing:20){
VStack(spacing:30){
HStack{
Spacer()
Text("Next")
.foregroundColor(.blue)
.fontWeight(.black)
.onTapGesture(count:1) {
if self.messageIndex != 4{
self.messageIndex += 1
}else{
self.messageIndex = 0
}
}
}
Text(dataModel.tutorialMessage[messageIndex][0])
.fontWeight(.black)
.font(.largeTitle)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
}.padding()
Text(dataModel.tutorialMessage[messageIndex][1])
.font(.title)
Spacer()
}.padding(.horizontal, 48)
.padding(.top, 50)
)
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity,
alignment: .center)
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
}
What #Asperi was getting at is you have provided a code snippet that depends upon a model we know nothing about. When you post code, you should post a simplified version. In this case I took your code and swapped out your data model for a simple array. I also downloaded an image to use from NASA's Image of the Day to use. And, Asperi nailed the problem with his second question...
Looking at it, the first thing that struck me is you are trying too hard to position things just so in SwiftUI. It doesn't work that way. And that led to your problems. I pulled out your frames which actually don't do anything in this layout. I also pulled out the specific padding amounts. I then pulled the Spacer() out that was in your HStack, and your "Next" button appeared on the screen. I suspect that setting the image as .scaledToFill() and an image that is not in the same dimensions of the screen caused the view to be larger than the screen you could see. Putting the Spacer() in caused your "Next" button to move all of the way to the right, which was off screen. Once I could see it, I was able to click it and it worked fine.
You have an extremely complicated structure that produces a pretty simple layout. Next time, start small and only add what you need. Less layout is better in SwiftUI, or you end up with unintended consequences. Try changing the HStack in your Overlay to the following and then work from there, one step at a time:
HStack{
Text("Next")
.foregroundColor(.blue)
.fontWeight(.black)
.onTapGesture(count:1) {
if self.messageIndex != 4{
self.messageIndex += 1
}else{
self.messageIndex = 0
}
}
}
Remember, in SwiftUI the Child View determines its own size. Don't try to force things from a Parent.

SwiftUI Nested GeometryReader - Breaking UI

So I am trying to understand why my subview (TopView) is having weird resizing issues.
Here is the sample
import SwiftUI
struct ContentView: View {
#State var isInterfaceHidden: Bool = false
var body: some View {
VStack(spacing: 0, content: {
if !isInterfaceHidden {
TopView()
.background(Color.yellow)
}
Rectangle()
.foregroundColor(Color.red)
/// We make sure it won't cover the top and bottom view.
.zIndex(-1)
if !isInterfaceHidden {
Rectangle()
.foregroundColor(Color.yellow)
.frame(height: 80)
}
})
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
struct TopView: View {var body: some View {
HStack(content: {
VStack(spacing: 0, content: {
Text("Text to show, it is a title.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
GeometryReader(content: { geometry in
Text("Text to show, it is a subline.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
})
.background(Color.purple)
})
})
.padding([.leading, .trailing], 20)
}
}
I tried to set a .fixedSize() like this:
GeometryReader(content: { geometry in
Text("Text to show, it is a subline.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
})
.background(Color.purple)
But it is not fitting the text exactly, so I am not sure if this is the right solution. Do you guys have any idea?
Be aware that GeometryReader has had what appears to be a regression as of 14.0 (26/Sep/20) - or perhaps a wonderfully undocumented change of behaviour - with weighting layouts towards the topleft corner - rather than the center.
This has only appeared with apps I developed and built with XCode 12 - an XCode-11-compiled-app running on iOS 14 did not exhibit the issue. Most tutorials on the net will be assuming this worked the way it did in iOS 13/XCode 11 and your code may function differently
iOS 14 has Changed (or broken?) SwiftUI GeometryReader for a more involved question with the same issues
As far as I know, GeometryReader passes back its parent a size that is given by the parent unless you set frame() to GeometryReader explicitly. Even so, If you want to fit the area of GeometryReader to the Text view (exactly your custom view), you will have to calculate a height of the custom view by using preference or anchorPreference and then set it as a height of GeometryReader in order to let the parent know what size it needs to assign.
I hope the following link will be helpful.
https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
GeometryReader fit to View
If you're looking for the GeometryReader to not affect the size of your view, you should make an inversion. The view that you return inside the GeometryReader should be out, and the GeometryReader itself should be put in a background or in a overlay of that View.
Text("Text to show, it is a subline.")
.tracking(0.2)
.foregroundColor(.white)
.lineLimit(1)
.overlay(
GeometryReader(content: { geometry -> Color in
print(geometry.frame(in: .global))
return Color.clear
})
)
.background(Color.purple)
Either way (background or overlay), would solve your problem. Try changing overlay to background to see.
Just remember to return a Color.clear, this way, the GeometryReader becomes invisible and it doesn't change the View.

HStack inside ScrollView animates weirdly when opening view

So I was trying to implement expanding/contracting rows in a for each embedded in a list view and it turns out the list view cells don't animate smoothly. After checking out this tutorial, it suggests using a scrollview with a foreach because it animates smoother. The expanding/contracting works fine, but there unintended side effects only when I first open the page. The HStack appears to start flattened on the left side of the view and animates expanding to its normal starting position. I've slowed down the animations in the video so it's easier to see, and I removed the ForEach since it doesn't seem to be the cause of the problems. I don't know what is causing this and google searching has yielded no answers. Does anyone here have an answer or at least some advice? Much appreciated
#State var showTemp: Bool = false
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
HStack(alignment: .top) {
Text("Hello")
.font(.system(size: 32))
if showTemp {
VStack {
Text("Middle 1")
Text("Middle 2")
Text("Middle 3")
Text("Middle 4")
Text("Middle 5")
}
}
Spacer()
Text("Goodbye")
}
.border(Color.black, width: 2)
.onTapGesture {
self.showTemp.toggle()
}
.background(Color.blue)
.animation(.default)
}
.background(Color.green)
.onAppear(perform: loadMethod)
}
It seems I've answered my own question after I looked at the problem from a different angle. Using GeogetryReader and setting the frame width of the HStack to geometry.size.width fixed it.
I think this works because without specifying the width, the HStack fills as much area as it can. For some reason, it starts smushed when the view is loaded and then expands to fill its area, which I guess isn't visible unless you apply an animation to it.
Trying to be helpful here, I don't want to be one of those people who answers their own questions but doesn't show what the answer it.

List inside ScrollView is not displayed on WatchOs

I have a list inside a scrollview and it is not displaying below the image and the buttons. I've also tried to put the list and other items inside of a VStack and that allows me to see one item of the list at the time opposed to scrolling past the Image and buttons to show the whole list.
ScrollView{
Image(uiImage: self.image)
.resizable()
.frame(width: 80, height: 80)
.scaledToFit()
Text("\(name)")
.lineLimit(2)
HStack{
Button(action: {
print("button1")
}){
Image(systemName: "pencil")
}
Button(action: {
print("button 2")
}){
Image(systemName: "trash")
}
}
List{
ForEach(self.items, id: \.self) { item in
VStack{
Text(item.name)
.font(.headline)
.lineLimit(1)
Text(item.subname)
.font(.subheadline)
.lineLimit(1)
}
}
}
}
.navigationBarTitle(Text("Tittle"))
.edgesIgnoringSafeArea(.bottom)
Ive also tried to add .frame( minHeight: 0, maxHeight: .infinity)
to the list to force it to have its whole height and that did not work either. Any suggestions or might this be a swiftUI bug ?
EDIT
I just realized Im getting this error when scrolling:
APPNAME Watch Extension[336:60406] [detents] could not play detent NO, 2, Error Domain=NSOSStatusErrorDomain Code=-536870187 "(null)", (
{
Gain = "0.01799999922513962";
OutputType = 0;
SlotIndex = 4;
},
{
Gain = "0.6000000238418579";
OutputType = 1;
SlotIndex = 5;
}
)
Indicate some height for your List like
List{
ForEach(self.items, id: \.self) { item in
VStack{
Text(item.name)
.font(.headline)
.lineLimit(1)
Text(item.subname)
.font(.subheadline)
.lineLimit(1)
}
}
}.frame(minHeight: 200, maxHeight: .infinity)
Have you tried putting the List inside of a GeometryReader and setting the frame there?
I got the same error with UIKit. The error message is related to haptic feedback, see here.
It says that a haptic feedback could not be played, probably type 2, i.e. directionDown, see here.
Since my code does not call play(_:), it must be called by watchOS itself.
The reason for the error might be very fast scrolling, which could result in too frequent calls to play(_:) that cannot be handled properly. The docs say:
Do not call this method multiple times in quick succession. If the
haptic engine is already engaged when you call this method, the system
stops the current feedback and imposes a minimum delay of 100
milliseconds before engaging the engine to generate the new feedback.
If this is really an effect of watchOS, I guess you cannot do anything to avoid the error.