This question already has answers here:
How to scroll List programmatically in SwiftUI?
(10 answers)
Closed 8 months ago.
I am trying to make an automatic custom paged scroll view but I have run into an issue. I specified in my code for every 5 seconds to add an increment of 1 and go to the next page and organized text to each card with Identifiable. This is not happening in the preview. Instead, it is refusing to move and it is wanting to move only to the first card. When I put a fixed string value and remove the array that I have made, it works. I have tried to use other alternatives like an enum and a fixed array embedded in the view but none of that worked. Please review my code below…
struct TestView: View {
private var amount = 1
private let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
#State private var currentIndex = 0
var item: Item = items[0]
var body: some View {
ZStack {
ScrollViewReader { scrollProxy in
SnappingScrollView(.horizontal, decelerationRate: .fast, showsIndicators: false) {
ForEach(items) { item in
ForEach(0..<amount) { selection in
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(Color.black.opacity(0.2))
.blur(radius: 7)
.frame(maxWidth: 350, maxHeight: 300, alignment: .center)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.2), radius: 20, x: 0, y: 0)
.offset(x: 15, y: 50)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
Rectangle()
.stroke(lineWidth: 50)
.foregroundColor(.gray)
.blur(radius: 100)
.frame(maxWidth: 350, maxHeight: 300, alignment: .center)
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.3), radius: 1)
.offset(x: 15, y: 50)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
VStack {
Text(item.title)
.font(.title2)
.fontWeight(.bold)
.padding(.bottom, 10)
Text(item.description)
.font(.system(size: 16))
Image(item.image)
.resizable()
.frame(width: 90, height: 90)
.cornerRadius(25)
.padding(.bottom, -30)
.padding(.top)
Text("\(selection)")
.opacity(0)
}
.frame(width: 300)
.multilineTextAlignment(.center)
.offset(y: 20)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
.padding(.leading, 25)
.padding(.top, 70)
}
}
.frame(width: 400, height: 500)
.offset(x: 12)
.id(selection)
.scrollSnappingAnchor(.bounds)
}
}
}
.onReceive(timer) { _ in
currentIndex = currentIndex < amount-1 ? currentIndex + 1 : 0
withAnimation {
scrollProxy.scrollTo(currentIndex) // scroll to next .id
}
}
}
}
}
}
The Identifiable array
struct Item: Identifiable {
var id = UUID()
var title: String
var description: String
var image: String
}
var items = [
Item(title: "Welcome to Mathematically!", description: "It's a pleasure to have you. From creating a playground to competing in the daily challenges, there will be wonderful days of math waiting for you.", image: "Mathematically"),
Item(title: "An award winning app.", description: "Mathematically was submitted to the WWDC22 Swift Student Challenge and was accepted among the 350 accepted apps world-wide.", image: "WWDC22"),
Item(title: "Need a Walkthrough?", description: "No need to be intimidated. Take the walkthrough if you need to familiarize yourself about the game.", image: "Arrows"),
Item(title: "Mathematically+", description: "The ultimate premium Mathematically experience, ad-free.", image: "Mathematically+"),
]
You're almost there .. you just don't do anything with your currentIndex when it changes. Introduce a ScrollViewReader and an .id so you can scroll to the respective position in your view:
struct ContentView: View {
private let amount = 4
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
#State private var currentIndex = 0
var body: some View {
ScrollViewReader { scrollProxy in // needed to scroll programmatically
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(0..<amount) { item in
GeometryReader { geometry in
ZStack {
Rectangle()
.foregroundColor(.gray)
.frame(maxWidth: 350, maxHeight: 300, alignment: .center)
.cornerRadius(16)
.offset(x: 15, y: 50)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
VStack {
Text("Hello World!")
.font(.title2)
.bold()
.padding(.bottom, 10)
Text("Item \(item)")
}
.frame(width: 300)
.rotation3DEffect(Angle(degrees: (Double(geometry.frame(in: .global).minX) - 30) / -20), axis: (x: 0, y: 1.0, z: 0))
.padding(.leading, 25)
.padding(.top, 70)
}
}
.frame(width: 400, height: 500)
.offset(x: 10)
.id(item) // define id here
}
}
}
.onReceive(timer) { _ in
currentIndex = currentIndex < amount-1 ? currentIndex + 1 : 0
withAnimation {
scrollProxy.scrollTo(currentIndex) // scroll to next .id
}
}
}
}
}
Related
Here is what I've done, but the problem is with Text background. It can be implemented on white background by setting the Text's background to white as well, but in case of image background it stays "strikedthrough". You can find a source code below where I tried to make it as close to the result as possible. How it could be resolved?
struct CustomTextField: View {
let placeholder: String
#Binding var text: String
var body: some View {
TextField("", text: $text)
.placeholder(when: $text.wrappedValue.isEmpty,
alignment: .leading,
placeholder: {
Text(placeholder)
.foregroundColor(.gray)
.font(.system(size: 20))
.padding(.leading, 15)
})
.foregroundColor(.gray)
.font(.system(size: 20))
.padding(EdgeInsets(top: 15, leading: 10, bottom: 15, trailing: 10))
.background {
ZStack {
RoundedRectangle(cornerRadius: 5)
.stroke(.gray, lineWidth: 1)
Text(placeholder)
.foregroundColor(.gray)
.padding(2)
.font(.caption)
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading)
.offset(x: 20, y: -10)
}
}
}
}
Here is a solution using .trim on two RoundedRectangles based on the length of the label text, which should give you the result you want:
struct CustomTextField: View {
let placeholder: String
#Binding var text: String
#State private var width = CGFloat.zero
#State private var labelWidth = CGFloat.zero
var body: some View {
TextField(placeholder, text: $text)
.foregroundColor(.gray)
.font(.system(size: 20))
.padding(EdgeInsets(top: 15, leading: 10, bottom: 15, trailing: 10))
.background {
ZStack {
RoundedRectangle(cornerRadius: 5)
.trim(from: 0, to: 0.55)
.stroke(.gray, lineWidth: 1)
RoundedRectangle(cornerRadius: 5)
.trim(from: 0.565 + (0.44 * (labelWidth / width)), to: 1)
.stroke(.gray, lineWidth: 1)
Text(placeholder)
.foregroundColor(.gray)
.overlay( GeometryReader { geo in Color.clear.onAppear { labelWidth = geo.size.width }})
.padding(2)
.font(.caption)
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading)
.offset(x: 20, y: -10)
}
}
.overlay( GeometryReader { geo in Color.clear.onAppear { width = geo.size.width }})
.onChange(of: width) { _ in
print("Width: ", width)
}
.onChange(of: labelWidth) { _ in
print("labelWidth: ", labelWidth)
}
}
}
Here is my version of the TextField.
struct TextInputField: View {
let placeHolder: String
#Binding var textValue: String
var body: some View {
ZStack(alignment: .leading) {
Text(placeHolder)
.foregroundColor(Color(.placeholderText))
.offset(y: textValue.isEmpty ? 0 : -25)
.scaleEffect(textValue.isEmpty ? 1: 0.8, anchor: .leading)
TextField("", text: $textValue)
}
.padding(.top, textValue.isEmpty ? 0 : 15)
.frame(height: 52)
.padding(.horizontal, 16)
.overlay(RoundedRectangle(cornerRadius: 12).stroke(lineWidth: 1).foregroundColor(.gray))
.animation(.default)
}
}
The above code is to create a CustomTextField named TextInputField. If you want to use the about component
struct ContentView: View {
#State var itemName: String = ""
var body: some View {
TextInputField(placeHolder: "Item Name": textValue: $itemName)
}
}
I'm using #ChrisR's answer as a base for my answer, so instead of doing all that calculation with two RoundedRectangles and label's width; you can position the Text on top and give it a background matching the app's background color
struct FloatingTitleTextField: View {
let placeholder: String
#Binding var text: String
var body: some View {
TextField("Placeholder", text: $text)
.foregroundColor(.gray)
.font(.system(size: 20))
.padding(EdgeInsets(top: 15, leading: 10, bottom: 15, trailing: 10))
.background {
ZStack {
RoundedRectangle(cornerRadius: 5)
.stroke(.black, lineWidth: 1)
Text(placeholder)
.foregroundColor(.gray)
.padding(2)
.background()
.frame(maxWidth: .infinity,
maxHeight: .infinity,
alignment: .topLeading)
.offset(x: 20, y: -10)
}
}
}
}
When calling the textfield you do it like this
FloatingTitleTextField(placeholder: "Placeholder", text: $text)
I also found this article very helpful
I've created a button with three animating dots using SwiftUI. When the screen geometry changes somehow (device rotation, keyboard open, etc.) the animation gets messed up. See the attached video. How can I get the circles to stay in their position relative to the button?
This is my code:
struct ContentView: View {
var body: some View {
Button(action: {}, label: {
AnimatedDot().frame(minWidth: 200, maxWidth: .infinity, minHeight: 20, maxHeight: 20)
}).foregroundColor(Color.blue)
.padding()
.background(Color.gray)
.cornerRadius(10.0)
.frame(minWidth: 0, maxWidth: 350)
}
}
struct AnimatedDot: View {
#State private var shouldAnimate = false
var body: some View {
HStack {
Circle()
.fill(Color.white)
.frame(width: 15, height: 15)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.3).repeatForever())
Circle()
.fill(Color.white)
.frame(width: 15, height: 15)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.3).repeatForever().delay(0.3))
Circle()
.fill(Color.white)
.frame(width: 15, height: 15)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.3).repeatForever().delay(0.6))
}
.onAppear {
self.shouldAnimate = true
}
}
}
Here is fixed body - join animation to state value.
Tested with Xcode 12.1 / iOS 14.1
var body: some View {
HStack {
Circle()
.fill(Color.white)
.frame(width: 15, height: 15)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.3).repeatForever(), value: shouldAnimate)
Circle()
.fill(Color.white)
.frame(width: 15, height: 15)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.3).repeatForever().delay(0.3), value: shouldAnimate)
Circle()
.fill(Color.white)
.frame(width: 15, height: 15)
.scaleEffect(shouldAnimate ? 1.0 : 0.5)
.animation(Animation.easeInOut(duration: 0.3).repeatForever().delay(0.6), value: shouldAnimate)
}
.onAppear {
self.shouldAnimate = true
}
}
I'm currently trying to handle SwiftUI by following a tutorial, but somehow I can't solve one issue:
I created another View, namely my HomeView.swift - this file contains the following code:
import SwiftUI
struct Home: View {
var menu = menuData
#State var show = false
var body: some View {
ZStack {
ZStack(alignment: .topLeading) {
Button(action: { self.show.toggle() }) {
HStack {
Spacer()
Image(systemName: "list.dash")
.foregroundColor(Color("primary"))
}
.padding(.trailing, 20)
.frame(width: 90, height: 60)
.background(Color.white)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
Spacer()
}
ZStack(alignment: .topTrailing) {
Button(action: { self.show.toggle() }) {
HStack {
Spacer()
Image(systemName: "map.fill")
.foregroundColor(Color("primary"))
}
.padding(.trailing, 20)
.frame(width: 44, height: 44)
.background(Color.white)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
Spacer()
}
MenuView(show: $show)
}
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
}
}
struct MenuRow: View {
var text: String?
var image: String?
var body: some View {
HStack {
Image(systemName: image ?? "")
.foregroundColor(Color("third"))
.frame(width: 32, height: 32, alignment: .trailing)
Text(text ?? "")
.font(Font.custom("Helvetica Now Display Bold", size: 15))
.foregroundColor(Color("primary"))
Spacer()
}
}
}
struct Menu: Identifiable {
var id = UUID()
var title: String
var icon: String
}
let menuData = [
Menu(title: "My Account", icon: "person.crop.circle.fill"),
Menu(title: "Reservations", icon: "house.fill"),
Menu(title: "Sign Out", icon: "arrow.uturn.down")
]
struct MenuView: View {
var menu = menuData
#Binding var show: Bool
var body: some View {
VStack(alignment: .leading, spacing: 20) {
ForEach(menu) { item in
MenuRow(text: item.title, image: item.icon)
}
Spacer()
}
.padding(.top, 20)
.padding(30)
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.white)
.cornerRadius(30)
.padding(.trailing, 60)
.shadow(radius: 20)
.rotation3DEffect(Angle(degrees: show ? 0 : 60), axis: (x: 0, y: 10, z: 0))
.animation(.default)
.offset(x: show ? 0 : -UIScreen.main.bounds.width)
.onTapGesture {
self.show.toggle()
}
}
}
As you can see, right in the beginning, inside of my Home struct, I tried to align two ZStacks - one .topLeading and one .topTrailing. Reading the docs, this should change its position, but somehow it doesn't. Both stack stay centered.
BTW I haven't particularly touched ContenView.swift yet.
Actually, for either inner ZStack, you need to set frames. This can make them reach edges.
ZStack{
ZStack{
Button(action: { self.show.toggle() }) {
HStack {
Spacer()
Image(systemName: "list.dash")
.foregroundColor(Color("primary"))
}
.padding(.trailing, 20)
.frame(width: 90, height: 60)
.background(Color.white)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
ZStack{
Button(action: { self.show.toggle() }) {
HStack {
Spacer()
Image(systemName: "map.fill")
.foregroundColor(Color("primary"))
}
.padding(.trailing, 20)
.frame(width: 44, height: 44)
.background(Color.white)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
MenuView(show: $show)
}
struct Home: View {
var menu = menuData
#State var show = false
var body: some View {
ZStack {
VStack {
HStack {
Button(action: { self.show.toggle() }) {
HStack {
Spacer()
Image(systemName: "list.dash")
.foregroundColor(Color("primary"))
}
.padding(.trailing, 20)
.frame(width: 90, height: 60)
.background(Color.white)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
Spacer()
Button(action: { self.show.toggle() }) {
HStack {
Spacer()
Image(systemName: "map.fill")
.foregroundColor(Color("primary"))
}
.padding(.trailing, 20)
.frame(width: 44, height: 44)
.background(Color.white)
.cornerRadius(30)
.shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
}
}
Spacer()
}
MenuView(show: $show)
}
}
}
Is this the layout that you are looking for? With VStack and HStack you can align the views to the top and on both edges
I need to make such a simple thing, but can't figure out how.
So I need to create a View which I already have inside another view. Here how it looks like now ↓
Here's my button:
struct CircleButton: View {
var body: some View {
Button(action: {
self
}, label: {
Text("+")
.font(.system(size: 42))
.frame(width: 57, height: 50)
.foregroundColor(Color.white)
.padding(.bottom, 7)
})
.background(Color(#colorLiteral(red: 0.4509803922, green: 0.8, blue: 0.5490196078, alpha: 1)))
.cornerRadius(50)
.padding()
.shadow(color: Color.black.opacity(0.15),
radius: 3,
x: 0,
y: 4)
}
}
Here's a view which I want to place when I tap the button ↓
struct IssueCardView: View {
var body: some View {
ZStack (alignment: .leading) {
Rectangle()
.fill(Color.white)
.frame(height: 50)
.shadow(color: .black, radius: 20, x: 0, y: 4)
.cornerRadius(8)
VStack (alignment: .leading) {
Rectangle()
.fill(Color(#colorLiteral(red: 0.6550863981, green: 0.8339114785, blue: 0.7129291892, alpha: 1)))
.frame(width: 30, height: 8)
.cornerRadius(8)
.padding(.horizontal, 10)
Text("Some text on card here")
.foregroundColor(Color(UIColor.dark.main))
.font(.system(size: 14))
.fontWeight(.regular)
.padding(.horizontal, 10)
}
}
}
}
Here's a view where I want to place this IssueCardView ↓. Instead of doing it manually like now I want to generate this View with button.
struct TaskListView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false, content: {
VStack (alignment: .leading, spacing: 8) {
**IssueCardView()
IssueCardView()
IssueCardView()
IssueCardView()
IssueCardView()**
}
.frame(minWidth: 320, maxWidth: 500, minHeight: 500, maxHeight: .infinity, alignment: .topLeading)
.padding(.horizontal, 0)
})
}
}
Although it works with scrollView and stack, You should use a List for these kind of UI issues (as you already mentioned in the name of TaskListView)
struct TaskListView: View {
typealias Issue = String // This is temporary, because I didn't have the original `Issue` type
#State var issues: [Issue] = ["Some issue", "Some issue"]
var body: some View {
ZStack {
List(issues, id: \.self) { issue in
IssueCardView()
}
CircleButton {
self.issues += ["new issue"]
}
}
}
}
I have added let action: ()->() to CircleButton. So I can pass the action to it.
I just upgrade to Xcode 11 Beta 5 and update my SwiftUI project.
In previous version I wanted to use PresentationLink component to show up a modal. I had the same problem than now, the modal has only shown once. I thought it was a bug as I saw in other SO posts. So I tried my chance by upgrading to Beta 5 but still no luck.
I noticed that this behaviour seems to be caused by wrapping in a ScrollView component. If I delete the ScrollView component everything works fine as expected.
Here's the code:
struct HomeList : View {
var listViewItems = listViewItemsData
#State var show = false
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text("Project title").font(.largeTitle).fontWeight(.heavy)
Text("Project subtitle").foregroundColor(Color.gray)
}
Spacer()
}.padding(.top, 78).padding(.leading, 60)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(listViewItems) { item in
GeometryReader { geometry in
Button(action: { self.show.toggle()}) {
ListView(title: item.title, image: item.image, color: item.color, destination: item.destination)
.rotation3DEffect(Angle(degrees: Double((geometry.frame(in: .global).minX - 30) / -30)), axis: (x: 0, y: 10, z: 0))
.sheet(isPresented: self.$show, content: { InformationView() })
}
}.frame(width: 246, height: 360)
}
}.padding(30)
Spacer()
}.frame(width: UIScreen.main.bounds.width, height: 480)
Spacer()
}
}
}
To summarize, without ScrollView wrapper the Modal behaviour works as expected.
I would like to know if there is a solution / workaround ? Or I just have to wait a release :)
Edit from answer:
struct HomeList : View {
var listViewItems = listViewItemsData
#State var show = false
#State var view: AnyView = AnyView(Text(""))
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text("Project title").font(.largeTitle).fontWeight(.heavy)
Text("Project subtitle").foregroundColor(Color.gray)
}
Spacer()
}.padding(.top, 78).padding(.leading, 60)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(listViewItems) { item in
GeometryReader { geometry in
Button(action: {
self.show.toggle()
self.view = item.destination
}) {
ListView(title: item.title, image: item.image, color: item.color, destination: item.destination)
.rotation3DEffect(Angle(degrees: Double((geometry.frame(in: .global).minX - 30) / -30)), axis: (x: 0, y: 10, z: 0))
}
}.frame(width: 246, height: 360)
}
}.padding(30)
Spacer()
}.frame(width: UIScreen.main.bounds.width, height: 480)
.sheet(isPresented: self.$show, content: { self.view })
Spacer()
}
}
}
This is the same issue as https://stackoverflow.com/a/57087399/3179416
Just move your .sheet outside of your ForEach.
import SwiftUI
struct Testing : View {
var listViewItems: [Int] = [1, 2, 3]
#State var show = false
var body: some View {
VStack {
HStack {
VStack(alignment: .leading) {
Text("Project title").font(.largeTitle).fontWeight(.heavy)
Text("Project subtitle").foregroundColor(Color.gray)
}
Spacer()
}.padding(.top, 78).padding(.leading, 60)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 30) {
ForEach(listViewItems, id: \.self) { item in
GeometryReader { geometry in
Button(action: { self.show.toggle()}) {
Text("Button")
.rotation3DEffect(Angle(degrees: Double((geometry.frame(in: .global).minX - 30) / -30)), axis: (x: 0, y: 10, z: 0))
}
}.frame(width: 246, height: 360)
}
}.padding(30)
Spacer()
}.frame(width: UIScreen.main.bounds.width, height: 480)
.sheet(isPresented: self.$show, content: { Text("Modal") })
Spacer()
}
}
}