نوشتن اپ iOS/mac را از کجا شروع کنم؟ و چگونه مدل خوب بنویسم؟

وقتی پروژه ای را شروع می‌کنیم، اولین سوالی که به ذهن می‌رسه اینه که از کجا باید شروع کنم. یه برنامه‌نویس عادی احتمالا پیاده‌سازی مدل، ویو و کنترلر را با هم شروع می‌کنه. تا بتونه همزمان پیشرفت کار را ببینه. این روش مزایای خودش را داره بخصوص وقتی قراره کار را به یه مدیر پروژه‌ی بی‌سواد پرزنت کنیم. اما میزان دوباره کاری و اتلاف وقت و انرزی توش زیاده، و طبیعتا هزینه‌ی تولید بالاتر میره.

به نظر من بهتره اول مدل را به طور کامل پیاده سازی کنیم. برای پیاده سازی ویوکنترلر حتما باید یه مدل (که با دیتای مشقی پر می‌شه) داشته باشیم تا قابل نمایش باشه. در صورتی که برای پیاده سازی مدل نیازی به ویوکنترلر نیست. در واقع حتی نیاز نیست صفحه‌ی اول اپ چیزی داشته باشه! کافیه یه تست مناسب برای قسمتهای مختلفش بنویسیم تا مطمئن شیم درست کار می‌کنه. تازه داشتن test case های جامع بخصوص برای قسمت مدل، هزینه نگهداری کد را در آینده به شدت کاهش میده و اطمینان میده که برنامه همیشه درست کار خواهد کرد.

علاوه بر اینها، طراحی مدل می‌تونه تمام ریلیشن شیپ های بین ویوهای مختلفتون را مشخص کنه. اینجوری حتی می‌تونیم بفهمیم کدوم ویوها/ویوکنترلرها تکرار می‌شن و می‌تونید با جداسازیش، با یکبار پیاده سازی همه جا استفاده ش کنیم.

برای پیاده سازی مدل چهار مرحله‌ی اجباری هست و یه مرحله‌ی اختیاری. من مراحل را برای پلتفرم iOS/macOS توضیح میدم ولی برای سیستم‌های دیگه هم رویه مشابهی ولی با ابزارهای متفاوت برقرار هست.

مرحله‌ی نخست: تعریف ساختار

از نظر فنی این مرحله تعریف memory representation داده‌ها ست.

این قسمت از نظر پیاده سازی بسیار ساده ست. کافیه تعداد کلاس/استراکت تعریف کنیم که یه سری متغیر دارن! ولی خب طراحیش پیچیده ست. اینکه چه کلاسهایی باید تعریف شه مستلزم اینه که یه پلن تقریبا کامل از برنامه داشته باشیم. بدونیم چه صفحاتی داره و چه اطلاعاتی قراره نگهداری کنه. اگر قبلا بخش بک‌اند/سرور را داشته باشیم یا قبلا اپ را روی پلتفرم دیگه‌ای مثل اندرویید و وب پیاده کرده باشیم این مرحله قبلا انجام شده. کافیه همون json را معادل کنیم. حتی ابزارهایی مثل quicktype می‌تونه این قسمت را خیلی سریع کنه.

توی همین مرجله ریلیشن بین قسمتها و ویو/ویوکنترلر ها هم در میاد. از اونجا که توی پیاده سازی خوب هر ویوکنترلر دقیقا باید نماینده‌ی یه موجود توی مدل باشه، هر متغیری که توی یه کلاس داشته باشیم که تایپش یه کلاس تعریف شده‌ی دیگه باشه یعنی ویوکنترلر مربوط به متغیر، فرزند ویوکنترلر دیگه می‌شه.

این مرحله فی الواقع abstraction ساختار داده‌ی ما را امکان پذیر می‌کنه.

نکته‌ی جانبی: اگر هنوز بلد نیستید که چندین ویوکنترلر را توی یه صفحه یا ویوکنترلر مادر نمایش بدید، این ویدیوی آموزشی از فرزاد نظیفی را در مورد MVC 2.0 مشاهده کنید. توصیه‌ی من اینه که حتی سلول‌های کالکشن ویو اگر چیزی بیش از یه تصویر دارن، یه ویوکنترلر باشن.

مرحله‌ی دوم: سریالیزیشن (Serialization)

