SwiftUI - How to center a component in a horizontal ScrollView when tapped? - swift

I have a horizontal ScrollView, and within it an HStack. It contains multiple Subviews, rendered by a ForEach. I want to make it so that when these Subviews are tapped, they become centered vertically in the view. For example, I have:
ScrollView(.horizontal) {
HStack(alignment: .center) {
Circle() // for demonstration purposes, let's say the subviews are circles
.frame(width: 50, height: 50)
Circle()
.frame(width: 50, height: 50)
Circle()
.frame(width: 50, height: 50)
}
.frame(width: UIScreen.main.bounds.size.width, alignment: .center)
}
I tried this code:
ScrollViewReader { scrollProxy in
ScrollView(.horizontal) {
HStack(alignment: .center) {
Circle()
.frame(width: 50, height: 50)
.id("someID3")
.frame(width: 50, height: 50)
.onTapGesture {
scrollProxy.scrollTo(item.id, anchor: .center)
}
Circle()
.frame(width: 50, height: 50)
.id("someID3")
.frame(width: 50, height: 50)
.onTapGesture {
scrollProxy.scrollTo(item.id, anchor: .center)
}
...
}
}
But it seemingly had no effect. Does anyone know how I can properly do this?

You can definitely do this with ScrollView and ScrollViewReader. However, I see a couple of things that could cause problems in your code sample:
You use the same id "someID3" twice.
I can't see where your item.id comes from, so I can't tell if it actually contains the same id ("someID3").
I don't know why you have two frames with the same bounds on the same view area. It shouldn't be a problem, but it's always best to keep things simple.
Here's a working example:
import SwiftUI
#main
struct MentalHealthLoggerApp: App {
var body: some Scene {
WindowGroup {
ScrollViewReader { scrollProxy in
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 10) {
Color.clear
.frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
ForEach(Array(0..<10), id: \.self) { id in
ZStack(alignment: .center) {
Circle()
.foregroundColor(.primary.opacity(Double(id)/10.0))
Text("\(id)")
}
.frame(width: 50, height: 50)
.onTapGesture {
withAnimation {
scrollProxy.scrollTo(id, anchor: .center)
}
}
.id(id)
}
Color.clear
.frame(width: (UIScreen.main.bounds.size.width - 70) / 2.0)
}
}
}
}
}
}
Here you can see it in action:
[EDIT: You might have to click on it if the GIF won't play automatically.]
Note that I added some empty space to both ends of the ScrollView, so it's actually possible to center the first and last elements as ScrollViewProxy will never scroll beyond limits.

Created a custom ScrollingHStack and using geometry reader and a bit calculation, here is what we have:
struct ContentView: View {
var body: some View {
ScrollingHStack(space: 10, height: 50)
}
}
struct ScrollingHStack: View {
var space: CGFloat
var height: CGFloat
var colors: [Color] = [.blue, .green, .yellow]
#State var dragOffset = CGSize.zero
var body: some View {
GeometryReader { geometry in
HStack(spacing: space) {
ForEach(0..<15, id: \.self) { index in
Circle()
.fill(colors[index % 3])
.frame(width: height, height: height)
.overlay(Text("\(Int(dragOffset.width))"))
.onAppear {
dragOffset.width = geometry.size.width / 2 - ((height + space) / 2)
}
.onTapGesture {
let totalItems = height * CGFloat(index)
let totalspace = space * CGFloat(index)
withAnimation {
dragOffset.width = (geometry.size.width / 2) - (totalItems + totalspace) - ((height + space) / 2)
}
}
}
}
.offset(x: dragOffset.width)
.gesture(DragGesture()
.onChanged({ dragOffset = $0.translation})
)
}
}
}

Related

Scaled Images Don't accessible in Grid view with right way

Main View Code , My Subview CodeMy CardView is in ZStack and, when stretching the background image, didn't select in the right way a GridView in Main View here is my codes of main and subview
ScrollView {
NavigationLink("", destination: CardCategoryView(cardCategoryVm: .init(card: cardsVm.selectedCard)), isActive: $cardsVm.isShowingCardCategory)
LazyVGrid(columns: gridColoumns,spacing: 8) {
ForEach(cardsVm.cards?.vendor ?? [] ,id: \.self) { symbol in
StoreCardView(image: symbol.image ?? "")
.frame(width: UIScreen.main.bounds.width / 2 - 20,height: UIScreen.main.bounds.width / 2 - 20)
.padding(.top)
.onTapGesture {
let _ = print(symbol.name)
cardsVm.selectedCard = symbol
cardsVm.isShowingCardCategory = true
}
}
}
}
struct StoreCardView: View {
var image:String
var body: some View {
GeometryReader { reader in
ZStack{
KFImage(URL(string:image))
.resizable()
.scaleEffect(x: 3, y: 3, anchor: .center)
.aspectRatio(contentMode: .fit)
.frame(width: reader.size.width , height: reader.size.height , alignment: .center)
.clipped()
.blur(radius: 25, opaque: true)
.opacity(0.7)
.cornerRadius(8)
KFImage(URL(string:image))
.resizable()
.aspectRatio( contentMode: .fill)
.frame(width: reader.size.width/2, height: reader.size.height/2, alignment: .center)
.clipShape(Circle())
.shadow(color: .movColor.opacity(0.4),radius: 5,
x: 0,
y: 0
)
//
} .fixedSize(horizontal: true, vertical: true)
.frame(width: reader.size.width, height: reader.size.height, alignment: .center)
.clipped()
}
}
}
I made it in Ztack and gave it a fixed frame with Scale Effect and it didn't work also, how can I make the background image stretching and didn't effect on gridview at main view

SwiftUI: Frame modifier in ZStack affecting other views

struct CircleTestView: View {
let diameter: CGFloat = 433
var body: some View {
ZStack {
Color(.yellow)
.ignoresSafeArea()
VStack {
Circle()
.fill(Color(.green))
.frame(width: diameter, height: diameter)
.padding(.top, -(diameter / 2))
Spacer()
}
VStack {
Spacer()
Button {} label: {
Color(.red)
.frame(height: 55)
.padding([.leading, .trailing], 16)
}
}
}
}
}
The code above creates the first image, yet for some reason if I remove the line the sets the frame for the Circle (ie. .frame(width: diameter, height: diameter)) I get the second image.
2.
I want the circle how it is in the first screen, and the button how it is in the second screen, but can't seem to achieve this. Somehow setting the frame of the Circle is affecting the other views, even though they're in a ZStack. Is this a bug with ZStacks, or am I misunderstanding how they work?
Lets call this one approach a:
struct CircleTestView: View {
let diameter: CGFloat = 433
var body: some View {
ZStack {
Color(.yellow)
.ignoresSafeArea()
VStack {
Circle()
.fill(Color(.green))
.frame(width: diameter, height: diameter)
.padding(.top, -(diameter / 2))
Spacer()
}
VStack {
Spacer()
Button {} label: {
Color(.red)
.frame(height: 55)
}
}
.padding(.horizontal, 16)
}
}
}
Lets call this one approach b:
struct CircleTestView: View {
let diameter: CGFloat = 433
var body: some View {
ZStack {
Color(.yellow)
.ignoresSafeArea()
VStack {
Circle()
.fill(Color(.green))
.offset(x: 0, y: -(diameter / 1.00))
// increment/decrement the offset by .01 example:
// .offset(x: 0, y: -(diameter / 1.06))
Spacer()
}
VStack {
Spacer()
Button {} label: {
Color(.red)
.frame(height: 55)
.padding([.leading, .trailing], 16)
}
}
}
}
}
A combination of the two approaches would land you at approach c.
Do any of these achieve what you are looking for?

Set SwiftUI view to always stay one size

I have a view that I always want to remain the same size, I haven't made it responsive to different screen sizes as eventually it will convert to a PDF. How do I set a specific view to always remain a size, and when I show it in a sheet it will display in the same format, just scaled down?
This is the view I am trying to maintain the size and aspect ratio of:
ScrollView(.vertical){
VStack(spacing: 0){
ForEach(processor.matchEvents){event in
if(event.team.home == true){
HomeArm(team: event.team, event: event) //Same as AwayArm, just changes text
}
else{
AwayArm(team: event.team, event: event)
}
}
}
}
struct AwayArm: View {
#ObservedObject var team: Team
var event: MatchEvent
var body: some View {
let mins: String = String(event.time)
ZStack {
Rectangle()
.frame(width: 5, height: 200)
HStack(spacing: 0){
Spacer()
Text("\(mins)'")
.font(.title)
.frame(width: 60, height: 60, alignment: .center)
.foregroundColor(team.teamTextColor)
.background(team.teamColor)
.clipShape(Circle())
.modifier(RoundedEdge(width: 5, color: .black, cornerRadius: 40))
Rectangle()
.frame(width: 80, height: 5)
VStack{
Text(event.event)
.font(.title)
Text("Event Details")
.font(.title2)
Text("\(event.player.shirtNumber) - \(event.player.playerName)")
.font(.title3)
}.padding()
.foregroundColor(.white)
.frame(width: 250, height: 150)
.background(LinearGradient(gradient: Gradient(colors: [Color(UIColor.lightGray), .gray]), startPoint: .top, endPoint: .bottom))
.cornerRadius(25)
.modifier(RoundedEdge(width: 5, color: .black, cornerRadius: 30))
}
.padding(.top)
.padding(.trailing, 35)
}
}
}
I want this to be displayed inside a sheet, but maintain the correct sizings and aspect ratio:
HStack{
ScrollView(.vertical){
MatchReportView()
}
Text("")
}
Any help on keeping this the same size, all the time, would be appreciated

How to present accurate star rating using SwiftUI?

I need to present accurate star rating like 3.1, 4.8 using SwiftUI.
The desired result should be like this:
Your general approach is good, but I believe it can be made much simpler.
The below code adapts to whatever size it is placed in (so if you want a specific size, put it in a frame).
Note that the internal ZStack isn't required in iOS 14, but GeometryReader still doesn't document its layout behavior (except in an Xcode 12 release note), so this makes it explicit.
struct StarsView: View {
var rating: CGFloat
var maxRating: Int
var body: some View {
let stars = HStack(spacing: 0) {
ForEach(0..<maxRating, id: \.self) { _ in
Image(systemName: "star.fill")
.resizable()
.aspectRatio(contentMode: .fit)
}
}
stars.overlay(
GeometryReader { g in
let width = rating / CGFloat(maxRating) * g.size.width
ZStack(alignment: .leading) {
Rectangle()
.frame(width: width)
.foregroundColor(.yellow)
}
}
.mask(stars)
)
.foregroundColor(.gray)
}
}
This draws all the stars in gray, and then creates a yellow rectangle of the correct width, masks it to the stars, and draws that on top as an overlay. Overlays are automatically the same size as the view they're attached to, so you don't need all the frames to make the sizes match they way you do with a ZStack.
After spending some time I did found a solution.
struct StarsView: View {
let rating: CGFloat
let maxRating: CGFloat
private let size: CGFloat = 12
var body: some View {
let text = HStack(spacing: 0) {
Image(systemName: "star.fill")
.resizable()
.frame(width: size, height: size, alignment: .center)
Image(systemName: "star.fill")
.resizable()
.frame(width: size, height: size, alignment: .center)
Image(systemName: "star.fill")
.resizable()
.frame(width: size, height: size, alignment: .center)
Image(systemName: "star.fill")
.resizable()
.frame(width: size, height: size, alignment: .center)
Image(systemName: "star.fill")
.resizable()
.frame(width: size, height: size, alignment: .center)
}
ZStack {
text
HStack(content: {
GeometryReader(content: { geometry in
HStack(spacing: 0, content: {
let width1 = self.valueForWidth(geometry.size.width, value: rating)
let width2 = self.valueForWidth(geometry.size.width, value: (maxRating - rating))
Rectangle()
.frame(width: width1, height: geometry.size.height, alignment: .center)
.foregroundColor(.yellow)
Rectangle()
.frame(width: width2, height: geometry.size.height, alignment: .center)
.foregroundColor(.gray)
})
})
.frame(width: size * maxRating, height: size, alignment: .trailing)
})
.mask(
text
)
}
.frame(width: size * maxRating, height: size, alignment: .leading)
}
func valueForWidth(_ width: CGFloat, value: CGFloat) -> CGFloat {
value * width / maxRating
}
}
Usage:
StarsView(rating: 2.4, maxRating: 5)
Another simple solution that enables star fill level through clipping. You can use it with following API:
/// Pass in rating as a Binding or provide a constant
StarRating(rating: .constant(1.6), maxRating: 5)
.font(.title2) // Provide font for sizing
Which looks like:
Implementation:
struct StarRating: View {
struct ClipShape: Shape {
let width: Double
func path(in rect: CGRect) -> Path {
Path(CGRect(x: rect.minX, y: rect.minY, width: width, height: rect.height))
}
}
#Binding var rating: Double
let maxRating: Int
init(rating: Binding<Double>, maxRating: Int) {
self.maxRating = maxRating
self._rating = rating
}
var body: some View {
HStack(spacing: 0) {
ForEach(0..<maxRating, id: \.self) { _ in
Text(Image(systemName: "star"))
.foregroundColor(.blue)
.aspectRatio(contentMode: .fill)
}
}.overlay(
GeometryReader { reader in
HStack(spacing: 0) {
ForEach(0..<maxRating, id: \.self) { _ in
Image(systemName: "star.fill")
.foregroundColor(.blue)
.aspectRatio(contentMode: .fit)
}
}
.clipShape(
ClipShape(width: (reader.size.width / CGFloat(maxRating)) * CGFloat(rating))
)
}
)
}
}

SwiftUI different children alignment and GeometryReader

I want to transform a web app into an iOS app and I am struggling with alignments and the GeometryReader().
This a a screenshot from my original web app template:
This is my current version in SwiftUI:
With the related code:
struct PileRow: View {
var body: some View {
HStack() {
VStack(alignment: .leading, spacing: 3) {
Text("Adobe".uppercased())
Bar(backgroundColor: Color.gray, width: 200)
Bar(backgroundColor: Color.black, width: 200)
HStack(alignment: .top, spacing: 3) {
Text("EUR 1,00")
.fontWeight(.light)
Text(" / ")
.fontWeight(.light)
.foregroundColor(Color.gray)
Text("35,69")
.fontWeight(.light)
.foregroundColor(Color.gray)
}
Image("dots")
.resizable()
.scaledToFit()
.frame(width: 20)
}
.font(.system(size: 14))
.padding()
}
.background(Color.white)
.cornerRadius(15)
.shadow(radius: 6, x: 5, y: 5)
}
}
struct Bar: View {
var backgroundColor: Color
var width: CGFloat
var body: some View {
Rectangle()
.fill(backgroundColor)
.frame(width: width, height: 3)
.cornerRadius(1.5)
}
}
I want:
the dots image to be centered in the parent VStack (like margin: 0 auto; in CSS)
the outer HStack to be 100% width with a left and right margin of 10px (like width: 100%; margin: auto 10px; in CSS)
The special challenge I want to realize is, to give the second Bar() view a percentage with (or some equivalent) to have a with of 1.00/35.69*100%.
In the apple documentation of Swift UI I found:
GeometryReader
A container view that defines its content as a function of its own size and coordinate space. 3
With a change from:
Bar(backgroundColor: Color.gray, width: 200)
Bar(backgroundColor: Color.black, width: 200)
to
GeometryReader { g in
Bar(backgroundColor: Color.gray, width: g.size.width)
}
.frame(height: 3)
GeometryReader { g in
Bar(backgroundColor: Color.black, width: g.size.width*(1/35.69))
}
.frame(height: 3)
it is going close to what I look for:
I don't understand what is happening here. My purpose was to give the Bar views a size relationship to the parent. But the parent itself gets a different width and hight that I need to fix with a frame with a height of 3 and the parent HStack fills the entire screen.
When I try this:
GeometryReader { g in
Bar(backgroundColor: Color.gray, width: g.size.width)
Bar(backgroundColor: Color.black, width: g.size.width*(1/35.69))
}
I am completely out, because the bars get somehow vstacked:
I think this is what you want:
import SwiftUI
struct ContentView: View {
var body: some View {
GeometryReader { g in
HStack {
VStack(alignment: .leading, spacing: 3) {
Text("Adobe".uppercased())
Bar(backgroundColor: Color.gray, width: g.size.width)
Bar(backgroundColor: Color.black, width: g.size.width*(1/35.69))
HStack(alignment: .top, spacing: 3) {
Text("EUR 1,00")
.fontWeight(.light)
Text(" / ")
.fontWeight(.light)
.foregroundColor(Color.gray)
Text("35,69")
.fontWeight(.light)
.foregroundColor(Color.gray)
}
HStack {
Spacer()
Image(systemName: "dots")
.resizable()
.scaledToFit()
.frame(width: 20)
Spacer()
}
}
.font(.system(size: 14))
.padding()
}
.background(Color.white)
.cornerRadius(15)
.shadow(radius: 6, x: 5, y: 5)
}
.padding()
}
}
struct Bar: View {
var backgroundColor: Color
var width: CGFloat
var body: some View {
Rectangle()
.fill(backgroundColor)
.frame(width: width, height: 3)
.cornerRadius(1.5)
}
}