بعد مشاهدة Keynote WWDC 2019 والتعرف على SwiftUI ، المصمم لوصف UIs التعريفي في التعليمات البرمجية ، أريد التكهن حول كيفية ملء اللوحات والمجموعات بشكل مُعلن. على سبيل المثال ، مثل هذا:
enum Builder { static func widgets(objects: Objects) -> [Widget] { let header = [ Spacing(height: 25).widget, Header(string: " ").widget, Spacing(height: 10, separator: .bottom).widget ] let body = objects .flatMap({ (object: Object) -> [Widgets] in return [ Title(object: object).widget, Spacing(height: 1, separator: .bottom).widget ] }) return header + body } } let objects: [Object] = ... Builder .widgets(objects: objects) .bind(to: collectionView)
في المجموعة ، يتم تقديم هذا على النحو التالي:
مقدمة
كما تعلم من مصادر موثوقة : الغالبية العظمى من وقتهم ، يقضي مطور نظام التشغيل iOS التقليدي العمل مع الأجهزة اللوحية. إذا افترضنا أن المطورين في المشروع يفتقرون إلى حد كبير وأن الأجهزة اللوحية ليست بسيطة ، فلا يوجد أي وقت لبقية التطبيق. وهناك شيء يجب القيام به مع هذا ... والحل المحتمل هو تحلل الخلايا.
تحلل الخلية يعني استبدال خلية واحدة بعدة خلايا أصغر. مع هذا الاستبدال ، لا شيء بصريا يجب أن يتغير. على سبيل المثال ، ضع في الاعتبار المنشورات من موجز أخبار VK لنظام التشغيل iOS. يمكن تقديم منشور واحد كخلية واحدة ، أو كمجموعة من الخلايا - بدائية .
الخلايا المتحللة لن تعمل دائما. سيكون من الصعب تقسيم الخلية إلى بدائل لها ظل أو تقريب من جميع الجوانب. في هذه الحالة ، ستكون الخلية الأصلية بدائية.
إيجابيات وسلبيات
باستخدام تحلل الخلايا ، تبدأ الجداول / المجموعات في تكوين بدائل يتم إعادة استخدامها غالبًا: بدائية بنص ، بدائية مع صورة ، بدائية بخلفية ، إلخ. حساب ارتفاع بدائية واحدة أبسط بكثير وأكثر فعالية من خلية معقدة مع عدد كبير من الحالات. إذا رغبت في ذلك ، يمكن حساب الارتفاع الديناميكي CTFramesetter
أو حتى رسمه في الخلفية (على سبيل المثال ، النص عبر CTFramesetter
).
من ناحية أخرى ، يصبح العمل مع البيانات أكثر تعقيدًا. ستكون البيانات مطلوبة لكل بدائي ، ووفقًا لـ IndexPath
، سيكون من الصعب تحديد الخلية الحقيقية التي تنتمي إليها. سيتعين علينا تقديم طبقات جديدة من التجريد أو حل هذه المشكلات بطريقة أو بأخرى.
يمكنك التحدث لفترة طويلة عن إيجابيات وسلبيات هذا المشروع ، لكن من الأفضل محاولة وصف طريقة تحلل الخلايا.
اختيار الأدوات
نظرًا لأن UITableView
محدود في قدراته ، وكما ذكرنا سابقًا ، لدينا لوحات معقدة إلى حد ما ، فإن الحل المناسب هو استخدام UICollectionView
. إنه حول UICollectionView
الذي سيتم مناقشته في هذا المنشور.
باستخدام UICollectionView
تواجه موقفًا حيث لا يمكن أن تشكل قاعدة UICollectionViewFlowLayout
الأساسية الترتيب المطلوب لعناصر المجموعة (لا نأخذ بعين الاعتبار UICollectionViewCompositionalLayout
الجديد). في مثل هذه اللحظات ، يتم اتخاذ القرار عادة للعثور على بعض المصادر المفتوحة المصدر UICollectionViewLayout
. ولكن حتى بين الحلول الجاهزة ، قد لا يكون هناك حل مناسب ، على سبيل المثال ، في حالة الصفحة الرئيسية الديناميكية لمتجر كبير عبر الإنترنت أو شبكة اجتماعية. نحن نفترض الأسوأ ، لذلك UICollectionViewLayout
العالمي الخاص UICollectionViewLayout
.
بالإضافة إلى صعوبة اختيار تخطيط ، يجب أن تقرر كيف ستتلقى المجموعة البيانات. بالإضافة إلى الطريقة المعتادة ، حيث يتوافق كائن (غالبًا UICollectionViewDataSource
) مع بروتوكول UICollectionViewDataSource
ويوفر بيانات لمجموعة ، فإن استخدام الأطر التي تعتمد على البيانات يكتسب شعبية. الممثلون الساطعون لهذا النهج هم CollectionKit و IGListKit و RxDataSources وغيرها. استخدام هذه الأُطُر يبسط العمل مع المجموعات ويوفر القدرة على تحريك تغييرات البيانات ، لأن خوارزمية الفرق موجودة بالفعل في الإطار. لأغراض النشر ، سيتم اختيار إطار عمل RxDataSources .
القطعة وخصائصها
نقدم بنية بيانات وسيطة نسميها عنصر واجهة مستخدم . نحن نصف الخصائص الرئيسية التي يجب أن تحتوي عليها القطعة:
- يجب أن تمتثل الأداة المصغّرة للبروتوكولات اللازمة لاستخدام إطار العمل المستند إلى البيانات. تحتوي هذه البروتوكولات عادةً على قيمة مرتبطة (مثل
IdentifiableType
في RxDataSources ) - يجب أن يكون من الممكن تجميع عناصر واجهة تعامل المستخدم للبدائل المختلفة في صفيف. لتحقيق ذلك ، لا ينبغي أن يكون عنصر واجهة المستخدم القيم المرتبطة. لهذه الأغراض ، يمكنك استخدام آلية محو الكتابة أو شيء من هذا القبيل.
- يجب أن تكون القطعة قادرة على حساب حجم البدائية. بعد ذلك ، عند تكوين
UICollectionViewLayout
، يبقى فقط وضع الأولويات بشكل صحيح وفقًا للقواعد المحددة مسبقًا. - يجب أن تكون القطعة هي مصنع
UICollectionViewCell
. لذلك ، من تطبيق UICollectionViewDataSource
كل منطق إنشاء الخلايا وكل ما تبقى هو:
let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell
تطبيق القطعة
لكي تتمكن من استخدام عنصر واجهة المستخدم باستخدام إطار عمل RxDataSources ، يجب أن تلتزم Equatable
و IdentifiableType
. نظرًا لأن عنصر واجهة المستخدم يمثل بدائية ، فسوف يكون كافياً لأغراض النشر إذا كانت القطعة تعرف نفسها على الامتثال لبروتوكول IdentifiableType
. في الممارسة العملية ، سيؤثر هذا على حقيقة أنه عندما تتغير الأداة ، لن يتم إعادة تحميل البدائية ، ولكن سيتم حذفها وإدراجها. للقيام بذلك ، قدم بروتوكول WidgetIdentifiable
الجديد:
protocol WidgetIdentifiable: IdentifiableType { } extension WidgetIdentifiable { var identity: Self { return self } }
لتتوافق مع WidgetIdentifiable
، تحتاج القطعة إلى الامتثال لبروتوكول Hashable
. سوف تأخذ القطعة Hashable
بيانات للتوافق مع البروتوكول من الكائن الذي سيصف بدائية معينة. يمكنك استخدام AnyHashable
"لمسح" عنصر واجهة مستخدم نوع الكائن.
struct Widget: WidgetIdentifiable { let underlying: AnyHashable init(_ underlying: AnyHashable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.hash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying == rhs.underlying } }
في هذه المرحلة ، يتم تنفيذ أول اثنين من خصائص عنصر واجهة المستخدم. ليس من الصعب التحقق من ذلك عن طريق جمع عدة عناصر واجهة مستخدم مع أنواع مختلفة من الكائنات في صفيف.
let widgets = [Widget("Hello world"), Widget(100500)]
لتنفيذ الخصائص المتبقية ، نقدم بروتوكول جديد WidgetPresentable
protocol WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell func widgetSize(containerWidth: CGFloat) -> CGSize }
سيتم استخدام الدالة widgetSize(containerWidth:)
في UICollectionViewLayout
عند إنشاء سمات الخلايا ، و widgetCell(collectionView:indexPath:)
- للحصول على الخلايا.
إذا كان عنصر واجهة المستخدم WidgetPresentable
بروتوكول WidgetPresentable
، فسوف تفي عنصر واجهة المستخدم بجميع الخصائص المشار إليها في بداية المنشور. ومع ذلك ، فإن الكائن الموجود في عنصر واجهة المستخدم AnyHashable سيتعين استبداله بتكوين WidgetPresentable
و WidgetHashable
، حيث لن يكون لـ WidgetHashable
قيمة مرتبطة (كما في حالة Hashable
) Hashable
نوع الكائن داخل عنصر واجهة المستخدم "محوها":
protocol WidgetHashable { func widgetEqual(_ any: Any) -> Bool func widgetHash(into hasher: inout Hasher) }
في الإصدار النهائي ، ستبدو الأداة كما يلي:
struct Widget: WidgetIdentifiable { let underlying: WidgetHashable & WidgetPresentable init(_ underlying: WidgetHashable & WidgetPresentable) { self.underlying = underlying } } extension Widget: Hashable { func hash(into hasher: inout Hasher) { self.underlying.widgetHash(into: &hasher) } static func ==(lhs: Widget, rhs: Widget) -> Bool { return lhs.underlying.widgetEqual(rhs.underlying) } } extension Widget: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { return underlying.widgetCell(collectionView: collectionView, indexPath: indexPath) } func widgetSize(containerWidth: CGFloat) -> CGSize { return underlying.widgetSize(containerWidth: containerWidth) } }
كائن بدائي
دعونا نحاول تجميع أبسط البدائية ، والتي ستكون المسافة البادئة لارتفاع معين.
struct Spacing: Hashable { let height: CGFloat } class SpacingView: UIView { lazy var constraint = self.heightAnchor.constraint(equalToConstant: 1) init() { super.init(frame: .zero) self.constraint.isActive = true } } extension Spacing: WidgetHashable { func widgetEqual(_ any: Any) -> Bool { if let spacing = any as? Spacing { return self == spacing } return false } func widgetHash(into hasher: inout Hasher) { self.hash(into: &hasher) } } extension Spacing: WidgetPresentable { func widgetCell(collectionView: UICollectionView, indexPath: IndexPath) -> UICollectionViewCell { let cell: WidgetCell<SpacingView> = collectionView.cellDequeueSafely(indexPath: indexPath) if cell.view == nil { cell.view = SpacingView() } cell.view?.constraint.constant = height return cell } func widgetSize(containerWidth: CGFloat) -> CGSize { return CGSize(width: containerWidth, height: height) } }
WidgetCell<T>
هو مجرد فئة فرعية من UICollectionViewCell
التي تقبل UIView
UICollectionViewCell
كطريقة عرض فرعية. cellDequeueSafely(indexPath:)
هي وظيفة تسجل خلية في مجموعة قبل إعادة استخدامها إذا لم يتم تسجيل خلية في المجموعة من قبل. سيتم استخدام Spacing
كما هو موضح في بداية المنشور.
بعد تلقي صفيف عناصر واجهة المستخدم ، يبقى فقط الربط بـ observerWidgets
:
typealias DataSource = RxCollectionViewSectionedAnimatedDataSource<WidgetSection> class Controller: UIViewController { private lazy var dataSource: DataSource = self.makeDataSource() var observerWidgets: (Observable<Widgets>) -> Disposable { return collectionView.rx.items(dataSource: dataSource) } func makeDataSource() -> DataSource { return DataSource(configureCell: { (_, collectionView: UICollectionView, indexPath: IndexPath, widget: Widget) in let cell = widget.widgetCell(collectionView: collectionView, indexPath: indexPath) return cell }) } }
النتائج
في الختام ، أود أن أعرض العمل الحقيقي للمجموعة ، والتي بنيت بالكامل على الحاجيات.
كما ترون ، فإن تحلل UICollectionViewCell
ممكن وفي الحالات المناسبة يمكن أن تبسط حياة المطور.
تصريحات
الكود الوارد في المنشور مبسط للغاية ويجب عدم تجميعه. كان الهدف هو وصف النهج ، وليس توفير حل جاهز.
يمكن توسيع بروتوكول WidgetPresentable
بوظائف أخرى تعمل على تحسين التخطيط ، على سبيل المثال ، widgetSizeEstimated(containerWidth:)
أو widgetSizePredefined(containerWidth:)
، والتي تُرجع الحجم المقدر والثابت ، على التوالي. تجدر الإشارة إلى أن الدالة widgetSize(containerWidth:)
يجب أن ترجع حجم البدائية حتى للحسابات المطلوبة ، على سبيل المثال ، systemLayoutSizeFitting(_:)
. يمكن تخزين هذه الحسابات مؤقتًا من خلال Dictionary
و NSCache
وما إلى ذلك.
كما تعلم ، يجب أن تكون جميع أنواع الخلايا التي يستخدمها UICollectionView
مسجلة مسبقًا في المجموعة. ومع ذلك ، لإعادة استخدام التطبيقات المصغّرة بين الشاشات / المجموعات المختلفة وعدم تسجيل جميع معرفات / أنواع الخلايا مقدمًا ، تحتاج إلى الحصول على آلية تسجل خلية قبل استخدامها لأول مرة مباشرةً في كل مجموعة. في المنشور ، cellDequeueSafely(indexPath:)
استخدام cellDequeueSafely(indexPath:)
الدالة cellDequeueSafely(indexPath:)
لهذا الغرض.
لا يمكن أن يكون هناك رؤوس أو تذييلات في المجموعة. في مكانها سوف تكون بدائية. وجود تكميلية في المجموعة لن يعطي أي مكافآت خاصة في النهج الحالي. عادةً ما يتم استخدامها عندما يتوافق صفيف البيانات تمامًا مع عدد الخلايا ويكون مطلوبًا إظهار طرق عرض إضافية قبل الخلايا أو بعدها أو فيما بينها. في حالتنا ، يمكن أيضًا إضافة بيانات طرق العرض الإضافية إلى مجموعة عناصر واجهة المستخدم ورسمها كأولويات.
داخل نفس المجموعة ، يمكن تحديد موقع عناصر واجهة التعامل مع نفس العناصر. على سبيل المثال ، Spacing
نفسه في بداية ونهاية المجموعة. سيؤدي وجود هذه الكائنات غير الفريدة إلى اختفاء الرسوم المتحركة في المجموعة. لجعل مثل هذه الكائنات فريدة من نوعها ، يمكنك استخدام علامات AnyHashable
خاصة و AnyHashable
#file
و #line
حيث تم إنشاء الكائن ، إلخ.