الوصفة السحرية لإنشاء واجهات المستخدم لل iOS

في هذا المقال اعرض لكم طريقتي في تصميم واجهات المستخدم باستخدام ال CollectionView

عمر الشمريمنذ 5 سنوات

 

بسم الله الرحمن الرحيم

 

في هذا المقال راح نستعرض طريقتي المفضلة في تصميم واجهات المستخدم باستخدام ال CollectionView و يعتبر هذا الدرس متوسط الصعوبة.

 

المتطلبات:

- المام بنظام ال iOS

- المام بال CollectionView

- المام بال Delegate Design Pattern

- معرفة بال Auto Layout

 

الفكرة:

سوف نقوم بالاستفادة من ال UICollectionView بتجهيز قوالب واجهة المستخدم و هي عبارة عن UICollectionViewCell وهذه القوالب سوف تحتوي على أساس واجهة المستخدم من أزرار و عبارات و غيرها. وبالمثال راح يتضح المقال بإذن الله.

 

الخطوات:

بعد إنشاء المشروع و تجهيزه نقوم بعمل class جديد يرث UICollectionViewController و نسميه DetailsCollectionViewController (راح نترك الكلاس هذا و نقوم بتجهيز ال ViewModel )

الآن سوف نقوم بتجهيز ال ViewModel وسيكون هو الأساس اللي راح يغذي ال DetailsCollectionViewController

نقوم بإنشاء كلاس و نسميه DetailsViewModel ونضع هذا الكود بداخله

import Foundation

import UIKit



//  هذا ال  Enum يمثل انواع الواجهات التي سنحتاجها في تطبيقنا

// مثلا هنا نحتاج ترأيس للمحتويات و نحتاج عبارات وأزرار و خلية بدون محتويات لإضافة فراغ بين المحتويات

enum DetailsCellType {

    case header

    case details

    case button

    case empty

}



// أي Struct ينتهي بكلمة Property سوف يحمل المعلومات اللتي تحتاجها كل خلية لعرض محتوياتها

struct HeaderCellProperty {

    let title: String

}



struct DetailsCellProperty {

    let title: String

    let subtitle: String

}



struct ButtonCellProperty {

    let buttonTitle: String

    let buttonColor: UIColor

    let buttonTitleColor: UIColor

    let buttonClickHandler: () -> Void

}



struct EmptyCellProperty {

}



// هذا هو ال View Model ويحتوي على جميع الخصائص المذكورة سابقا

struct DetailsViewModel {

    var cellType: DetailsCellType

    

    var headerCellProperty: HeaderCellProperty?

    var detailsCellProperty: DetailsCellProperty?

    var buttonCellProperty: ButtonCellProperty?

    var emptyCellProperty: EmptyCellProperty?

    

    init(headerCellProperty: HeaderCellProperty) {

        self.cellType = .header

        self.headerCellProperty = headerCellProperty

    }

    init(detailsCellProperty: DetailsCellProperty) {

        self.cellType = .details

        self.detailsCellProperty = detailsCellProperty

    }

    init(buttonCellProperty: ButtonCellProperty) {

        self.cellType = .button

        self.buttonCellProperty = buttonCellProperty

    }

    

    init(emptyCellProperty: EmptyCellProperty) {

        self.cellType = .empty

        self.emptyCellProperty = emptyCellProperty

    }

    

    func reuseIdentifier() -> String {

        switch self.cellType {

        case .header:

            return "HeaderNameCollectionViewCell"

        case .details:

            return "DetailTextCollectionViewCell"

        case .button:

            return "ButtonCollectionViewCell"

        case .empty:

            return "EmptyCollectionViewCell"

        }

    }

}

 

 

نعود الآن الى DetailsCollectionViewController ونضيف متغير من نوع Array من نوع DetailsViewModel 

    var viewModels = [DetailsViewModel]()

ومن ثم نقوم بتجهيز ال CollectionViewDataSource  في كلاس DetailsCollectionViewController كالتالي

 

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }


    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.viewModels.count
    }

    // لا نستطيع القيام بشيد هنا حتى نقوم بتجهيز الخلايا
    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
    
        // Configure the cell
    
        return cell
    }

 

 

