четверг, 11 января 2018 г.

iOS. Swift. Enum with associated values for UITableView, UICollectionView

Статья из категории `Я решил это так, возможно у тебя есть другое решение :)`.

К нам приходит очередная задача - `экран фильтров`, на котором отображаются диапазон цен, категории ( можно добавлять несколько категорий к фильтру ), тип продавца (магазин, частный продавец) и тд.

Хороший вариант использовать UITableView или UICollectionView. Но как отобразить настолько разные данные в таблице?

Для таблицы нам нужен массив элементов и хотелось бы не делать никаких преобразований данных в методе cellForRowAtIndexPath. Так как фильтра содержать достаточно разные данные,  а элемент массива у нас должен быть одного типа, нам нужно как то сконвертировать модель фильтра в понятный формат для таблицы. Лучше всего в данном случае ( но это не точно ) подойдет enum.


Представление модели фильтра через enum для таблицы.

enum Item {
    case price(min: Int, max: Int, cur: Int)
    case seller(type: String)
    case categories(selected: [String], all: [String])
    ...
}


В результате использования enum-a мы можем сделать массив элементов для таблицы.

items = [
    .price(min: 100, max: 5000, cur: 1200), // ценовой диапазон и текущее значение
    .seller(type: "private"),
    .categories(selected: ["a"], all: ["a", "b", "c"]) // категории фильтра 
]


Полный пример выглядит следующим образом:

class FilterViewController {

    enum SellerType {
        case all, shop, private
    }

    enum Item {
        case price(min: Int, max: Int, cur: Int)
        case seller(type: SellerType)
        case categories(selected: [String], all: [String])
        ...
    }

    var items: [Item] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        items = [
            .price(min: 100, max: 5000, cur: 1200),
            .seller(type: .private),
            .categories(selected: ["a"], all: ["a", "b", "c"])
        ]
    }
}

extension FilterViewController: UITableViewDataSource {

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch(items[indexPath.row]) {
        case let .price(min, max, cur):
            let cell = tableView.dequeueReusableCell(withIdentifier: "PriceCell", for: indexPath)
            // отображаем min, max, cur в ячейке

            return cell

        case let .seller(type):
            let cell = tableView.dequeueReusableCell(withIdentifier: "SellerCell", for: indexPath)
            // отображаем type в ячейке (или переключаем switch, segmented control)

            return cell

        case let .categories(selected, all):
            let cell = tableView.dequeueReusableCell(withIdentifier: "CategoriesCell", for: indexPath)
            // отображаем selected, all в ячейке

            return cell
        }
    }
}

В результате наш метод `cellForRow` становится достаточно простым и не нагружен никакой бизнес логикой. Мы можем протестировать создание, обновление нашего массива элементов.

P.S. Оставляем комментарии, темы для рассмотрения. И кликаем +1, f, в, t, что располагаются чуть ниже статьи.

суббота, 18 ноября 2017 г.

iOS. Swift. Router with protocol composition

В данной статье хочу поделится своим вариантов как вынести переходы между экранами в iOS приложении в отдельный класс ( Router ). Как дополнять возможности router-a переходить на большее количество экранов при этом не копируя код и избегая наследования.

Если у нас приложение построено на MVC ( да и любой другой ) архитектуре, то мы часто можем столкнуться в коде ViewController-a со следующим вариантом показать экран ( пример перейти на экран деталей пользователя ):

func showUserProfile() {
    let profile = UserProfileViewController()

    self.show(profile, sender: nil)
}

Теперь нам нужно перейти на экран деталей пользователя например из списка контактов, результатов поиска или из пин-а на карте. Итого получается что нам нужно открыть экран деталей пользователя из нескольких ViewController-ов.

Как вынести метод showUserProfile в одно место?

Для решение задачи можно создать отдельный класс ( в нашем варианте UserRouter ) и скопировать туда метод. Получаем:

class UserRouter {

    func showUserProfile(from controller: UIViewController) {
        let profile = UserProfileViewController()

        controller.show(profile, sender: nil)
    }
}

Теперь нам нужно подключить наш роутер во все controller-ы где мы будем использовать метод showUserProfile. Итого получаем:

UsersListViewController.swift
class UsersListViewController {

    var router: UserRouter?

    ...

    router?.showUserProfile(from: self)

    ...
}

SearchResultViewController.swift
class SearchResultViewController {

