I need to create a list view with item like a square on a scrollview.
But I need to make the scroll only work to one item to another.
I could see the article allowing to set up this system on a HStack but I can't adapt it on a VStack
This article : https://levelup.gitconnected.com/snap-to-item-scrolling-debccdcbb22f
If someone has an idea to help me in this research?
I join an example of scrollview with the square :
struct ScrollingSnapped: View {
var colors: [Color] = [.blue, .green, .red, .orange, .gray, .brown, .yellow, .purple]
var body: some View {
ScrollView {
VStack(alignment: .trailing, spacing: 30) {
ForEach(0..<colors.count) { index in
colors[index]
.frame(width: 250, height: 250, alignment: .center)
.cornerRadius(10)
}
}
}
}
}
struct ScrollingSnapped_Previews: PreviewProvider {
static var previews: some View {
ScrollingSnapped()
}
}
I tried to work with a ScrollViewReader that allows to scrollTo. It works when clicking on a button but I can't get it to work when scrolling :
import SwiftUI
struct ScrollingSnapped: View {
var colors: [Color] = [.blue, .green, .red, .orange, .gray, .brown, .yellow, .purple]
var body: some View {
ScrollViewReader { proxy in
ScrollView {
Button {
proxy.scrollTo(4, anchor: .top)
} label: {
Text("Scroll to square 5")
}
VStack(alignment: .trailing, spacing: 30) {
ForEach(0..<colors.count) { index in
colors[index]
.frame(width: 250, height: 250, alignment: .center)
.cornerRadius(10)
.id(index)
}
}
}
}
}
}
struct ScrollingSnapped_Previews: PreviewProvider {
static var previews: some View {
ScrollingSnapped()
}
}
Related
I have encountered this problem during the Swift programming. I have this long frame that I created, here it is.
The main idea that this frame will be used in a horizontal Scroll View in different view, like this. It will be opening different view on tap.
Here's the catch. If we want to transition to different view, we need NavigationLink. In order to work NavigationLink needs NavigationView. When we add our LongFrame in NavigationView, this happens
If we tap on it, it will display View, but in small frame
And If we, for example, add our LongFrameScrollView somewhere, It won't even show up sometimes
I will provide the code here. My guess that should be connected to .frame, but without this line of code I can't create this frame(.
// FRAME ITSELF
import SwiftUI
struct LongFrameView: View {
var body: some View {
NavigationView {
NavigationLink {
PlayerView()
} label: {
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [Color(red: 0.268, green: 0.376, blue: 0.587), Color(red: 0.139, green: 0.267, blue: 0.517)]),
startPoint: .leading,
endPoint: .trailing))
.frame(width: 310, height: 62)
.cornerRadius(8)
HStack {
Image("mountains")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 70, height: 62)
.cornerRadius(8, corners: [.topLeft, .bottomLeft])
VStack(alignment: .leading) {
Text("Sense of anxiety")
.font(.custom("Manrope-Bold", size: 14))
.foregroundColor(.white)
Text("11 MIN")
.font(.custom("Manrope-Medium", size: 12))
.foregroundColor(.white)
}
Spacer()
}
}
.frame(width: 310, height: 62)
}
}
}
}
struct LongFrameView_Previews: PreviewProvider {
static var previews: some View {
LongFrameView()
}
}
// MARK: - WITH THIS CODE WE CAN DEFINE WHERE CORNER RADIUS WILL BE CHANGED OR NOT. DO NOT MODIFY
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
// SCROLL VIEW WITH FRAMES
import SwiftUI
struct LongFrameScrollView: View {
let rows = Array(repeating: GridItem(.fixed(60), spacing: 10, alignment: .leading), count: 2)
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 10) {
// PLACEHOLDER UNTIL API IS READY
LongFrameView()
LongFrameView()
LongFrameView()
LongFrameView()
}
}
.padding([.horizontal, .bottom], 10)
}
}
struct LongFrameScrollView_Previews: PreviewProvider {
static var previews: some View {
LongFrameScrollView()
}
}
NavigationView should be added as the first/top view. So embed your ScrollView with NavigationView inside LongFrameScrollView and removed it from LongFrameView. Inside LongFrameView you just need NavigationLink.
NavigationView {
ScrollView(.horizontal, showsIndicators: false) {
LazyHGrid(rows: rows, spacing: 10) {
// PLACEHOLDER UNTIL API IS READY
LongFrameView()
LongFrameView()
LongFrameView()
LongFrameView()
}
}
.padding([.horizontal, .bottom], 10)
}
I created a SampleView where a Button is wrapped in a ZStack, and the whole ZStack is animated on tap of the Button. It looks like below:
struct SampleView: View {
#State private var toggle: Bool = false
var body: some View {
ZStack {
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
}
.padding(10)
.background(.red)
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
// ZStack {
SampleView()
// }
}
}
There's a bunch of other views that I want to present, so I simply wrapped SampleView inside a VStack (or ZStack). Now, the animation started to break:
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
SampleView()
}
}
}
I noticed that I can work around this behavior by wrapping the button action with withAnimation.
withAnimation(.easeInOut(duration: 1)) {
toggle.toggle()
}
Still, I wonder what's going on here. Is this a bug?
This certainly looks like a bug. The Text label of the button is not getting the .animation() value applied to it, but the background is.
Here is another way to fix it. Apply the .animation() modifier to the Text label as well:
struct SampleView: View {
#State private var toggle: Bool = false
var body: some View {
ZStack {
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
.animation(.easeInOut(duration: 1), value: toggle) // here
}
.padding(10)
.background(.red)
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
More Evidence of the Bug
In this example, we have two buttons in a VStack that move together. This works correctly in iOS 15.5, but it breaks in iOS 16. The animation breaks for the button that is being pressed, but it works for the other button.
struct ContentView: View {
#State private var toggle: Bool = false
var body: some View {
VStack {
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
}
.padding(10)
.background(.red)
Button {
toggle.toggle()
} label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
}
.padding(10)
.background(.blue)
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
The same fixes apply here: add the .animation() modifier to the Text label of each button, or wrap the button action with withAnimation { }.
I think it's a bug too. I found a temporary workaround.
You can try onTapGesture.
I fixed the weird animation.
struct SampleView: View {
#State private var toggle: Bool = false
var body: some View {
ZStack {
// Button {
// toggle.toggle()
// } label: {
Text("Tap here to move!")
.font(.system(size: 20, weight: .black))
.foregroundColor(.black)
// }
.padding(10)
.background(.red)
.onTapGesture {
toggle.toggle()
}
}
.offset(x: 0, y: toggle ? 0 : 200)
.animation(.easeInOut(duration: 1), value: toggle)
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
ZStack {
SampleView()
}
}
}
I have created a custom picker that is larger than the native SwiftUI picker. This picker is being used on an iPad which is why I need it larger than usual. When I use the picker, I can't tap on the padding portions. The picker is only opened when I tap directly in the horizontal center of my picker. I have read about using a .frame() modifier to change the tappable area of things like buttons, but that does not seem to work here when I try to add a frame modifier to the base Picker itself. Here is an image of the additional area (in orange) I would like to make tappable
And here is my code:
import SwiftUI
struct CustomPickerStyle: ViewModifier {
var labelText: String
var width: CGFloat
func body(content: Content) -> some View {
Menu {
content
} label: {
HStack {
if let labelText = labelText {
Text(labelText)
.font(.title2)
.fontWeight(.bold)
Spacer()
Image(systemName: "triangle.fill")
.resizable()
.frame(width: 12, height: 8)
.rotationEffect(.degrees(180))
}
}
}
.frame(maxWidth: width, alignment: .leading)
.padding()
.background(.white)
.overlay(
RoundedRectangle(cornerRadius: 3)
.stroke(.gray, lineWidth: 2)
)
}
}
extension View {
func customPickerStyle(labelText: String, width: CGFloat) -> some View {
self.modifier(CustomPickerStyle(labelText: labelText, width: width))
}
}
struct CustomPicker: View {
enum Flavor: String, CaseIterable, Identifiable {
case chocolate, vanilla, strawberry
var id: Self { self }
}
#State private var selectedFlavor: Flavor = .chocolate
var body: some View {
Picker("Flavor", selection: $selectedFlavor) {
Text("Chocolate").tag(Flavor.chocolate)
Text("Vanilla").tag(Flavor.vanilla)
Text("Strawberry").tag(Flavor.strawberry)
}
.customPickerStyle(labelText: selectedFlavor.rawValue, width: 200)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
CustomPicker()
}
}
Just move your padding and background stylings directly inside the label:
struct CustomPickerStyle: ViewModifier {
var labelText: String
var width: CGFloat
func body(content: Content) -> some View {
Menu {
content
} label: {
HStack {
if let labelText = labelText {
Text(labelText)
.font(.title2)
.fontWeight(.bold)
Spacer()
Image(systemName: "triangle.fill")
.resizable()
.frame(width: 12, height: 8)
.rotationEffect(.degrees(180))
}
}
.frame(maxWidth: width, alignment: .leading)
.padding()
.background(.white)
.overlay(
RoundedRectangle(cornerRadius: 3)
.stroke(.gray, lineWidth: 2)
)
}
}
}
I am very new to SwiftUI. Maybe I am not completely understanding how #State annotated variables work. As far as I know, when you update their values, if they're within the same View, the screen will get updated too (in this case the background color of the screen). I think I followed exactly the video tutorial (SwiftUI Basics Tutorial) but nonetheless I did not get my code to work. By the way, I'm using Xcode Version 13.2.1, if it's worth mentioning. In my ContentView.swift this is what I have:
//
// ContentView.swift
//
import SwiftUI
struct ContentView: View {
#State private var isNight = false //Variable to be changed when pressing the button
var body: some View {
ZStack {
//The BackgroundView color should change when pressing the button
BackgroundView(topColor: isNight ? .black : .blue,
bottomColor: isNight ? .gray : Color("lightBlue"))
VStack{
CityTextView(cityName: "Cupertino, CA")
MainWeatherStatusView(imageName: "cloud.sun.rain.fill",
temperature: 72)
HStack(spacing: 0){
WeatherView(dayOfWeek: "TUE",
imageName: "cloud.sun.fill",
temperature: 74)
WeatherView(dayOfWeek: "WED",
imageName: "sun.max.fill",
temperature: 30)
WeatherView(dayOfWeek: "THU",
imageName: "wind.snow",
temperature: 5)
WeatherView(dayOfWeek: "FRI",
imageName: "sunset.fill",
temperature: 60)
WeatherView(dayOfWeek: "SAT",
imageName: "snow",
temperature: 74)
}
Button{
isNight.toggle() //Changing this should update my View, right?
} label: {
WeatherButton(title: "Change Day Time",
textColor: .blue,
backgroundColor: .white)
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct WeatherView: View {
var dayOfWeek: String
var imageName: String
var temperature: Int
var body: some View {
VStack{
Text(dayOfWeek)
.font(.system(size: 15, weight: .medium, design: .default))
.foregroundColor(.white)
.padding()
Image(systemName: imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
Text("\(temperature)°")
.font(.system(size: 30, weight: .medium))
.foregroundColor(.white)
Spacer()
}
}
}
struct BackgroundView: View {
var topColor: Color
var bottomColor: Color
var body: some View {
LinearGradient(gradient: Gradient(colors: [.blue, Color("lightBlue")]), startPoint: .topLeading, endPoint: .bottomTrailing)
.edgesIgnoringSafeArea(.all)
}
}
struct CityTextView: View {
var cityName: String
var body: some View {
Text(cityName)
.font(.system(size: 32, weight: .medium, design: .default))
.foregroundColor(.white)
.padding()
}
}
struct MainWeatherStatusView: View {
var imageName: String
var temperature: Int
var body: some View {
VStack(spacing: 8){
Image(systemName: imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 180, height: 180)
Text("\(temperature)°")
.font(.system(size: 70, weight: .medium))
.foregroundColor(.white)
}
}
}
In case you want to copy paste the code and want to get rid of the icons, here's the code without them:
//
// ContentView.swift
//
import SwiftUI
struct ContentView: View {
#State private var isNight = false //VARIABLE TO BE CHANGED WHEN PRESSING BUTTON
var body: some View {
ZStack {
//The BackgroundView color should change when pressing the button
BackgroundView(topColor: isNight ? .black : .blue,
bottomColor: isNight ? .gray : Color("lightBlue"))
VStack{
CityTextView(cityName: "Cupertino, CA")
Button{
isNight.toggle() //CHANGING THIS SHOULD UPDATE MY VIEW, RIGHT?
} label: {
WeatherButton(title: "Change Day Time",
textColor: .blue,
backgroundColor: .white)
}
Spacer()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct WeatherView: View {
var dayOfWeek: String
var imageName: String
var temperature: Int
var body: some View {
VStack{
Text(dayOfWeek)
.font(.system(size: 15, weight: .medium, design: .default))
.foregroundColor(.white)
.padding()
Image(systemName: imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
Text("\(temperature)°")
.font(.system(size: 30, weight: .medium))
.foregroundColor(.white)
Spacer()
}
}
}
struct BackgroundView: View {
var topColor: Color
var bottomColor: Color
var body: some View {
LinearGradient(gradient: Gradient(colors: [.blue, Color("lightBlue")]), startPoint: .topLeading, endPoint: .bottomTrailing)
.edgesIgnoringSafeArea(.all)
}
}
struct CityTextView: View {
var cityName: String
var body: some View {
Text(cityName)
.font(.system(size: 32, weight: .medium, design: .default))
.foregroundColor(.white)
.padding()
}
}
struct MainWeatherStatusView: View {
var imageName: String
var temperature: Int
var body: some View {
VStack(spacing: 8){
Image(systemName: imageName)
.renderingMode(.original)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 180, height: 180)
Text("\(temperature)°")
.font(.system(size: 70, weight: .medium))
.foregroundColor(.white)
}
}
}
In your current BackgroundView code, although you have topColor and bottomColor parameters, you don't actually use them in your view. You probably want to include them in your gradient:
struct BackgroundView: View {
var topColor: Color
var bottomColor: Color
var body: some View {
LinearGradient(gradient: Gradient(colors: [topColor, bottomColor]), startPoint: .topLeading, endPoint: .bottomTrailing)
.edgesIgnoringSafeArea(.all)
}
}
Also, make sure you have your custom colors (like Color("lightBlue")) in your asset catalogue.
I have a simple ForEach showing 5 or 10 Text views according to the expanded flag. everything is wrapped in a horizontal ScrollView because the text can be very long.
The animation when expanding the group looks fine, but when collapsing the group there is a small bouncing, the views goes up and down. This does not happen if I remove the ScrollView. Any idea what could cause this bouncing?
struct ContentView: View {
#State var expanded = false
let colors: [Color] = [.red, .green, .blue, .orange, .blue, .brown, .cyan, .gray, .indigo, .mint]
var body: some View {
VStack {
ScrollView(.horizontal) {
VStack(spacing: 20) {
ForEach(colors.prefix(upTo: expanded ? 10 : 5), id: \.self) { color in
Text(color.description.capitalized)
}
}
}
Button("Expand/collapse") {
expanded.toggle()
}
Spacer()
}.animation(.easeIn(duration: 1))
}
}
You need to use value for animation:
struct ContentView: View {
#State var expanded = false
let colors: [Color] = [.red, .green, .blue, .orange, .blue, .red, .green, .blue, .orange, .blue]
var body: some View {
VStack {
ScrollView(.horizontal) {
VStack(spacing: 20) {
ForEach(colors.prefix(upTo: expanded ? 10 : 5), id: \.self) { color in
Text(color.description.capitalized)
}
}
}
Button("Expand/collapse") {
expanded.toggle()
}
Spacer()
}.animation(.easeIn(duration: 1), value: expanded) // <<: Here!
}
}