توثيق تطبيقات iOS بصريًا
كيف بنينا أداة UI automation نستخدمها لالتقاط التسلسل الهرمي الكامل للعروض (view hierarchy) في كل صفحة من التطبيق وتصديره إلى ملف Sketch.

لنعترف بالأمر: من الصعب تذكّر كل أصناف UIView، حتى في تطبيقات iOS متوسطة الحجم. هناك مئات الملفات من هذا النوع، كتبها أشخاص مختلفون، بلغات مختلفة، وهي مبعثرة في أرجاء المشروع كله!
باختصار: نستخدم UI automation لالتقاط التسلسل الهرمي الكامل للعروض (view hierarchy) في كل صفحة من التطبيق، ثم نصدّره إلى ملف Sketch. 👉 اطّلعوا على الكود.
البحث عن UIButton
قصة حقيقية
تبدأ بالنظر إلى الصفحة في التطبيق أثناء تشغيله. تلاحظ أنه ليس UIButton عاديًا، فربما هناك subclass ما.
ثم تخمّن:
"آه، إنه زر لإضافة منتج إلى السلة، لا بد أن اسمه شيء مثل AddToCartButton.swift"
تكتب الاسم في Open Quickly، فتجد ملفًا باسم مختلف قليلًا، AddCartButton.swift. قريب بما يكفي. وتجد أيضًا ملف XIB باسم AddCartButton.xib! فتقول في نفسك:
"الحمد لله أننا نستخدم Interface Builder. سأعرف شكله بمجرد النظر إليه."
ثم تنتظر حتى يُفتح ملف XIB… لحسن الحظ لم ينهَر Xcode، لكن يبدو أنه ليس الملف الذي تبحث عنه. على أي حال، تخرج بتخمين آخر:
"همم. إنه في صفحة Promotion، ربما اسمه PromotionCartButton أو شيء من هذا القبيل."
بعدها قد تلجأ إلى مزيج مما يلي:
- تكرار عملية التخمين نفسها بهذا الاسم،
- البحث عن الشخص الذي يعرف فعلًا (على أمل أن يتذكر)،
- التتبّع انطلاقًا من view controller (إن كنت تعرف أيّها)،
- التتبّع انطلاقًا من Main.storyboard،
- التتبّع انطلاقًا من AppDelegate.swift والبحث نزولًا حتى النهاية،
- ولا تنسَ أن العروض المُنشأة برمجيًا تحتاج إلى معالجة منفصلة.
وبعد المرور بكل ذلك، تصل أخيرًا إلى الجواب.
ويتضح أن…
إنه UIButton عادي، مدمج داخل subclass مخصص من UIView اسمه AddToCartView! ما كان لنا أن نعرف ذلك بأي طريقة.
توثيق بصري للتطبيق
باستخدام اختبارات UI automation، نستطيع التقاط لقطات للتطبيق في الوقت الفعلي بينما يتنقل فيه الـ bot.
ولتحقيق ذلك نستخدم Salsa لتصدير عروض iOS إلى ملف Sketch. وبذلك نستطيع محاكاة التسلسل الهرمي الدقيق للعروض في التطبيق أثناء تشغيله وفي حالات مختلفة.
مثال على حالة اختبار:
import EarlGrey
import Salsa
import XCTest
@testable import YourApp
class Test: XCTestCase {
override func setup() {
super.setup()
}
func testSnap() {
makeSureKeyWindowVisible()
let allTabBarsFlow = artboardSession(name: "All TabBars") { (s) in
tap(id: "Tab1")
waitUntilLoadingFinish()
s.snapWindow(name: "Tab 1")
tap(id: "Tab2")
waitUntilLoadingFinish()
s.snapWindow(name: "Tab 2")
...
}
// Stack all artboards vertically
let artboards = AutoSketch.arrange(layers: [[allTabBarsFlow, ...]], verticalPadding: 200, horizontalPadding: 200)
let page = Page(name: "Artboards", layers: artboards)
let document = Document(pages: [page], colors: [], textStyles: [])
do {
try document.export()
} catch let error {
XCTFail("Failed to export file: \(error)")
}
}
}
lang-swift
باستخدام Salsa مع UI automation، نستطيع إنشاء ملف Sketch يضم مسار التطبيق كاملًا، ما يتيح لنا تتبّع مشكلات العروض (debug) في Sketch بشكل شبه فوري. الأمر أشبه بميزة View Debugging في Xcode لكن بقدرات مضاعفة.
ويستطيع أعضاء الفريق الجدد ببساطة النقر على مجموعة العرض ومعرفة الـ subclass الخاص بها فورًا.
اطّلعوا على طريقة الإعداد هنا:
salsa-AutoSketch: أداة لتصدير مكوّنات iOS إلى Sketch 📱💎
لكنها ليست حلًا سحريًا لكل شيء، مع مراعاة النقاط التالية:
المزايا
- لا حاجة إلى انتظار تحميل ملفات XIB أو Storyboard.
- لا مزيد من التخمين في أسماء الـ subclass الخاصة بالعروض.
- تعمل مع العروض المُنشأة برمجيًا.
- تحتفظ بـسياق كل عرض بشكل جيد. فمثلًا، قد تستخدم أزرار متشابهة الشكل في صفحات مختلفة ملفات XIB مختلفة. هل تستخدم هذه الصفحة collection view أم table view أم مجرد عرض مخصص؟ هل هذه خلايا قابلة لإعادة الاستخدام أم مجرد عروض؟ كل عرض في ملفه الخاص، والأمر يعتمد على طريقة استخدامه. بالتوفيق في البحث عنه في المشروع كله (cmd + shift + f).
- كلما كبر فريق التطوير، زادت الفائدة.
- تمنح نظرة عامة سريعة على التطبيق.
العيوب
- لا يمكنها التقاط محتوى scroll view أو table view أو collection view بالكامل، لأن جزءًا من المحتوى يكون خارج الشاشة. أحد الحلول البديلة هو التمرير للأسفل ثم الالتقاط، لكنه حل غير مريح نوعًا ما. أما إذا كنتم تستخدمون scroll view مع content view مغلِّف، فيمكنكم التقاط ذلك الـ content view بدلًا منه.
- تنفيذ UI automation عبر التطبيق كله هو الجزء الأكثر جهدًا هنا. إنها عملية قائمة على التجربة والخطأ: العثور على الزر المطلوب النقر عليه، وانتظار اختفاء مؤشر التحميل قبل الالتقاط، وانتظار ظهور عنصر ما، وهكذا.
- يجب إعادة تشغيل الأتمتة عند حدوث تغيير كبير.
النتيجة النهائية
المجموعات نسخة شبه مطابقة لما ترونه في ميزة View Debugging في Xcode.