سریالیزیشن یعنی عملی که داده‌ی توی مموری یا رم را تبدیل به داده‌ی قابل ذخیره یا انتقال بکنه. بعبارتی هدف نهایی ما این هست که بتونیم استراکت یا کلاس خودمون را تبدیل به فرمتهای استانداردی مثل json یا فرمت‌های اختصاصی بکنیم تا به بک اند انتقال بدیم یا روی دیسک ذخیره کنیم. بستگی به اینکه می‌خوایم تحت اینترنت باشیم یا داده را روی دیسک ذخیره کنیم، اپروچ به این قسمت ممکنه فرق کنه.

برای انتقال روی وب، قبلا طراحی این قسمت کار آسونی نبود چون برای فرستادن باید همه‌ی متغیرها را به یه دیکشنری منتقل می‌کردیم و بعد با کلاس NSJSONSerialization اون را تبدیل به json می‌کردیم. برای خوندن هم باید با همین کلاس، فایل را تبدیل به دیکشنری می‌کردیم و متغیرهای کلاس را دونه دونه مقدار دهی می‌کردیم.

اگر هنوز از objective c استفاده می‌کنید هنوز راه حل به همین سختی هست. ولی توی سوییفت با استفاده از پروتکل‌های Encodable و Decodable (یا به صورت کلی Codable) این قسمت خیلی راحت شده. کافیه کلاس یا استراکت را از نوع Codable تعریف کنیم تا متدهای مربوط به تبدیل و سریالیزیشن خودشون ایجاد بشن.

البته هنوز اگر نیاز به اعتبارسنجی json دریافتی داریم مجبوریم دستی پیاده‌سازی کنیم.

گفتم اعتبارسنجی. این قسمت خیلی مهمی هست که معمولا توی این مرحله گم می‌شه. همین نکات کوچیک فرق یه برنامه‌ی معمولی و یه برنامه‌ی حرفه‌ای هست. مثلا فرض کنید یه فیلد تاریخ داریم که انتظار داریم از تاریخ عرضه‌ی برنامه عقب‌تر نباشه. یا تاریخ تغییر قطعا باید بعد از تاریخ ساخت یه مورد باشه. خب همین جا چک کنیم و اگر منطبق نبود، از ادامه‌ی خوندن فایل خودداری کنیم و یه پیغام خطای مناسب برای خراب بودن داده را نمایش بدیم. این قسمت باید توی متد decode پیاده سازی بشه و اگر اطلاعات دریافتی معتبر نبود، یه ارور مناسب throw بشه.

اما اگر می‌خوایم داده را روی دیسک یا جایی مثل NSUserDefaults ذخیره کنیم، بهتره کلاسمون را منطبق با پروتکل NSCoding کنیم و از NSKeyedArchiver و NSKeyedUnarchiver برای ذخیره و بازیابی استفاده کنیم. این سیستم تا حدود زیادی تضمین می‌کنه که اطلاعات را روی کلاس مناسب و مختص این نوع داده منتقل کنیم.

مرحله‌ی سوم: انتقال و ذخیره سازی یا Persistency

مرحله‌ی قبلی انجام شد تا بتونیم این مرحله را پیاده سازی کنیم. حالا فایل json یا دیتایی داریم که می‌تونیم ذخیره کنیم یا بفرستیم. یا اینکه فایل json را از سرور بگیریم. برای این مرحله باید روی شبکه مسلط باشیم ولی خب توی iOS یه سیستم NSURLSession هست که کار باهاش خیلی راحته و امکانات خیلی خوبی هم داره. فقط کافیه یه ریکوئست مناسب بسازیم و بفرستیم به سرور تا دیتای خام دریافتی را در اختیارمون بذاره. حالا همین داده را در اختیار متدهای مرحله ۲ میذاریم.

یه سری third-party library مثل Alamofire هست که در عمل یه wrapper روی همین URLSession هست. اگر با اونها راحتترید یا روشون مسلط هستید خب از اونا استفاده کنید ولی اگر تازه کارید حتما تمرکزتون را روی یادگیری URLSession بذارید.