    var router: UserRouter?

    ...

    router?.showUserProfile(from: self)

    ...
}

Вроде все хорошо и мы получили что хотели.

Но теперь нам потребовалось добавить переход на экран добавления нового пользователя из экрана UsersListViewController. Можно просто добавить метод showAddUserScreen метод в UserRouter, но SearchResultViewController-у этот метод не нужен и не хотелось бы видеть лишние метод при использовании router-a. Простой вариант создаем новый роутер UsersListRouter наследуемся от UserRouter и добавляем метод showAddUserScreen. Получаем:

class UsersListRouter: UserRouter {

    func showAddUserScreen(from controller: UIViewController) {
        let add = AddUserViewController()

        controller.show(add, sender: nil)
    }
}

Итого наш UsersListController может открыть экран деталей пользователи и экран добавления нового пользователя. В свою очередь SearchResultController может только открыть экран деталей пользователя. В результате мы не нарушили наше правило НЕ дублировать код.

Наш проект не стоит на месте и добавление нового функционала влечет за собой изменения в нашем коде.
Пришло новое требование: добавить возможность отравления сообщения из UsersListViewController и из UserProfileViewController.

Сейчас нам нужно сделать так чтобы
UserListViewController мог переходить на экраны: UserProfile, AddUser, SendMessage,
SearchResultViewController на экраны: UserProfile
UserProfileViewController на экраны: SendMessage

Наследование здесь не поможет потому что так или иначе какой то класс будет видеть методы ( возможность ) перехода на экран, на который ему переходить не надо :)

В данном случае нам помогут protocol и protocol extension. Изменим наш UserRouter:

UserRouter.swift
protocol UserRouter {

    func showUserProfile(from controller: UIViewController)
}

extension UserRouter {

    func showUserProfile(from controller: UIViewController) {
        let profile = UserProfileViewController()

        controller.show(profile, sender: nil)
    }
}

Добавим новый роутер SystemToolsRouter:

SystemToolsRouter.swift
protocol SystemToolsRouter {

    func showSendMessageScreen(from controller: UIViewController)
}

extension SystemToolsRouter {

    func showSendMessageScreen(from controller: UIViewController) {
        let sendMessage = SendMessageViewController()

        controller.show(sendMessage, sender: nil)
    }
}

И добавим изменения в наши роутеры экранов:

UsersListRouter.swift
class UsersListRouter: SystemToolsRouter, UserRouter {

    func showAddUserScreen(from controller: UIViewController) {
        let add = AddUserViewController()

        controller.show(add, sender: nil)
    }
}

SearchResultRouter.swift
class SearchResultRouter: UserRouter {

}

UsersProfileRouter.swift
class UserProfileRouter: SystemToolsRouter {

}

В результате наши контроллеры будут выглядеть:

UsersListViewController.swift
class UsersListViewController {

    var router: UsersListRouter?

    ...

    router?.showUserProfile(from: self)
    router?.showSendMessageScreen(from: self)
    router?.showAddUserScreen(from: self)

    ...
}


SearchResultViewController.swift
class SearchResultViewController {

    var router: SearchResultRouter?

    ...

    router?.showUserProfile(from: self)

    ...
}


UserProfileViewController.swift
class UserProfileViewController {

    var router: UserProfileRouter?

    ...

    router?.showSendMessageScreen(from: self)

    ...
}

Итого при использовании protocol composition, protocol extension мы легко можем добавлять переходы на другие экраны не используя наследования и дублирования кода.

P.S. Оставляем комментарии, темы для рассмотрения. И кликаем +1, f, в, t, что располагаются чуть ниже статьи.

пятница, 12 июля 2013 г.

iOS Image Loader. Загрузка картинок.

В этой статье я покажу как пользоваться небольшой библиотечкой ( собственной разработки ) для загрузки картинок. Так как все время копировать код и подправлять немного мне надоело, решил написать загрузку с простым кэшированием картинок. Сделал так чтобы можно было подключить как говорится в один клик. Библиотека называется DLImageLoader.

Создаем проект. Добавляем в него библиотеку.

Я покажу пример на UITableView.
Наследуем ViewController от UITableViewController.
Изменяем класс ViewController должно получиться примерно вот так:

 #import "ViewController.h"  
 #import "DLImageLoader.h"  

 @interface ViewController ()  

 @property (nonatomic, strong) NSArray *urls;  

 @end  

 @implementation ViewController  

 - (id)initWithStyle:(UITableViewStyle)style {  
   self = [super initWithStyle:style];  
   if (self) {  
     // Custom initialization  
   }  
   return self;  
 }  
 
- (void)viewDidLoad {  
   [super viewDidLoad];  
   self.urls = [NSArray arrayWithObjects:@"url to image here", nil];  
 }  

 - (void)didReceiveMemoryWarning {  
   [super didReceiveMemoryWarning];  
   // Dispose of any resources that can be recreated.  
 }  

 #pragma mark - Table view data source  

 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {  
   return 1;  
 }  

 - (NSInteger)tableView:(UITableView *)tableView  
  numberOfRowsInSection:(NSInteger)section {  
   return [self.urls count];  
 }  

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {  
   static NSString *CellIdentifier = @"Cell";  
   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];  
   if (cell == nil) {  
     cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault  
                    reuseIdentifier:CellIdentifier];  
   }  
   // Configure the cell...  
   cell.imageView.image = nil;  
   [[DLImageLoader sharedInstance] loadImageFromURL:[self.urls objectAtIndex:indexPath.row]  
                         completed:^(NSError *error, NSData *imgData) {  
                             cell.imageView.image = [UIImage imageWithData:imgData];  
                             [cell setNeedsLayout];  
   }];  
   return cell;  
 }  
 @end  

Вот и все.



Библиотека c полным примером -> DLImageLoader-iOS

среда, 11 июля 2012 г.

Android. Themes and Styles. Использование тем и стилей.

Иногда приходится сталкиваться с необходимостью оформления приложения в разном цвете не изменяя расположения и размеров UI элементов.

В этой статье расскажу об использовании тем и стилей в android.
Будет показано как применять стили для Button, EditText, ListView, TextView.
Для начала создадим новый проект и добавим в него два новых activity.
Для одного мы будем применять Theme.Green для второго Theme.Blue.

Я проект назвал AndroidStyle. Дополнительные activity: BlueThemeActivity, GreenThemeActivity.
Вот так вот выглядит только-что созданный проект:


Не забываем прописать activity в AndroidManifest файле.

 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
   package="com.lunevich.androidstyle"  
   android:versionCode="1"  
   android:versionName="1.0" >  
   //...

   <application  
     android:icon="@drawable/ic_launcher"  
     android:label="@string/app_name" >  
     //...

     <activity android:name=".activity.BlueThemeActivity" ></activity>  
     <activity android:name=".activity.GreenThemeActivity" ></activity>  

   </application>  
 </manifest>  

Теперь добавим layout которому мы будем менять тему. Назовем stylish.xml, добавим в него наши  UI элементы. Должно получиться что-то такое:

stylish.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   android:layout_width="fill_parent"  
   android:layout_height="fill_parent"  
   android:orientation="vertical" >  
   <TextView  
     android:layout_width="fill_parent"  
     android:layout_height="wrap_content"  
     android:text="@string/app_name" />  
   <Button  
     android:id="@+id/btn"  
     android:layout_width="fill_parent"  
     android:layout_height="wrap_content"  
     android:layout_margin="3dp"  
     android:text="@string/btn" />
   <LinearLayout  
     android:layout_width="fill_parent"  
     android:layout_height="wrap_content"  
     android:layout_marginBottom="10dp"  
     android:layout_marginTop="10dp"  
     android:orientation="horizontal" >  
     <TextView  
       android:id="@+id/tv1"  
       style="?attr/textView1"  
       android:layout_width="fill_parent"  
       android:layout_height="wrap_content"  
       android:layout_weight="1"  
       android:gravity="center"  
       android:text="@string/tv1"  
       android:textAppearance="?android:attr/textAppearanceMedium" />  
     <TextView  
       android:id="@+id/tv2"  
       style="?attr/textView2"  
       android:layout_width="fill_parent"  
       android:layout_height="wrap_content"  
       android:layout_weight="1"  
       android:gravity="center"  
       android:text="@string/tv2"  
       android:textAppearance="?android:attr/textAppearanceMedium" />  
   </LinearLayout>  
   <EditText  
     android:id="@+id/editText"  
     android:layout_width="fill_parent"  
     android:layout_height="wrap_content"  
     android:layout_marginBottom="10dp"  
     android:ems="10"  
     android:inputType="text" >  
     <requestFocus />  
   </EditText>  
   <ListView  
     android:id="@+id/listView"  
     android:layout_width="fill_parent"  
     android:layout_height="fill_parent" >  
   </ListView>  
 </LinearLayout>  

Чтобы определить наши style="?attr/..." выделенные жирным шрифтом в stylish.xml нам необходимо добавить файл attrs.xml. Вот такой файл получается:

attrs.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <resources>  
   <attr name="textView1" format="reference" />  
   <attr name="textView2" format="reference" />  
 </resources>  

Можно приступить к написанию стилей. Я покажу пример для синего стиля.
В папке res/values/ создадим файл styles.xml и напишем в него наш синий стиль для UI элементов.
Вот такой файл должен получиться:

styles.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <resources>  

   <!-- =============== -->  
   <!--   Blue styles   -->  
   <!-- =============== --> 
 
   <!-- blue text view style -->  
   <style name="blue_text_view_1">  
       <item name="android:textColor">#60c4e4</item>  
       <item name="android:textSize">14sp</item>  
       <item name="android:gravity">center_vertical|left</item>  
       <item name="android:shadowDx">1.0</item>  
       <item name="android:shadowDy">1.0</item>  
       <item name="android:shadowRadius">1</item>  
       <item name="android:shadowColor">#DDD</item>  
   </style>  

   <!-- blue text view style -->  
   <style name="blue_text_view_2">  
       <item name="android:textColor">#60c4e4</item>  
       <item name="android:textSize">24sp</item> 
       <item name="android:textStyle">bold</item>  
       <item name="android:gravity">center_vertical|left</item>  
       <item name="android:shadowDx">1.0</item>  
       <item name="android:shadowDy">1.0</item>  
       <item name="android:shadowRadius">1</item>  
       <item name="android:shadowColor">#DDD</item>  
   </style>  

   <!-- blue button style -->  
   <style name="blue_button" parent="@android:style/Widget.Button">  
       <item name="android:background">@drawable/blue_button</item>   
   </style>  

   <!-- blue edit text style -->  
   <style name="blue_edit_text" parent="@android:style/Widget.EditText">  
       <item name="android:background">@drawable/blue_edit_text</item>  
   </style>  

   <!-- blue edit text style -->  
   <style name="blue_list_view">  
       <item name="android:listSelector">@drawable/blue_list_view_item</item>  
   </style> 
  
 </resources>  
В нашем стиле используются drawable ресурсы drawable/blue_button.xml, drawable/blue_edit_text.xml, drawable/blue_list_view_item.xml. Для blue_button, blue_edit_text и blue_list_view_item создадим в папке res  папку drawable и в ней файл blue_button.xml, blue_edit_text.xml и blue_list_view_item.xml. Добавим в наши созданные файлы вот такое контент ( он отвечает за внешний вид наших UI элементов ):

blue_button.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <selector xmlns:android="http://schemas.android.com/apk/res/android">  
   <item android:state_pressed="true">  
     <shape>  
       <solid android:color="#449def" />  
       <stroke android:width="1dp" android:color="#2f6699" />  
       <corners android:radius="3dp" />  
       <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />  
     </shape>  
   </item>  
   <item>  
     <shape>  
       <gradient android:angle="270" android:endColor="#2f6699" android:startColor="#449def" />  
       <stroke android:width="1dp" android:color="#2f6699" />  
       <corners android:radius="4dp" />  
       <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />  
     </shape>  
   </item>  
 </selector>  
В blue_button.xml  прописан внешний вид кнопки в нажатом и не нажатом состоянии.
Атрибут android:state_pressed="true" указывает на то что данный стиль применим для кнопки в нажатом состоянии.

blue_edit_text.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <selector xmlns:android="http://schemas.android.com/apk/res/android">  
   <item android:state_pressed="true">  
     <shape>  
       <gradient android:angle="270" android:centerColor="#603C86C9" android:centerX="0.5" android:centerY="0.5" android:endColor="#9045A0F5" android:startColor="#403270A8" />  
       <stroke android:width="3dp" android:color="#50449def" />  
       <corners android:radius="7dp" />  
       <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />  
     </shape>  
   </item>  
   <item>  
     <shape android:shape="rectangle">  
       <solid android:color="#20486E" />  
       <stroke android:width="3dp" android:color="#50449def" />  
       <corners android:radius="7dp" />  
       <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />  
     </shape>  
   </item>  
 </selector>  
В blue_edit_text.xml прописан внешний вид EditText в активном и неактивном состоянии.
Атрибут android:state_pressed="true" указывает на то, какой стиль будет использоваться когда мы нажмем на EditText для ввода текста.

blue_list_view_item.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <selector xmlns:android="http://schemas.android.com/apk/res/android">  
   <item android:state_pressed="true">  
     <shape>  
       <gradient android:angle="270" android:centerColor="#603C86C9" android:centerX="0.5" android:centerY="0.5" android:endColor="#9045A0F5" android:startColor="#403270A8" />  
       <stroke android:width="3dp" android:color="#50449def" />  
       <corners android:radius="7dp" />  
       <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />  
     </shape>  
   </item>  
   <item android:state_focused="true">  
     <shape android:shape="rectangle">  
       <solid android:color="#20486E" />  
       <stroke android:width="3dp" android:color="#50449def" />  
       <corners android:radius="7dp" />  
       <padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />  
     </shape>  
   </item>  
 </selector>  

Теперь непосредственно создадим тему для наших стилей. Для этого в папке res/values/ создадим файл themes.xml. И применим в нем для наших атрибутов созданные стили.

themes.xml
 <?xml version="1.0" encoding="utf-8"?>  
 <resources>  

   <!-- =============== -->  
   <!--   Blue theme    -->  
   <!-- =============== -->  

   <style name="Theme.Blue" parent="@android:style/Theme">  
     <item name="textView1">@style/blue_text_view_1</item> 
     <item name="textView2">@style/blue_text_view_2</item> 
     <item name="android:buttonStyle">@style/blue_button</item> 
     <item name="android:editTextStyle">@style/blue_edit_text</item>  
     <item name="android:listViewStyle">@style/blue_list_view</item>    
   </style>  
 </resources>  
textView1 и textView2 наши собственные атрибуты, которые мы создали ранее в attrs.xml.
android:buttonStyle, android:editTextStyle атрибуты по-умолчанию.

Теперь мы сделали все что необходимо для стилей и тем в нашем приложении. Применить созданный стиль мы можем в AndroidManifest файле для выбранного нами activity или же ( как сделал я ) определить стиль непосредственно в activity.

В AndroidManifest.xml это желается вот так:
 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
   package="com.lunevich.androidstyle"  
   android:versionCode="1"  
   android:versionName="1.0" >  
   //...

   <application  
     android:icon="@drawable/ic_launcher"  
     android:label="@string/app_name" >  
     //...

     <activity android:name=".activity.BlueThemeActivity" android:theme="@style/Theme.Blue"></activity>  
     <activity android:name=".activity.GreenThemeActivity" android:theme="@style/Theme.Green"></activity>  

   </application>  
 </manifest>  

В activity делается вот так:
 package com.lunevich.androidstyle.activity;  

 import com.lunevich.androidstyle.R; 
 
 import android.app.Activity;  
 import android.os.Bundle;  
 import android.view.View;  
 import android.view.View.OnClickListener;  
 import android.widget.ArrayAdapter;  
 import android.widget.Button;  
 import android.widget.ListView;  

 public class BlueThemeActivity extends Activity implements OnClickListener { 
 
      @Override  
      public void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);
  
           setTheme(R.style.Theme_Blue);  
           setContentView(R.layout.stylish);  

           String[] items = { "One", "Two", "Three", "Four", "Five" };  
           ListView listView = (ListView) findViewById(R.id.listView);  
           listView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items));  

           Button btn = (Button) findViewById(R.id.btn);  
           btn.setOnClickListener(this);  
      }  

      @Override  
      public void onClick(View v) {  
           switch (v.getId()) {  
           case R.id.btn:  
                onBackPressed();  
                break;  
           }  
      } 
 
      @Override  
      public void onBackPressed() {  
           super.onBackPressed();  
           this.finish();  
      }  
 }  


Посмотрим на результаты:

blue-theme
green-theme

полный код приложения лежит вот тут -> AndroidStyle
прямая ссылка для скачивания архива -> Download

Вышло очень даже симпатично :). Спасибо за внимание.

P.S. Оставляем пожелания, комментарии, темы для рассмотрения. И кликаем +1, f, в, t, что располагаются чуть ниже статьи.