نقوم الآن بتجهيز الخلايا حتى نستخدمها لعرضها. و نبدأ بال HeaderCollectionViewCell حيث نقوم بإنشاء كلاس بهذا الاسم يرث UICollectionViewCell وكذلك نقوم بإنشاء ملف xib بنفس الاسم ونقوم بوضع الكود التالي:

 

import UIKit

class HeaderCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var headerLabel: UILabel!
    @IBOutlet weak var seperatorView: UIView!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        self.headerLabel.textColor = .black
        self.headerLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        self.seperatorView.backgroundColor = .black
    }

}

 

وهذا شكل ملف ال xib 

Header Collection View Cell

 

وهذه هي ال Constraints

Collection View Cell Constraints

**ولا ننسى من استخدام ال ReuseIdentifier كما هو في ال ViewModel

 

الآن سوف نقوم بالعودة الى DetailsCollectionViewController لنختبر أن ما قمنا به الى الان يعمل بالشكل الصحيح وطريقتي المفضلة هي أن نقوم بعمل Extension على كلاس DetailsCollectionViewController و يحتوي على الدوال المساعدة لإضافة هذه الخلايا. ويكون هذا الكود بداخل ال Extension 

extension DetailsCollectionViewController {
    func addHeaderNameWithTitle(title: String) {
        let headerCellProperty = HeaderCellProperty(title: title)
        let viewModel = DetailsViewModel(headerCellProperty: headerCellProperty)
        self.viewModels.append(viewModel)
    }
    
}

 

و نقوم الآن بتسجيل الخلية الجديدة في ال CollectionView ومن ثم استخدامها في CellForItemAtIndexPath كالتالي:

 

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let headerNameNib = UINib(nibName: "HeaderCollectionViewCell", bundle: Bundle.main)
        self.collectionView?.register(headerNameNib, forCellWithReuseIdentifier: "HeaderNameCollectionViewCell")
    }




    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        let viewModel = self.viewModels[indexPath.item]
        
        switch viewModel.cellType {
        case .header:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifier(), for: indexPath) as! HeaderCollectionViewCell
            cell.headerLabel.text = viewModel.headerCellProperty?.title
            return cell
        default:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
            return cell
        }
    }

 

الآن قمنا بتجهيز جميع ما نحتاجه و لكن تبقى شيء واحد و هو أن نضمن أن أبعاد الخلية كما يجب و حتى نتمكن من ذلك نقوم بإضافة الكود التالي:

//نحتاج لهذا البروتوكول حتى يقوم النظام باستدعاء الدوال للأحجام

class WalletDetailsViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout







    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        var height: CGFloat
        let viewItem = self.viewModels[indexPath.item]
        
        switch viewItem.cellType {
        case .header:
            height = 50
        case .details:
            height = 50
        case.button:
            height = 100
        case .empty:
            height = 20
        }
        
        let width = self.view.frame.size.width
        return CGSize(width: width, height: height)
    }

 

الحمد لله. الآن كل ما تبقى علينا هو استخدام هذا الكود و سوف نقوم بالتعديل على كلاس ViewController الذي تم إنشائه من ال XCode حسب التالي:

import UIKit

//فقط نجعل الكلاس يرث DetailsCollectionViewController
class ViewController: DetailsCollectionViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        //نقوم باستدعاء الدالة المساعدة لإضافة العنوان/الترأيس
        self.addHeaderNameWithTitle(title: "My First Title")
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


}

ولكن قبل ذلك نحتاج أن نضيف في ال Storyboard واجهة جديدة من نوع Collection View Controller وتسنيدها الى الكلاس السابق وتعيينها ك initial view controller و حذف الواجهة القديمة من الStoryboard

وتكون هذه هي النتيجة:

 

الان سوف نقوم بإعادة الخطوات نفسها لإضافة عبارات من نوع UILabel الى واجهة المستخدم.  و سوف تكون من نوع key/value بحيث يكون Label للعنوان و Label آخر للقيمة

 

نبدأ بتجهيز الخلية كما فعلنا في السابق مع العنوان وننشئ كلاس يرث UICollectionViewCell ونسميه LabelCollectionViewCell ونقوم بإنشاء ملف ال xib كذلك و نقوم بتجهيزه كالتالي:

 

