Jak uniknąć dużego i niezgrabnego UITableViewController na iOS?

36

Mam problem z implementacją wzorca MVC na iOS. Przeszukałem Internet, ale wydaje się, że nie znalazłem żadnego fajnego rozwiązania tego problemu.

Wiele UITableViewControllerwdrożeń wydaje się być dość dużych. Większość przykładów, które widziałem, pozwala na UITableViewControllerwdrożenie <UITableViewDelegate>i <UITableViewDataSource>. Te wdrożenia są dużym powodem, dla którego UITableViewControllerrobi się duże. Jednym rozwiązaniem byłoby stworzenie osobnych klas, które implementują <UITableViewDelegate>i <UITableViewDataSource>. Oczywiście klasy te musiałyby mieć odniesienie do UITableViewController. Czy korzystanie z tego rozwiązania ma jakieś wady? Ogólnie myślę, że powinieneś przekazać tę funkcjonalność innym klasom „pomocników” lub podobnym, korzystając ze wzoru delegowania. Czy istnieją jakieś dobrze ustalone sposoby rozwiązania tego problemu?

Nie chcę, aby model zawierał zbyt wiele funkcji ani widoku. Uważam, że logika powinna naprawdę należeć do klasy kontrolerów, ponieważ jest to jeden z fundamentów wzorca MVC. Ale najważniejsze pytanie brzmi:

Jak podzielić kontroler implementacji MVC na mniejsze części, którymi można zarządzać? (W tym przypadku dotyczy MVC w iOS)

Być może istnieje ogólny wzorzec rozwiązania tego problemu, chociaż konkretnie szukam rozwiązania dla systemu iOS. Podaj przykład dobrego wzoru rozwiązania tego problemu. Podaj argument, dlaczego Twoje rozwiązanie jest niesamowite.

Johan Karlsson
źródło
1
„Także argument, dlaczego to rozwiązanie jest niesamowite.” :)
occulus
1
To trochę poza tym, ale UITableViewControllermechanika wydaje mi się dość dziwna, więc mogę odnieść się do problemu. Jestem naprawdę zadowolony, że stosowanie MonoTouch, ponieważ MonoTouch.Dialogspecjalnie robi to że dużo łatwiej jest pracować z tabelami na iOS. Tymczasem jestem ciekawy, co mogą zasugerować tu inni, bardziej kompetentni ludzie ...
Patryk Ćwiek,

Odpowiedzi:

43

Unikam używania UITableViewController, ponieważ nakłada wiele obowiązków na pojedynczy obiekt. Dlatego oddzielam UIViewControllerpodklasę od źródła danych i deleguję. Obowiązkiem kontrolera widoku jest przygotowanie widoku tabeli, utworzenie źródła danych z danymi i połączenie tych elementów. Zmiana sposobu reprezentacji widoku tabeli może być wykonana bez zmiany kontrolera widoku, a w rzeczywistości ten sam kontroler widoku może być używany dla wielu źródeł danych, które wszystkie stosują ten wzorzec. Podobnie zmiana przepływu pracy aplikacji oznacza zmiany w kontrolerze widoku bez obawy o to, co stanie się z tabelą.

Próbowałem rozdzielić protokoły UITableViewDataSourcei UITableViewDelegatena różne obiekty, ale zwykle kończy się to fałszywym podziałem, ponieważ prawie każda metoda na uczestniku musi wkopać się w źródło danych (np. Po wybraniu uczestnik musi wiedzieć, który obiekt jest reprezentowany przez wybrany wiersz). W rezultacie otrzymuję pojedynczy obiekt, który jest zarówno źródłem danych, jak i delegatem. Ten obiekt zawsze zapewnia metodę, w -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathktórej zarówno źródło danych, jak i delegat muszą wiedzieć, nad czym pracują.

To moja separacja problemów na „poziomie 0”. Poziom 1 angażuje się, jeśli muszę reprezentować różnego rodzaju obiekty w tym samym widoku tabeli. Na przykład wyobraź sobie, że musiałeś napisać aplikację Kontakty - dla jednego kontaktu możesz mieć wiersze reprezentujące numery telefonów, inne wiersze reprezentujące adresy, inne reprezentujące adresy e-mail i tak dalej. Chcę uniknąć tego podejścia:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

Jak dotąd zaprezentowano dwa rozwiązania. Jednym z nich jest dynamiczne skonstruowanie selektora:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

W tym podejściu nie trzeba edytować epickiego if()drzewa, aby obsługiwać nowy typ - wystarczy dodać metodę, która obsługuje nową klasę. Jest to świetne podejście, jeśli ten widok tabeli jest jedynym, który musi reprezentować te obiekty lub prezentować je w specjalny sposób. Jeśli te same obiekty będą reprezentowane w różnych tabelach z różnymi źródłami danych, to podejście załamuje się, ponieważ metody tworzenia komórek wymagają współużytkowania przez źródła danych - możesz zdefiniować wspólną nadklasę, która udostępnia te metody, lub możesz to zrobić:

@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Następnie w klasie źródła danych:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

Oznacza to, że każde źródło danych, które musi wyświetlać numery telefonów, adresy itp., Może po prostu zapytać o dowolny obiekt reprezentowany dla komórki widoku tabeli. Samo źródło danych nie musi już nic wiedzieć o wyświetlanym obiekcie.

„Ale czekaj”, słyszę hipotetyczny wtrącający się rozmówca, „czy to nie łamie MVC? Czy nie umieszczasz szczegółów widoku w klasie modelki?”

Nie, to nie psuje MVC. W tym przypadku kategorie można traktować jako implementację programu Decorator ; podobnie PhoneNumberjak klasa modelu, ale PhoneNumber(TableViewRepresentation)jest kategorią widoku. Źródło danych (obiekt kontrolera) pośredniczy między modelem a widokiem, więc architektura MVC nadal obowiązuje.

To wykorzystanie kategorii można również postrzegać jako dekorację w ramach Apple. NSAttributedStringjest klasą modelową, zawierającą tekst i atrybuty. AppKit zapewnia, NSAttributedString(AppKitAdditions)a UIKit zapewnia NSAttributedString(NSStringDrawing)kategorie dekoratorów, które dodają zachowanie rysunkowe do tych klas modeli.


źródło
Jaka jest dobra nazwa dla klasy, która działa jako delegat źródła danych i widoku tabeli?
Johan Karlsson,
1
@JohanKarlsson Często nazywam to źródłem danych. Być może jest trochę niechlujny, ale łączę je wystarczająco często, aby wiedzieć, że moje „źródło danych” jest adaptacją do bardziej ograniczonej definicji Apple.
1
W tym artykule: objc.io/issue-1/table-views.html zaproponowano sposób obsługi wielu typów komórek, w którym opracowano klasę komórki w cellForPhotoAtIndexPathmetodzie źródła danych, a następnie wywołano odpowiednią metodę fabryczną. Co oczywiście jest możliwe tylko wtedy, gdy określone klasy mogą zajmować określone wiersze. Twój system generowania widoków kategorii na modelach jest chyba bardziej elegancki w praktyce, choć może to niekonwencjonalne podejście do MVC! :)
Benji XVI
1
Próbowałem demonstrować ten wzór na github.com/yonglam/TableViewPattern . Mam nadzieję, że jest to przydatne dla kogoś.
Andrew
1
Zagłosuję za ostateczną odmową podejścia dynamicznego wyboru. Jest to bardzo niebezpieczne, ponieważ problemy pojawiają się tylko w czasie wykonywania. Nie ma zautomatyzowanego sposobu, aby upewnić się, że dany selektor istnieje i że został poprawnie wpisany, a tego rodzaju podejście ostatecznie się rozpadnie i będzie koszmarem do utrzymania. Drugie podejście jest jednak bardzo sprytne.
mkko
3

Ludzie często pakują się dużo w UIViewController / UITableViewController.

Delegowanie do innej klasy (nie kontrolera widoku) zwykle działa dobrze. Delegaci niekoniecznie potrzebują odwołania z powrotem do kontrolera widoku, ponieważ wszystkie metody delegowania otrzymują odniesienie do UITableView, ale będą potrzebować dostępu do danych, dla których delegują.

Kilka pomysłów na reorganizację w celu skrócenia długości:

  • jeśli konstruujesz komórki widoku tabeli w kodzie, rozważ załadowanie ich zamiast z pliku stalówki lub ze scenorysu. Scenorysy zezwalają na prototypowe i statyczne komórki tabeli - sprawdź te funkcje, jeśli nie jesteś zaznajomiony

  • jeśli twoje metody delegowania zawierają wiele instrukcji „if” (lub instrukcji switch), jest to klasyczny znak, że możesz dokonać refaktoryzacji

Zawsze wydawało mi się trochę zabawne, że to on UITableViewDataSourcebył odpowiedzialny za uzyskanie poprawnego kawałka danych i skonfigurowanie widoku, aby to pokazać. Jednym fajnym punktem refaktoryzacji może być zmiana danych w cellForRowAtIndexPathcelu uzyskania uchwytu na dane, które wymagają wyświetlenia w komórce, a następnie przekazanie utworzenia widoku komórki innemu delegatowi (np. Wykonanie CellViewDelegatelub podobnym), który zostanie przekazany w odpowiednim elemencie danych.

okluzja
źródło
To miła odpowiedź. Jednak w mojej głowie pojawia się kilka pytań. Dlaczego uważasz, że wiele instrukcji if (lub instrukcji switch) jest złym projektem? Czy naprawdę masz na myśli wiele zagnieżdżonych instrukcji if i switch? Jak ponownie uwzględnić czynniki, aby uniknąć instrukcji if lub switch?
Johan Karlsson,
@JohanKarlsson jedna technika polega na polimorfizmie. Jeśli musisz zrobić jedną rzecz z jednym typem obiektu, a coś innego z innym typem, uczyń te obiekty różnymi klasami i pozwól im wybrać pracę za Ciebie.
@GrahamLee Tak, znam polimorfizm ;-) Jednak nie jestem pewien, jak zastosować go w tym kontekście. Proszę o rozwinięcie tego.
Johan Karlsson,
@JohanKarlsson gotowe;)
2

