
Tinder - نعلم جميعًا أن هذا هو تطبيق مواعدة حيث يمكنك ببساطة رفض أو قبول شخص ما عن طريق تمرير اليسار أو اليمين. تُستخدم الآن فكرة قارئ البطاقات في العديد من التطبيقات. طريقة عرض البيانات هذه مناسبة لك إذا مللت من استخدام طرق عرض المجموعة والتجميع. هناك العديد من الكتب المدرسية حول هذا الموضوع ، لكن هذا المشروع استغرقني كثيرًا من الوقت.
يمكنك أن ترى المشروع الكامل على
جيثب بلدي.
بادئ ذي بدء ، أود أن أشيد
بوظيفة Phill Farrugia بشأن هذه المسألة ، ثم إلى سلسلة
YouTube في استوديو Big Mountain حول موضوع مماثل. فكيف نصنع هذه الواجهة؟ حصلت على مساعدة في نشر Phil حول هذا الموضوع. في الأساس ، تتمثل الفكرة في إنشاء UIViews وإدراجها كعروض فرعية في عرض الحاوية. بعد ذلك ، باستخدام الفهرس ، سنمنح كل UIView إدخالًا أفقيًا وعموديًا ونغير عرضه قليلاً. علاوة على ذلك ، عندما نسحب إصبعًا على خريطة واحدة ، سيتم إعادة ترتيب جميع إطارات طرق العرض وفقًا لقيمة الفهرس الجديد.
سنبدأ بإنشاء طريقة عرض حاوية في ViewController بسيط.
class ViewController: UIViewController {
كما ترون ، قمتُ بإنشاء فصل خاص بي يسمى SwipeContainerView وقمت فقط بتكوين stackViewContainer باستخدام القيود التلقائية. لا شيء يدعو للقلق. سيكون حجم SwipeContainerView 300 × 400 وسيتم تركيزه على المحور X وفقط 60 بكسل فقط فوق منتصف المحور Y.
الآن وبعد أن قمنا بتكوين stackContainer ، سننتقل إلى الفئة الفرعية من StackContainerView ونحمّل جميع أنواع الخرائط فيه. قبل ذلك ، سنقوم بإنشاء بروتوكول يحتوي على ثلاث طرق:
protocol SwipeCardsDataSource { func numberOfCardsToShow() -> Int func card(at index: Int) -> SwipeCardView func emptyView() -> UIView? }
فكر في هذا البروتوكول باعتباره TableViewDataSource. يسمح توافق فئة ViewController لدينا مع هذا البروتوكول بنقل معلومات حول بياناتنا إلى فئة SwipeCardContainer. لديها ثلاث طرق:
numberOfCardsToShow () -> Int
: numberOfCardsToShow () -> Int
عدد البطاقات التي نحتاج إلى عرضها. انها مجرد عداد مجموعة البيانات.card(at index: Int) -> SwipeCardView
: بإرجاع SwipeCardView (سننشئ هذه الفئة في لحظة واحدة)EmptyView
-> لن نفعل أي شيء معها ، ولكن بمجرد حذف جميع البطاقات ، فإن استدعاء طريقة التفويض هذه سيعود إلى طريقة عرض فارغة مع بعض الرسائل (لن أطبق هذا في هذا الدرس المحدد ، جربه بنفسك)
محاذاة وحدة تحكم العرض مع هذا البروتوكول:
extension ViewController : SwipeCardsDataSource { func numberOfCardsToShow() -> Int { return viewModelData.count } func card(at index: Int) -> SwipeCardView { let card = SwipeCardView() card.dataSource = viewModelData[index] return card } func emptyView() -> UIView? { return nil } }
الطريقة الأولى ستُرجع عدد العناصر في صفيف البيانات. في الطريقة الثانية ، قم بإنشاء مثيل SwipeCardView () جديد وإرسال بيانات الصفيف لهذا الفهرس ، ثم قم بإرجاع مثيل SwipeCardView.
SwipeCardView هي فئة فرعية من UIView التي تحتوي على UIImage و UILabel ومعرف بالإيماءات. المزيد عن هذا في وقت لاحق. سوف نستخدم هذا البروتوكول للتواصل مع عرض الحاوية.
stackContainer.dataSource = self
عند إطلاق رمز أعلاه ، يتم استدعاء وظيفة reloadData ، والتي تستدعي وظائف مصدر البيانات هذه.
Class StackViewContainer: UIView { . . var dataSource: SwipeCardsDataSource? { didSet { reloadData() } } ....
وظيفة إعادة تحميل البيانات:
func reloadData() { guard let datasource = dataSource else { return } setNeedsLayout() layoutIfNeeded() numberOfCardsToShow = datasource.numberOfCardsToShow() remainingcards = numberOfCardsToShow for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) } }
في وظيفة reloadData ، نحصل أولاً على عدد البطاقات ونخزنها في numberOfCardsToShow المتغير. ثم نقوم بتعيين هذا إلى متغير آخر باسم باقي البطاقات. في الحلقة for ، نقوم بإنشاء خريطة تمثل مثيل SwipeCardView باستخدام قيمة الفهرس.
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) { addCardView(cardView: datasource.card(at: i), atIndex: i ) }
في الواقع ، نريد ظهور أقل من 3 بطاقات في وقت واحد. لذلك ، نحن نستخدم وظيفة دقيقة. CardsToBeVisible هو ثابت يساوي 3. إذا كان numberOfToShow أكبر من 3 ، فسيتم عرض ثلاث بطاقات فقط. نقوم بإنشاء هذه البطاقات من البروتوكول:
func card(at index: Int) -> SwipeCardView
يتم استخدام وظيفة addCardView () ببساطة لإدراج خرائط كقوائم فرعية.
private func addCardView(cardView: SwipeCardView, atIndex index: Int) { cardView.delegate = self addCardFrame(index: index, cardView: cardView) cardViews.append(cardView) insertSubview(cardView, at: 0) remainingcards -= 1 }
في هذه الوظيفة ، نضيف cardView إلى التسلسل الهرمي لطرق العرض ، ونضيف البطاقات كمقابلة فرعية ، ونقوم بتقليل البطاقات المتبقية بمقدار 1. بمجرد أن نضيف cardView كطريقة عرض فرعية ، نقوم بتعيين إطار هذه البطاقات. للقيام بذلك ، نستخدم وظيفة أخرى addCardFrame ():
func addCardFrame(index: Int, cardView: SwipeCardView) { var cardViewFrame = bounds let horizontalInset = (CGFloat(index) * self.horizontalInset) let verticalInset = CGFloat(index) * self.verticalInset cardViewFrame.size.width -= 2 * horizontalInset cardViewFrame.origin.x += horizontalInset cardViewFrame.origin.y += verticalInset cardView.frame = cardViewFrame }
يتم أخذ منطق addCardFrame () هذا مباشرةً من مشاركة Phil. هنا نضع إطار الخريطة وفقًا لمؤشرها. سيكون للبطاقة الأولى ذات الفهرس 0 إطار ، تمامًا مثل الحاوية. ثم نغير أصل الإطار وعرض الخريطة وفقًا للإدراج. وبالتالي ، نضيف البطاقة قليلاً إلى يمين البطاقة أعلاه ، ونخفض عرضها ، وأيضًا نسحب البطاقات إلى أسفل لإيجاد شعور بأن البطاقات مكدسة فوق بعضها البعض.
بمجرد الانتهاء من ذلك ، سترى أن البطاقات مكدسة فوق بعضها البعض. جيد جدا

ومع ذلك ، نحتاج الآن إلى إضافة لفتة انتقاد إلى عرض الخريطة. دعونا الآن نحول انتباهنا إلى فئة SwipeCardView.
SwipeCardView
فئة swipeCardView هي فئة فرعية منتظمة من UIView. ومع ذلك ، لأسباب معروفة فقط لمهندسي Apple ، من الصعب للغاية إضافة ظلال إلى UIView مع زاوية مستديرة. لإضافة ظلال إلى طرق عرض الخريطة ، أقوم بإنشاء جهازي UIViews. واحد منهم هو shadowView ، وبعد ذلك swipeView. في الجوهر ، يحتوي shadowView على ظل وهذا كل شيء. SwipeView له تقريب الزوايا. في swipeView ، أضفت UIImageView ، UILabel لعرض البيانات والصور.
var swipeView : UIView! var shadowView : UIView!
إعداد shadowView و swipeView:
func configureShadowView() { shadowView = UIView() shadowView.backgroundColor = .clear shadowView.layer.shadowColor = UIColor.black.cgColor shadowView.layer.shadowOffset = CGSize(width: 0, height: 0) shadowView.layer.shadowOpacity = 0.8 shadowView.layer.shadowRadius = 4.0 addSubview(shadowView) shadowView.translatesAutoresizingMaskIntoConstraints = false shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true } func configureSwipeView() { swipeView = UIView() swipeView.layer.cornerRadius = 15 swipeView.clipsToBounds = true shadowView.addSubview(swipeView) swipeView.translatesAutoresizingMaskIntoConstraints = false swipeView.leftAnchor.constraint(equalTo: shadowView.leftAnchor).isActive = true swipeView.rightAnchor.constraint(equalTo: shadowView.rightAnchor).isActive = true swipeView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor).isActive = true swipeView.topAnchor.constraint(equalTo: shadowView.topAnchor).isActive = true }
ثم أضفت أداة التعرف على الإيماءات إلى هذا النوع من البطاقات وتسمى وظيفة المحدد عند التعرف. وظيفة التحديد هذه لديها الكثير من المنطق للتمرير والإمالة ، إلخ. لنرى:
@objc func handlePanGesture(sender: UIPanGestureRecognizer){ let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y) switch sender.state { case .ended: if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return } UIView.animate(withDuration: 0.2) { card.transform = .identity card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) self.layoutIfNeeded() } case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation) default: break } }
الأسطر الأربعة الأولى في الكود أعلاه:
let card = sender.view as! SwipeCardView let point = sender.translation(in: self) let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
أولاً ، حصلنا على الفكرة التي تم بها الإيماءة. بعد ذلك ، نستخدم طريقة النقل لمعرفة عدد مرات ضرب المستخدم للبطاقة. السطر الثالث يحصل بشكل أساسي على نقطة الوسط للحاوية الأصل. السطر الأخير حيث نقوم بتثبيت card.center. عندما يضرب المستخدم إصبعًا على البطاقة ، يتم زيادة مركز البطاقة بالقيمة المترجمة x والقيمة المترجمة y. للحصول على هذا السلوك المفاجئ ، قمنا بتغيير نقطة مركز الخريطة بشكل كبير من الإحداثيات الثابتة. عندما تنتهي ترجمة الإيماءات ، نعيدها إلى card.center.
في حالة الحالة.
if (card.center.x) > 400 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }else if card.center.x < -65 { delegate?.swipeDidEnd(on: card) UIView.animate(withDuration: 0.2) { card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75) card.alpha = 0 self.layoutIfNeeded() } return }
نتحقق مما إذا كان card.center.x أكبر من 400 أو إذا كان card.center.x أقل من -65. إذا كان الأمر كذلك ، فإننا نتجاهل هذه البطاقات ، ونغير المركز.
إذا مرر إلى اليمين:
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
إذا مرر لليسار:
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
إذا أنهى المستخدم الإيماءة في الوسط ما بين 400 و -65 ، فسنقوم بإعادة تعيين مركز الخريطة. نحن نسمي أيضًا طريقة التفويض عندما ينتهي التمرير السريع. المزيد عن هذا في وقت لاحق.
للحصول على هذا الميل عند تمرير الخريطة ؛ سأكون صادقا بوحشية. لقد استخدمت هندسة صغيرة واستخدمت قيمًا مختلفة للعمودي والقاعدة ، ثم استخدمت الدالة tan للحصول على زاوية الدوران. مرة أخرى ، كان هذا مجرد محاكمة والخطأ. باستخدام point.x وعرض الحاوية كما يبدو أن محيطين يعملان بشكل جيد. لا تتردد في تجربة هذه القيم.
case .changed: let rotation = tan(point.x / (self.frame.width * 2.0)) card.transform = CGAffineTransform(rotationAngle: rotation)
الآن دعونا نتحدث عن وظيفة مندوب. سوف نستخدم وظيفة المفوض للتواصل بين SwipeCardView و ContainerView.
protocol SwipeCardsDelegate { func swipeDidEnd(on view: SwipeCardView) }
ستأخذ هذه الوظيفة في الاعتبار النوع الذي حدث فيه التمرير السريع ، وسنتخذ عدة خطوات لإزالته من المقاطع الفرعية ، ثم إعادة جميع الإطارات للبطاقات الموجودة تحتها. إليك كيف:
func swipeDidEnd(on view: SwipeCardView) { guard let datasource = dataSource else { return } view.removeFromSuperview() if remainingcards > 0 { let newIndex = datasource.numberOfCardsToShow() - remainingcards addCardView(cardView: datasource.card(at: newIndex), atIndex: 2) for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } }else { for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) } } }
أولاً قم بإزالة هذا العرض من العرض الفائق. بمجرد الانتهاء من ذلك ، تحقق مما إذا كانت هناك بطاقة متبقية. إذا كان هناك ، سنقوم بإنشاء فهرس جديد للبطاقة المراد إنشاؤها. سننشئ newIndex بطرح العدد الإجمالي للبطاقات لعرضها مع بقية البطاقات. ثم سنضيف الخريطة كمقابلة فرعية. ومع ذلك ، ستكون هذه البطاقة الجديدة هي الأقل بحيث تضمن البطاقة التي نرسلها أساسًا تطابق الإطار المضاف مع المؤشر 2 أو الأقل.
لتحريك إطارات البطاقات المتبقية ، سوف نستخدم فهرس العرض الفرعي. للقيام بذلك ، سنقوم بإنشاء صفيف visualCards ، والذي سيحتوي على جميع العروض الفرعية للحاوية كصفيف.
var visibleCards: [SwipeCardView] { return subviews as? [SwipeCardView] ?? [] }
المشكلة ، مع ذلك ، هي أن صفيف visualCards سيكون له فهرس عرض فرعي معكوس. وبالتالي ، ستكون البطاقة الأولى الثالثة ، وستبقى الثانية في المرتبة الثانية ، والثالثة ستكون في المركز الأول. لمنع حدوث ذلك ، سنقوم بتشغيل مجموعة visualCards بالترتيب العكسي للحصول على فهرس العرض الفرعي الفعلي ، وليس كيفية تحديد موقعها في صفيف visualCards.
for (cardIndex, cardView) in visibleCards.reversed().enumerated() { UIView.animate(withDuration: 0.2, animations: { cardView.center = self.center self.addCardFrame(index: cardIndex, cardView: cardView) self.layoutIfNeeded() }) }
حتى الآن سنقوم بتحديث إطارات بقية cardViews.
هذا كل شيء. هذه طريقة مثالية لتقديم كمية صغيرة من البيانات.