import UIKit

class LabelCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
        self.titleLabel.font = UIFont.preferredFont(forTextStyle: .body)
        self.subtitleLabel.font = UIFont.preferredFont(forTextStyle: .body)
        
        self.titleLabel.textColor = UIColor.darkText
    }

}

 

ونجهز ملف xib كالتالي: 

وال constraints كالتالي:

 

نقوم الآن بالتعديل على كلاس DetailsCollectionViewController وتجهيز الدوال المساعدة لإضافة الخلية الجديدة.

    func  addDetails(with title: String, and subtitle: String) {
        let detailsCellProperty = DetailsCellProperty(title: title, subtitle: subtitle)
        let viewModel = DetailsViewModel(detailsCellProperty: detailsCellProperty)
        self.viewModels.append(viewModel)
    }

** ولا ننسى إضافة ال reuseIdentifier كما هو من ال ViewModel

 

و من ثم نقوم بتسجيل الخلية لاستخدامها في ال CollectionView كالتالي:

        let detailsNameNib = UINib(nibName: "LabelCollectionViewCell", bundle: Bundle.main)
        self.collectionView?.register(detailsNameNib, forCellWithReuseIdentifier: "DetailTextCollectionViewCell")

نقوم الآن بالتعديل على CellForItemAtIndexPath لتجهيز الخلية من النوع الجديد بإضافة case جديد كالتالي:

        case .details:
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifier(), for: indexPath) as! LabelCollectionViewCell
            cell.titleLabel.text = viewModel.detailsCellProperty?.title
            cell.subtitleLabel.text = viewModel.detailsCellProperty?.subtitle
            return cell

 

الآن فقط نقوم باستخدام الدالة الجديدة في كلاس ViewController لإضافة واجهة مستخدم من نوع Key/Value كالتالي:

        self.addDetails(with: "Title 1", and: "This is the first subtitle")
        self.addDetails(with: "Title 2", and: "This is the second subtitle")
        self.addDetails(with: "Title 3", and: "This is the third subtitle")
        self.addDetails(with: "Title 4", and: "This is the fourth subtitle")
        self.addDetails(with: "Title 5", and: "This is the fifth subtitle")

 

و تكون هذه هي النتيجة:

 

 

الى هنا تنتهي هذه المقالة!!!!

 

 

ولكن ماذا عن إضافة الأازرار والخلايا الفراغة؟ سوف أترك عمل هذا كتمرين لكم واستقبل أي استفسارات بهذا الخصوص. 

 

ستجدون الأكواد كاملة هنا على GitHub

 

الفوائد

  • يمكننا انشاء العديد من الواجهات بدون إعادة كتابة الأكواد فقط نقوم بإنشاء كلاس يرث DetailsCollectionViewController 
  • للشاشات الطويلة سيتكفل ال CollectionView بال scroll ولا نحتاج إلى إضافة أي سطر لعمل ال Scrolling

 

 

في الختام شكرا لكم على القراءة واتمنى أن المقالة كانت لها فائدة. 

كلمات دليلية: ios swift
6
إعجاب
3314
مشاهدات
0
مشاركة
4
متابع
متميز
محتوى رهيب

التعليقات (3)

العنود:

هل الكلمات المحددة بالهيلاتير هي برامج ؟ يعني كيف اطبق ؟ فقط باستخدام الاكواد

عبد الله:

وعليكم السلام ورحمة الله وبركاته

عمر طريقتك هذه تحتاج لشخص متفرغ لتصميم الواجهات بال ios ، بمعنى يكون موظف ومتخصص بنوع واحد من العمل حتى يكون ملم فيه بشكل كامل وباحترافية، هذا رأيي.

ووفقت بإذن الله بهذا المقال الفريد من نوعه، بارك الله فيك ولك.

عمر الشمري:

 العنود: الكلمات المحددة بالهايلايت هي الاكواد المستخدمة. المقالة للناس اللي عندهم خبرة في برمجة تطبيقات الايفون.

 

عبدالله: شكرا على مرورك. ماني عارف ليش تحتاج واحد متفرغ؟ 

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !