dadalar / swiftui-cardstackview Goto Github PK
View Code? Open in Web Editor NEWA easy-to-use SwiftUI view for Tinder like cards on iOS, macOS & watchOS.
License: MIT License
A easy-to-use SwiftUI view for Tinder like cards on iOS, macOS & watchOS.
License: MIT License
When I want to click the button to go back to the first one,What do I need to do?
When swiping in speed my card stucks in between. After some debugging, the issue I find is with the DragGesture(). The drag gesture's .onEnded function is not working when swiping in speed.
How do i remove any animations when i swipe the cards ?
I want to remove the animation that the cards are shrink when they are swiped
Your library does a great job with cards that have images, and everything is super and perfect. But unfortunately I ran into a small problem. I use Lottie animations in my card. The problem is that if 50 cards arrive with Lottie animation, I get freezes in the application, especially on the latest cards. As soon as my view changes state (the CardStack disappears from view), my memory returns to normal (50 MB approximately). As soon as I do an unswipe, my memory goes back to 500MB of memory (maybe more). Maybe you can tell me, as the author of this library, what can be done in my case?
When using Podfiles, you have to import as SwiftCardStackUI, while SwiftPM requires importing as CardStack.
Hi there, seems like card views don`t like matchedgeometryeffect modifier, because when I add it, all cards inside of stack loose their offset and swiping animation just breaks
Hi,
Thanks for you help,
How can I Swipe left and right on button click
as when I click Like and Dislike button card should auto Swipe
Your response will be helpful to me
Thanks
Hey my cards are sticking to the left for some reason. How can I fix this?
(Looking at the Restaurant Stuff, ignore the media references)
struct RestCardView: View {
let restaurants: Restaurants
@Environment(\.colorScheme) var colorScheme
var body: some View {
GeometryReader { geo in
let card = (
VStack {
if #available(iOS 15, *) {
AsyncImage(url: restaurants.imageUrl) { image in
imag
![Simulator Screen Shot - iPhone 13 - 2022-03-09 at 11 48 24](https://user-images.githubusercontent.com/81453549/157522210-0892cbd6-6cc7-4d9f-9889-9a3ffdfe2ffe.png)
e
.resizable()
.frame(idealWidth: geo.size.width, idealHeight: geo.size.height)//, maxHeight: 10)
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "photo")
.res
![Simulator Screen Shot - iPhone 13 - 2022-03-09 at 11 48 24](https://user-images.githubusercontent.com/81453549/157522269-6dee8139-720b-4384-9424-9105e3c5accb.png)
izable()
// .frame(height: geo.size.width)
.aspectRatio(contentMode: .fit)
.imageScale(.large)
.foregroundColor(.gray)
}
.ignoresSafeArea()
// switch image {
// case .empty:
// Image(named: "No Image")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// case .success(let image):
// image
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// case .failure:
// Image(named: "No Image")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// @unknown default:
// // Since the AsyncImagePhase enum isn't frozen,
// // we need to add this currently unused fallback
// // to handle any new cases that might be added
// // in the future:
// Image(named: "No Image")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// }
// }
}else {
URLImage(restaurants.imageUrl) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(idealHeight: geo.size.width)//, maxHeight: 50)
.clipped()
}
}
HStack{
Text(restaurants.name)
if restaurants.price != "Unknown" {
Text(restaurants.price)
}
}
.padding(.bottom)
if restaurants.category != [] {
Text("\(restaurants.category.joined(separator: ", "))")
.padding(.horizontal)
}
// if !restaurants.isClosed {
// Text("Open")
// .foregroundColor(Color.green)
// }else {
// Text("Closed")
// .foregroundColor(Color.red)
// }
Button(action: {
print("opening site pressed")
if let url = URL(string: restaurants.url) {
UIApplication.shared.open(url)
}
}) {
HStack {
RestaurantReviews(reviews: restaurants.rating, forgroundColor: .yelpDefault, yelpIconSelectedSize: .large)
Text("\(restaurants.reviewCount)")
.foregroundColor(.primary)
.padding(.trailing)
Text("More Info")
Image("yelp logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 30)
}
}
.padding(.horizontal)
Button(action: {
print("opening location pressed")
Utilities.encodeForGoogleMaps(url: restaurants.name) { loc in
print("loc = \(loc)")
if let url = URL(string: "https://www.google.com/maps/search/?api=1&query=\(loc)") {
UIApplication.shared.open(url)
}
}
}, label: {
Text(restaurants.location)
})
.padding(.bottom)
})
if colorScheme == .dark {
card
// .opacity(0.2)
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
.cornerRadius(12)
.shadow(radius: 4)
}else {//if colorScheme == .light {
card
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 4)
}
}
}
}
struct MediaCardView: View {
let media: Media
@Environment(\.colorScheme) var colorScheme
var body: some View {
GeometryReader { geo in
let card = (
VStack {
if #available(iOS 15, *) {
AsyncImage(url: media.imageUrl) { image in
image
.resizable()
.frame(idealWidth: geo.size.width, idealHeight: geo.size.height)//, maxHeight: 10)
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "photo")
.resizable()
// .frame(height: geo.size.width)
.aspectRatio(contentMode: .fit)
.imageScale(.large)
.foregroundColor(.gray)
}
.ignoresSafeArea()
// switch image {
// case .empty:
// Image(named: "No Image")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// case .success(let image):
// image
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// case .failure:
// Image(named: "No Image")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// @unknown default:
// // Since the AsyncImagePhase enum isn't frozen,
// // we need to add this currently unused fallback
// // to handle any new cases that might be added
// // in the future:
// Image(named: "No Image")
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(height: geo.size.width)
// .clipped()
// }
// }
}else {
URLImage(media.imageUrl) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(idealHeight: geo.size.width)//, maxHeight: 50)
.clipped()
}
}
HStack{
Text(media.name)
}
// .padding(.bottom)
let rating = "Avg. Rating: \(media.rating.rounded())/10 from \(media.ratingCount) reviews"
Text(rating)
.padding(.bottom)
Text(media.description)
.padding(.horizontal)
}
.padding(.bottom))
.onAppear {
print("Avg. Rating: \(media.rating)/10 from \(media.ratingCount) reviews")
}
if colorScheme == .dark {
card
// .opacity(0.2)
.background(Color(red: 0.15, green: 0.15, blue: 0.15))
.cornerRadius(12)
.shadow(radius: 4)
}else {//if colorScheme == .light {
card
.background(Color.white)
.cornerRadius(12)
.shadow(radius: 4)
}
}
}
}
struct CardViewWithThumbs: View {
let restaurants: Restaurants
let media: Media
let direction: LeftRight?
static var yes = Array<Any>()
var body: some View {
ZStack(alignment: .topTrailing) {
ZStack(alignment: .topLeading) {
if Utilities.picktType == .media {
MediaCardView(media: media)
.accessibilityIdentifier("Card")
}else {
RestCardView(restaurants: restaurants)
.accessibilityIdentifier("Card")
}
Image(systemName: "hand.thumbsup.fill")
.resizable()
.foregroundColor(Color.green)
.opacity(direction == .right ? 1 : 0)
.frame(width: 100, height: 100)
.padding()
}
Image(systemName: "hand.thumbsdown.fill")
.resizable()
.foregroundColor(Color.red)
.opacity(direction == .left ? 1 : 0)
.frame(width: 100, height: 100)
.padding()
}
.animation(.default)
}
}
struct Thumbs: View {
...
func showFinalRestView() {
db.collection("parties").document(Utilities.code).addSnapshotListener { doc, error in
print("Start data fetch...")
if error == nil {
print("error = nil")
if doc != nil && doc!.exists {
print("Doc Exists")
if let num = doc!.get("devicesNum") as? Int {
devicesNum = num
if let dev = doc!.get("devices") as? Array<String> {
devices = dev
print("devices = \(dev)")
for name in dev {
print("DeviceList = \(Devices.list)")
if !Devices.list.contains(Devices(id: name, name: name)) {
print("DeviceList Contains = \(name)")
Devices.list.append(Devices(id: name, name: name))
}
}
if let done = doc!.get("devicesDone") as? Array<String> {
print("devicesDone = \(done), devices = \(devices)")
devicesDone = done
if done.sorted() == dev.sorted() && done.count == num {
if var topController = UIApplication.shared.windows.first!.rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
self.fullScreenAd.showAd(root: topController, then: {
// print("fetch data")
// viewModels.fetchData {
// viewModels.fetchData { //yesRestaurant, AllRestaurants, yesMedia, allMedia, devices, devicesDone in
// vmRestAll = AllRestaurants
// vmRestYes = yesRestaurant
// vmDevices = devices
// vmMediaAll = allMedia
// vmMediaYes = yesMedia
print("show final view")
// fullScreenAd.showAd {
Utilities.showFinal = true
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
self.presentation.wrappedValue.dismiss()
results = false
topPick = false
}
print("yesRestaurants from restPicker = \(viewModels.yesRestaurants), allRestaurants = \(viewModels.allRestaurants)")
print("done from completion handler")
// }
// }
})
}
}
}else {
print("error: \(error)")
}
}else {
print("error: \(error)")
}
}else {
print("error: \(error)")
}
}
}
}
}
func showfinalview() {
print("show final view, yes.restlist = \(yes.restList), yes.medialist = \(yes.mediaList)")
results = true
topPick = true
}
func addRestData(card: Restaurants, direction: LeftRight) {
if NetworkMonitor.shared.isConnected {
if direction == .right {
itemCount += 1
print("Code = \(Utilities.code)")
print("Name = \(Utilities.name)")
yes.restList.append(Restaurants(name: card.name, imageUrl: card.imageUrl, id: card.id, rating: card.rating, url: card.url, location: card.location, category: card.category, tag: [], isClosed: card.isClosed, price: card.price, reviewCount: card.reviewCount))
print("yes, list = \(yes.restList)")
}else {
print("no")
itemNo += 1
no.restList.append(Restaurants(name: card.name, imageUrl: card.imageUrl, id: card.id, rating: card.rating, url: card.url, location: card.location, category: card.category, tag: [], isClosed: card.isClosed, price: card.price, reviewCount: card.reviewCount))
}
if card.name == restData.last?.name {
showfinalview()
// showFinal = true
}
}else {
disabledAlert = true
}
}
...
var body: some View {
GeometryReader { geo in
// NavigationView {
VStack(alignment: .center) {
Text(" ")
.padding(.bottom)
HStack {
Text("Party Code: \(Utilities.code)")
.padding(.trailing)
if Utilities.joinDisabled == false {
Button(action: {
copy = true
switch Utilities.shareMode {
case .sheet:
// shareSheet.toggle()
Utilities.presentShareSheet(activityItems: ["pickt://join/\(Utilities.code)"])
// let activityVC = UIActivityViewController(activityItems: ["pickt://join/\(Utilities.code)"], applicationActivities: nil)
// UIApplication.shared.windows.first?.rootViewController?.present(activityVC, animated: true, completion: nil)
case .clipboard:
let pasteboard = UIPasteboard.general
pasteboard.string = "pickt://join/\(Utilities.code)"
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
copy = false
}
}, label: {
if !copy {
Group {
switch Utilities.shareMode {
case .sheet:
Image(systemName: "square.and.arrow.up")
case .clipboard:
Image(systemName: "doc.on.clipboard")
}
}
}else {
Image(systemName: "checkmark")
}
})
.foregroundColor(.primary)
.frame(width: 35, height: 35)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.primary, lineWidth: 1)
)
}
}
.padding(.top)
// Button("Stop Searching") {
// if yes.list.count > 0 {
// showfinalview()
// }
// }
if restData == [] && Utilities.picktType == .restaurants {
Text("No Results Found For Query Terms:\n\nLocation: \(Utilities.location),\nRestaurant Filter: \(Utilities.restFilter),\nCuisine: \(Utilities.cuisine)")
.multilineTextAlignment(.center)
.padding(.top)
}else {
Group {
if Utilities.picktType == .media {
if Utilities.mediaType == .TVShows {
Text("Searching for TV Shows")
// Text("Everyone is seeing the same tv cards, please swipe right for yes and left for no.")
}else if Utilities.mediaType == .Movies {
Text("Searching for Movies")
// Text("Everyone is seeing the same movie cards, please swipe right for yes and left for no.")
}
}else {
Text("Searching \(Utilities.location) with \(Utilities.restFilter), \(Utilities.cuisine)")
// .padding(.horizontal)
// Text("Everyone is seeing the same restaurant cards, please swipe right for yes and left for no.")
}
}
.padding()
.accessibility(identifier: "Searching")
.onTapGesture {
if UserDefaults.standard.bool(forKey: "FASTLANE_SNAPSHOT") {
for item in Restaurants.viewModels {
yes.restList.append(item)
}
self.showfinalview()
}
}
HStack {
Button(action: {
var mediaList = Array<Media>()
var restList = Array<Restaurants>()
for item in no.mediaList {
if !mediaList.contains(item) {
mediaList.append(item)
}
}
for item in no.restList {
if !restList.contains(item) {
restList.append(item)
}
}
no.mediaList = mediaList
no.restList = restList
noList = true
}, label: {
Image(systemName: "hand.thumbsdown")
Text("\(itemNo)")
})
.accessibility(identifier: "No List")
.foregroundColor(.red)
Spacer()
.frame(width: 40)
Button(action: {
var mediaList = Array<Media>()
var restList = Array<Restaurants>()
for item in yes.mediaList {
if !mediaList.contains(item) {
mediaList.append(item)
}
}
for item in yes.restList {
if !restList.contains(item) {
restList.append(item)
}
}
yes.mediaList = mediaList
yes.restList = restList
yesList = true
}, label: {
Image(systemName: "hand.thumbsup")
Text("\(itemCount)")
})
.accessibility(identifier: "Yes List")
.foregroundColor(.green)
}
.padding()
}
if results {
Group {
Text("\(waitingPhrases.randomElement()!)")
// VStack {
// Text("People Who Have Finished:\n")
// ForEach(devicesDone.indices) {
// Text(self.devicesDone[$0])
// }
// }
// .padding()
Button("Override, Show Results") {
overrideResults = true
}
Button {
topPick = true
} label: {
HStack {
Image(systemName: "filemenu.and.selection")
if pick != "None" {
Text("Change your top pick")
}else {
Text("Choose your top pick")
}
}
}
}
.padding()
}
if !results && restData != [] && restData != [Restaurants(name: "nil", imageUrl: URL(string: Utilities.noImage)!, id: "abcdef", rating: 0, url: "https://google.com", location: "nil", category: ["nil"], tag: [], isClosed: false, price: "Unknown", reviewCount: 0)] && Utilities.picktType == .restaurants {
ZStack(alignment: Alignment(horizontal: .center, vertical: .center), content: {
CardStack(
direction: LeftRight.direction,
data: restData,
onSwipe: { card, direction in
print("Swiped \(card.name) to \(direction)")
addRestData(card: card, direction: direction)
},
content: { restaurants, direction, _ in
CardViewWithThumbs(restaurants: restaurants, media: Media(name: "nil", imageUrl: URL(string: Utilities.noImage)!, id: "abcde", description: "abcde", ratingCount: 1, rating: 8, tag: []), direction: direction)
}
)
.padding()
.scaledToFit()
.frame(alignment: .center)
// .frame(maxWidth: screen.width)
.environment(\.cardStackConfiguration, CardStackConfiguration(
maxVisibleCards: 3
// swipeThreshold: 0.1,
// cardOffset: 10,
// cardScale: 0.2,
// animation: .linear
))
})
}else if Utilities.picktType == .media && mediaData != [] {
ZStack(alignment: Alignment(horizontal: .center, vertical: .center), content: {
CardStack(
direction: LeftRight.direction,
data: mediaData,
onSwipe: { card, direction in
print("Swiped \(card.name) to \(direction)")
addMediaData(card: card, direction: direction)
if card.name == mediaData.last?.name {
showfinalview()
// showFinal = true
}
},
content: { media, direction, _ in
CardViewWithThumbs(restaurants: Restaurants(name: "nil", imageUrl: URL(string: Utilities.noImage)!, id: "abcdef", rating: 0, url: "https://google.com", location: "nil", category: ["nil"], tag: [], isClosed: false, price: "Unknown", reviewCount: 0), media: media, direction: direction)
}
)
.padding(.all)
// .frame(height: screen.height)
// .frame(minHeight: 0, idealHeight: screen.height, maxHeight: 450, alignment: .center)
.scaledToFit()
.frame(maxWidth: 700, maxHeight: 800)
// .frame(maxWidth: screen.width)
.environment(\.cardStackConfiguration, CardStackConfiguration(
maxVisibleCards: 3
// swipeThreshold: 0.1,
// cardOffset: 10,
// cardScale: 0.2,
// animation: .linear
))
})
}
}
...
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
Thanks in advanced!!!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.