یه مزیت عمده‌ی URLSession نسبت به Alamorfire روی کانفیگ بکگراند هست. این امکان وقتی به درد میخوره که برنامه حالت آفلاین هم داره. از اونجا که دانلودها و آپلودهای سیشن بکگراند مستقل از برنامه ست،‌ می‌تونیم زمانبندی کنیم که در تاریخ خاصی ارسال بشن (البته iOS 11.0). یا اینکه فرض کنید می‌خواید هروقت اینترنت وصل شد، دیتای سابمیت شده توسط کاربر فرستاده بشه (کاری که اپلیکیشنهای ایمیل می‌کنن!) خب اینجا کانفیگ بک‌گراند کار را راه میندازه. Task یی که روی سیشن بکگراند ایجاد می‌کنیم تا ۷ روز منتظر وصل شدن اینترنت میمونه و موقع اتصال حتی اگر برنامه در حال اجرا نباشه، داده را آپلود/دانلود می‌کنه. البته کار با سیشن بکگراند نکاتی داره، مثلا نمیتونیم از کلوژر/بلاک استفاده کنیم (خطای runtime میده) و دیتای دریافتی را حتما باید با دلگیت هندل کنیم. همچنین برای آپلود حتما باید فایل روی دیسک باشه.

مرحله‌ی چهارم: پیاده سازی Cache

این مرحله اختیاری هست. ممکنه برنامه‌ واقعا بهش احتیاجی نداشته باشه. ولی این مرحله دقیقا فرق یه برنامه‌ی متوسط و یه برنامه‌ی خوب هست. کاربر انتظار نداره عکس یا داده‌ای که قبلا دانلود شده مجدد دانلود بشه! و انتظار حداکثر سرعت از برنامه را داره. پیاده ساری صحیح این قسمت خیلی مهم هست چون پیاده سازی نادرست می‌تونه مقدار زیادی از رم را مصرف کنه که توی iOS منجر به کرش برنامه می‌شه! به همین سادگی ممکنه کاربر شاکی بشه!

خود پلتفرم سه تا کلاس مناسب در اختیار ما میذاره. در ۹۰ درصد موارد همینها کافیه. ممکنه از کلاسهای third-party هم استفاده کنید براش که کارتون راحتتر بشه.

اولین کلاسی که باید بلد باشیم NSCache هست. این کلاس عین Dictionary هست منتهی با سه تا مزیت برای کار ما:

۱- برخلاف دیکشنری کاملا thread-safe هست. از اونجا که لود داده و آپدیت روی تردهای پس‌زمینه انجام می‌شه این موضوع خیلی مهم هست. عدم توجه به این بخش باعث می‌شه برنامه شما از هر صد دفعه، یه دفعه کرش کنه که خوب نیست

۲- مدیریت حافظه روش خیلی بهینه هست. برخلاف دیکشنری عادی، خالی کردنش مستلزم دسترسی به مقادیر ذخیره شده نیست. علاوه بر این خود سیستم توی شرایط تحت فشار خودش کش را پاک می‌کنه.

۳- بهش می‌شه حد بالا داد که برای کش، تا مقدار زیادی از رم مصرف نشه و سهمیه بندی بکنیم.

البته این کلاس برخلاف دیکشنری سوییفت، فقط با انواع سازگار با objective-c یعنی کلاس‌های مشتق از NSObject کار می‌کنه. (مثل NSString یا NSURL یا UIImage)

این کلاس داده‌ها را روی رم نگهداری می‌کنه و نیاز به دسترسی به دیسک را کاهش میده. گزینه‌ی ایده‌آلی برای نگهداری thumbnailها و لیست ها هست.

کلاس دوم NSURLCache هست که می‌تونه response های برگشته از سرور را ذخیره کنه. در واقع قبل از درخواست جدید به سرور می‌تونیم چک کنیم که آیا این مورد را قبلا گرفتیم یا نه. محدودیت‌های روی دیسک و رم هم قابل تنظیم هست. دقت کنید که برای کانفیک URLSession می‌تونیم urlCache تعریف کنیم تا کار تقریبا اتومات بشه. البته باید سرور شما هدر Cache-Control بفرسته تا این سیستم خیلی خوب کار کنه. ولی می‌تونید ریسپانسها را توش دستی ذخیره و دریافت کنید.

کلاس سوم NSPurgableData هست که دقیقا مثل NSMutableData هست ولی این امکان را میده که در مواقع لازم خود سیستم تخلیه‌ش کنه. سعی کنید دیتاهایی که میتونید از سرور مجدد بگیرید توی این ذخیره کنید تا به رم فشار نیاد.

نکته‌ی جانبی اینکه CGImage/UIImage خیلی حافطه می‌گیره. دقیقا ۴ بایت برای هر پیکسل یا بعبارتی ۸۰ مگ برای تصویر ۲۰ مگاپیکسلی! برای کوچیک کردن تصویر (یعنی درست کردن thumbnail)، بدون اینکه به رم فشار بیاد از این روتین استفاده کنید تا موقع پردازش تصویر peak مموری نداشته باشید. اگر تصویر خیلی بزرگ هست و نیاز به زوم نیست، با همین روش می‌تونید کوچکترش کنید (دقیقا اندازه‌ی ویوی مقصد) و بعد نمایش بدید.

اگر برنامه امکان offline داشته باشه، باید مقداری زحمت بکشیم و قسمتهای تکمیلی را با استفاده از کلاس NSKeyedArchiver پیاده سازی کنیم. برای یادگیری ذخیره و بازیابی حالت برنامه اینجا را بخونید.

مرحله پنجم: نوشتن test case

این مرحله اکثرا به طور کامل نادیده گرفته می‌شه. به نظر من شاید برای قسمت UI بتونیم از خیر تست بگذریم اما برای قسمت مدل تنبلی موجب خسران هست. کوچکترین تغییری در سمت سرور ممکنه بدون اینکه متوجه بشیم قسمتی از اپ را از کار بندازه. ولی با وجود تست اطمینان کاملی به کارکرد قسمت مدل خواهیم داشت. تازه نوشتن تست کیس برای مدل خیلی ساده ست.

نوشتن تست بسیار ساده ست. آموزشش را اینجا بخونید. ولی سوالی که مطرح می‌شه اینه که خب چه چیزایی باید تست بشه و چه چیزایی را بنویسیم. برای هر کدوم از ۴ مرحله‌ی بالا باید تست نوشته بشه. اگر این اصول رعایت شه، یه تست کیس خوب خواهیم داشت:

۱- باید کلیه‌ی متغیرهای computed و توابع تست بشن که داده‌ی مناسبی را با توجه به متغیرهای ذخیره‌ای و ورودی‌هاشون برمی‌گردونن. و در مقابل ورودی غیرمتعارف چه واکنشی میدن.

۲- باید مطمئن شیم قسمت سریالیزیشن بدون مشکل هست. کافیه یه json آماده داشته باشیم (یا از سرور بگیریم) و چک کنیم کلاسهای مربوطه ساخته میشن یا ارور میده. همچنین کلاسهای مقداردهی شده آیا json مورد انتظار ما را تولید می‌کنن یا خیر.

۳- باید مطمئن شیم اعتبارسنجی یا validation مقادیر به درستی صورت می‌گیره. کافیه یه سری json مشکل‌دار داشته باشیم و سعی کنیم باهاشون کلاس را initialize کنیم. استرینگ بجای عدد، عدد بجای استرینگ یا تاریخهای در بازه‌ی غیرصحیح نمونه‌ی چیزایی هست که تست می‌شن.

۴- می‌تونیم (انتخابی) مطمئن شیم ارتباط با سرور برقرار می‌شه. دیتا مناسب دریافت می‌شه و جواب مناسب برای دیتا درست می‌فرسته. همچنین می‌تونیم منطق authentication کاربر را همینجا تست کنیم.

و درنهایت...

وقتی مطمئن شدیم مدل درست و مطابق انتظار کار می‌کنه، بدون درگیری ذهنی و اتلاف وقت خیلی سریع بریم روی پیاده سازی ویوها و ویوکنترلها.

نکته‌ی جانبی اینکه سعی کنید تا حد امکان طراحی شما ماژولار باشه تا اگر خواستید بتونید مدل خودتون را به‌صورت یه فریمورک جدا در بیارید.

اگر یه تیم باشه، خیلی راحت می‌شه همزمان با پیاده سازی قسمت مدل، یه دیزاینر داشت تا نمای نهایی برنامه را طراحی کنه. اینجوری سرعت طراحی برنامه چند برابر می‌شه و کارفرما راضی‌تر. همزمان چیزی هم برای ارایه به مدیر پروژه وجود داره. برای این کار استفاده از اپلیکیشن Sketch توصیه می‌شه که می‌تونید منابع مورد نیاز برای طراحی اینترفیس iOS را از خود سایت اپل دریافت کنید.