Oto mniej więcej to, co obecnie robię, gdy napotykam podobny problem:

  • Przenieś operacje związane z danymi do klasy XXXDataSource (która dziedziczy z BaseDataSource: NSObject). BaseDataSource zapewnia kilka wygodnych metod, takich jak - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;podklasa zastępująca metodę ładowania danych (ponieważ aplikacje zwykle mają jakąś metodę ładowania pamięci podręcznej typu offlie, - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;tak, abyśmy mogli aktualizować interfejs użytkownika danymi buforowanymi odebranymi w LoadProgressBlock podczas aktualizacji informacji z sieci i w bloku zakończenia odświeżamy interfejs użytkownika nowymi danymi i usuwamy ewentualne wskaźniki postępu). Klasy te NIE są zgodne z UITableViewDataSourceprotokołem.

  • W BaseTableViewController (który jest zgodny z UITableViewDataSourcei UITableViewDelegateprotokoły) Mam odniesienie do BaseDataSource, które tworzą w regulator init. W UITableViewDataSourceczęści kontrolera po prostu zwracam wartości z dataSource (jak - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Oto mój cellForRow w klasie bazowej (nie trzeba zastępować w podklasach):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCell musi zostać zastąpione przez podklasy, a createCell zwraca UITableViewCell, więc jeśli chcesz niestandardową komórkę, również ją zastąp.

  • Po skonfigurowaniu podstawowych rzeczy (w rzeczywistości w pierwszym projekcie, który korzysta z takiego schematu, potem ta część może być ponownie wykorzystana) pozostaje BaseTableViewControllerpodklasa:

    • Przesłonięcie configCell (zwykle przekształca się w zapytanie źródła danych o obiekt o ścieżkę indeksu i przekazanie go do metody configWithXXX: komórki lub uzyskanie reprezentacji obiektu UITableViewCell jak w odpowiedzi użytkownika 4051)

    • Zastąp didSelectRowAtIndexPath: (oczywiście)

    • Napisz podklasę BaseDataSource, która zajmie się pracą z niezbędną częścią Modelu (załóżmy, że istnieją 2 klasy, Accounta Languagewięc podklasy będą to AccountDataSource i LanguageDataSource).

I to wszystko dotyczy części widoku tabeli. W razie potrzeby mogę opublikować kod na GitHub.

Edycja: niektóre rekomendacje można znaleźć na stronie http://www.objc.io/issue-1/lighter-view-controllers.html (która ma link do tego pytania) oraz artykuł towarzyszący o kontrolerach tableview.

Timur Kuchkarov
źródło
2

Uważam, że model musi podać tablicę obiektów o nazwie ViewModel lub viewData zawartych w konfiguratorze cellConfigurator. CellConfigurator przechowuje CellInfo potrzebną do usunięcia z pamięci i skonfigurowania komórki. przekazuje komórce trochę danych, aby mogła ona sama się skonfigurować. działa to również z sekcją, jeśli dodasz jakiś obiekt SectionConfigurator, który zawiera CellConfigurators. Zacząłem używać tego jakiś czas temu, po prostu dając komórce viewData i miałem ViewController zajmujący się usuwaniem kolejki z komórki. ale przeczytałem artykuł wskazujący na to repozytorium gitHub.

https://github.com/fastred/ConfigurableTableViewController

może to zmienić sposób, w jaki do tego podchodzisz.

Pascale Beaulac
źródło
2

Niedawno napisałem artykuł o wdrażaniu delegatów i źródeł danych dla UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

Główną ideą jest podzielenie obowiązków na osobne klasy, takie jak fabryka ogniw, fabryka sekcji i zapewnienie ogólnego interfejsu dla modelu, który wyświetli UITableView. Poniższy schemat wyjaśnia wszystko:

wprowadź opis zdjęcia tutaj

Piotr Wach
źródło
Ten link już nie działa.
koen
1

W następstwie SOLID zasady będą rozwiązywać wszelkiego rodzaju problemy jak te.

Jeśli chcesz się swoimi klasami mieć tylko jeden odpowiedzialność, należy zdefiniować odrębny DataSourcei Delegatezajęcia i po prostu wstrzyknąć je do tableViewwłaściciela (może być UITableViewControllerlub UIViewControllerczy cokolwiek innego). W ten sposób przezwyciężysz rozłąkę .

Ale jeśli chcesz mieć czysty i czytelny kod i chcesz pozbyć się tego ogromnego pliku viewController i jesteś w Swif , możesz do tego użyć extensions. Rozszerzenia pojedynczej klasy mogą być zapisywane w różnych plikach i wszystkie mają do siebie dostęp. Ale to jest naprawdę rozwiązuje problem SoC, jak wspomniałem.

Mojtaba Hosseini
źródło