İOS'ta büyük ve sakar UITableViewController nasıl önlenir?


36

MVC şablonunu iOS'ta uygularken bir sorunum var. İnterneti aradım ancak bu soruna güzel bir çözüm bulamadım.

Birçok UITableViewControlleruygulama oldukça büyük görünmektedir. Gördüğüm En örnekler sağlar UITableViewControlleruygulamak <UITableViewDelegate>ve <UITableViewDataSource>. Bu uygulamalar neden büyüdüğü için büyük bir sebep UITableViewController. Bir çözüm <UITableViewDelegate>ve uygulayan ayrı sınıflar oluşturmak olacaktır <UITableViewDataSource>. Tabii ki bu sınıflar için bir referans olması gerekirdi UITableViewController. Bu çözümü kullanarak herhangi bir sakınca var mı? Genel olarak, işlevsellik temsilcisini kullanarak diğer "Yardımcı" sınıflara veya benzerlerine işlevsellik devretmeniz gerektiğini düşünüyorum. Bu sorunu çözmenin iyi bilinen bir yolu var mı?

Modelin çok fazla işlevsellik veya görünüm içermesini istemiyorum. Mantığın gerçekten denetleyici sınıfında olması gerektiğine inanıyorum, çünkü bu MVC modelinin temel taşlarından biri. Ancak büyük soru şudur:

Bir MVC uygulamasının denetleyicisini daha küçük yönetilebilir parçalara nasıl bölmelisiniz? (Bu durumda iOS'ta MVC için geçerlidir)

Özellikle iOS için bir çözüm aradığım halde, bunu çözmek için genel bir model olabilir. Lütfen bu sorunu çözmek için iyi bir örnek veriniz. Lütfen çözümünüzün neden harika olduğuna dair bir argüman sunun.


1
"Ayrıca, bu çözümün neden harika olduğu konusunda bir tartışma." :)
occulus

1
Meselenin yanında biraz, ama UITableViewControllermekanik bana çok tuhaf geliyor, bu yüzden sorunla ilgili olabilirim. Aslında sevindim kullanacağım MonoTouch, çünkü MonoTouch.Dialogözellikle yapar o iOS'ta tablolarla işin çok daha kolay. Bu arada, başka, daha bilgili insanların burada neler önerebileceğini merak ediyorum ...
Patryk Ćwiek

Yanıtlar:


43

UITableViewControllerTek bir nesneye çok fazla sorumluluk koyduğu için kullanmaktan kaçınıyorum . Bu nedenle, UIViewControlleralt sınıfı veri kaynağından ve temsilcisinden ayırırım. Görünüm denetleyicisinin sorumluluğu, tablo görünümünü hazırlamak, verileri içeren bir veri kaynağı oluşturmak ve bunları birbirine bağlamaktır. Tablo görüntüsünün temsil edilme şeklini değiştirmek, görünüm denetleyicisini değiştirmeden yapılabilir ve aslında aynı görünüm denetleyicisi, tümü bu modeli izleyen birden fazla veri kaynağı için kullanılabilir. Benzer şekilde, uygulama iş akışını değiştirmek, tabloya ne olduğu konusunda endişelenmeden görüntüleme denetleyicisindeki değişiklikler anlamına gelir.

UITableViewDataSourceVe UITableViewDelegateprotokolleri farklı nesnelere ayırmaya çalıştım , ancak bu genellikle temsilci üzerindeki hemen hemen her yöntemin veri kaynağının içine girmesi gerektiği için yanlış bir bölünme olarak sona eriyor (örneğin seçimde, temsilci, hangi nesnenin temsil ettiği nesneyi temsil ettiğini bilmek istiyor) seçili satır). Böylece hem veri kaynağı hem de temsilci olan tek bir nesne ile karşılaşıyorum. Bu nesne her zaman -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathhem veri kaynağının hem de temsilci özelliklerinin ne üzerinde çalıştıklarını bilmeleri gereken bir yöntem sağlar .

Bu benim endişelerimin "seviye 0" ayrımı. Aynı tablo görünümünde farklı türdeki nesneleri temsil etmem gerekiyorsa, Seviye 1 devreye giriyor. Örnek olarak, Rehber uygulamasını yazmanız gerektiğini düşünün; tek bir kişi için, telefon numaralarını temsil eden satırlara, adresleri temsil eden diğer satırlara, e-posta adreslerini temsil eden diğerlerine vb. Sahip olabileceğinizi düşünün. Bu yaklaşımdan kaçınmak istiyorum:

- (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 …
}

Şimdiye kadar iki çözüm önerdi. Biri dinamik olarak bir seçici seçmektir:

- (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
}

Bu yaklaşımda, if()yeni bir türü desteklemek için destansı ağacı düzenlemenize gerek yoktur - sadece yeni sınıfı destekleyen yöntemi ekleyin. Bu tablo görüntüsü, bu nesneleri temsil etmesi gereken veya onları özel bir şekilde sunması gereken tek şeyse, bu harika bir yaklaşımdır. Aynı nesneler farklı veri kaynaklarına sahip farklı tablolarda temsil edilecekse, hücre oluşturma yöntemlerinin veri kaynakları arasında paylaşılması gerektiğinden bu yaklaşım bozulur; bu yöntemleri sağlayan ortak bir üst sınıf tanımlayabilirsiniz veya şunları yapabilirsiniz:

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Sonra veri kaynağı sınıfınızda:

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

Bu, telefon numaralarını, adresleri vb. Görüntülemesi gereken herhangi bir veri kaynağının yalnızca bir tablo görüntüleme hücresi için hangi nesnenin temsil edildiğini sorabileceği anlamına gelir . Veri kaynağının artık görüntülenen nesne hakkında hiçbir şey bilmesine gerek yok.

"Ama bekle," Ben varsayımsal bir muhatap araya girdiğini duyuyorum ", bu MVC'yi kırmaz mı?

Hayır, MVC'yi kırmaz. Bu durumda kategorileri bir Dekoratör uygulaması olarak düşünebilirsiniz ; yani PhoneNumberbir model sınıfı ama PhoneNumber(TableViewRepresentation)bir görünüm kategorisi. Veri kaynağı (bir denetleyici nesnesi) model ve görünüm arasında aracılık eder, böylece MVC mimarisi hala tutar.

Bu kategorilerin kullanımını Apple'ın çerçevelerinde dekorasyon olarak da görebilirsiniz. NSAttributedStringbazı metinleri ve nitelikleri tutan bir model sınıfıdır. AppKit sağlar NSAttributedString(AppKitAdditions)ve UIKit, NSAttributedString(NSStringDrawing)bu model sınıflarına çizim davranışı katan dekoratör kategorileri sağlar .


Veri kaynağı ve tablo görünümü temsilcisi olarak çalışan sınıf için iyi bir ad nedir?
Johan Karlsson

1
@JohanKarlsson Genellikle sadece veri kaynağı olarak adlandırırım. Belki biraz özensiz, ama ikisini bir araya getiriyorum, "veri kaynağımın" Apple'ın daha kısıtlı tanımına bir adaptasyon olduğunu bilmek için yeterince sık kullanıyorum.

1
Bu makale: objc.io/issue-1/table-views.html , hücre sınıfını cellForPhotoAtIndexPathveri kaynağı yönteminde hesapladığınız ve daha sonra uygun fabrika yöntemini çağırdığınız birden fazla hücre türünü işlemenin bir yolunu önerir . Elbette hangisi yalnızca belirli sınıflar belirli satırları belirli bir şekilde işgal ettiğinde mümkündür. Modeller üzerinde görünüm üreten kategoriler sisteminiz pratikte çok daha zarif, bence, MVC için sıradışı bir yaklaşım olsa da! :)
Benji XVI

1
Bu kalıbı github.com/yonglam/TableViewPattern adresinde göstermeye çalıştım . Umarım birisi için yararlıdır.
Andrew,

1
Dinamik seçici yaklaşım için kesin bir oy vereceğim . Meseleler sadece çalışma zamanında tezahür ettiği için çok tehlikelidir. Verilen seçicinin var olduğundan ve doğru şekilde yazdığından ve bu tür bir yaklaşımın sonunda parçalanacağından ve sürdürülecek bir kabus olduğundan emin olmanın otomatik bir yolu yoktur . Bununla birlikte, diğer yaklaşım çok zekicedir.
mkko

3

İnsanlar UIViewController / UITableViewController içine çok fazla şey kullanma eğilimindedir.

Başka bir sınıfa devretme (görünüm denetleyicisi değil) genellikle iyi sonuç verir. Delegeler, tüm delege yöntemleri bir referanstan geçtiğinden, görünüm denetleyicisine bir referans vermek zorunda kalmazlar UITableView, ancak bir şekilde delegasyon yaptıkları verilere erişmeleri gerekir.

Yeniden yapılanma süresini azaltmak için birkaç fikir:

  • Tablo görünümü hücrelerini kodda oluşturuyorsanız, bunları bir uç dosyadan veya bir storyboard'dan yüklemeyi düşünün. Storyboard'lar prototip ve statik tablo hücrelerine izin verir - aşina değilseniz, bu özellikleri inceleyin

  • Temsilci yöntemleriniz çok fazla 'if' ifadesi içeriyorsa (veya ifadeleri değiştirirseniz), bazı yeniden düzenleme işlemlerini yapabileceğiniz klasik bir işarettir.

Her zaman biraz komik hissettim UITableViewDataSource, doğru veriyi ele almaktan ve bunu göstermek için bir görünümü yapılandırmaktan sorumluydu . Hoş bir yeniden değerlendirme noktası cellForRowAtIndexPath, bir hücrede görüntülenmesini gerektiren verilerle ilgilenmek için değişiklik yapmak, sonra hücre görünümünün oluşturulmasını CellViewDelegateuygun veri maddesinden geçen başka bir temsilciye (örneğin bir veya benzerini yapmak) devretmek olabilir .


Bu güzel bir cevap. Ancak kafamda birkaç soru ortaya çıkıyor. Neden çok fazla if-ifadesi (veya switch-ifadesi) tasarımı kötü buluyorsunuz? Gerçekten çok fazla iç içe geçmiş if- ve switch-cümleleri mi demek istiyorsun? İf-ya da switch-deyimlerinden kaçınmak için nasıl faktörü tekrarlarsınız?
Johan Karlsson

@ JohnKarlsson'ın bir tekniği polimorfizmdir. Bir nesneyi bir tür nesne ve farklı bir tür başka bir şey yapmanız gerekiyorsa, bu nesneleri farklı sınıflar yapın ve işi sizin için seçmelerini sağlayın.

@GrahamLee Evet, polimorfizmi biliyorum ;-) Ancak bu bağlamda nasıl uygulanacağından emin değilim. Lütfen bunu açıklayın.
Johan Karlsson

@JohanKarlsson;;)

2

İşte kabaca şu anda benzer bir sorunla karşılaştığımda ne yapıyorum:

  • Veri ile ilgili işlemleri XXXDataSource sınıfına taşıyın (BaseDataSource: NSObject kaynağından devralır). BaseDataSource - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;, alt sınıfın veri yükleme yöntemini geçersiz kıldığı gibi bazı uygun yöntemler sağlar (uygulamalar genellikle bir tür offlie önbellek yükleme yöntemi gibi göründüğü için - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;, ağdan gelen bilgileri güncellerken UI'yi LoadProgressBlock'ta alınan önbelleğe alınmış verilerle ve tamamlama bloğunda güncelleyebiliriz. kullanıcı arayüzünü yeni verilerle yeniliyoruz ve varsa progess göstergelerini kaldırıyoruz). Bu sınıflar UITableViewDataSourceprotokole uygun DEĞİLDİR .

  • BaseTableViewController'da (bunlara UITableViewDataSourceve UITableViewDelegateprotokollere uygundur ), denetleyici init sırasında oluşturduğum BaseDataSource referansına sahibim. Olarak UITableViewDataSourcekontrol cihazının bir parçası Sadece dataSource (benzerinden dönüş değerleri - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

İşte benim temel sınıf benim cellForRow (alt sınıflarda geçersiz kılmaya gerek yok):

- (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, alt sınıflar tarafından geçersiz kılınmalı ve createCell, UITableViewCell öğesini döndürür, böylece özel hücre istiyorsanız, geçersiz kılmayı da geçersiz kılın.

  • Temel şeyler yapılandırıldıktan sonra (aslında, böyle bir şema kullanan ilk projede, bu bölüm tekrar kullanılabilir) BaseTableViewControlleralt sınıflar için geriye kalanlar :

    • ConfigureCell öğesini geçersiz kıl (bu, genellikle dizin için dizin için dataSource isteme ve onu hücrenin configureWithXXX: yöntemine besleme veya user4051'in yanıtındaki gibi nesnenin UITableViewCell temsilini alma işlemine dönüştürür)

    • DidSelectRowAtIndexPath öğesini geçersiz kıl: (açıkça)

    • Modelin gerekli bir kısmıyla çalışmayı önemseyen BaseDataSource alt sınıfını yazın (varsayalım 2 sınıf var Accountve Languagealt sınıflar AccountDataSource ve LanguageDataSource olacaktır).

Ve hepsi masa görünümü için. Gerekirse GitHub'a bazı kodlar gönderebilirim.

Düzenleme: bazı öneriler http://www.objc.io/issue-1/lighter-view-controllers.html (bu soruya bağlantısı olan) ve tableviewcontrollers hakkında eşlik eden bir makalede bulunabilir.


2

Bu konuda benim görüşüm, modelin, bir cellConfigurator'da kapsüllenmiş ViewModel veya viewData denilen bir dizi nesne vermesi gerektiğidir. CellConfigurator, onu temizlemek ve hücreyi yapılandırmak için gereken CellInfo öğesini tutar. hücreye bazı veriler verir, böylece hücre kendi kendini yapılandırabilir. CellConfigurators tutan bazı SectionConfigurator nesnesi eklerseniz, bu da bölümle çalışır. Bunu bir süre önce başlangıçta sadece hücreye bir viewData vererek kullanmaya başladım ve ViewController hücreyi temizlemekle ilgilendi. ama bu gitHub deposuna işaret eden bir yazı okudum.

https://github.com/fastred/ConfigurableTableViewController

bu, yaklaşma şeklinizi değiştirebilir.


2

Son zamanlarda UITableView için delegelerin ve veri kaynaklarının nasıl uygulanacağı hakkında bir makale yazdım: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

Ana fikir, sorumlulukları hücre fabrikası, bölüm fabrikası gibi ayrı sınıflara ayırmak ve UITableView'ün göstereceği model için bazı genel arayüzler sağlamaktır. Aşağıdaki diyagram hepsini açıklıyor:

görüntü tanımını buraya girin


Bu bağlantı artık çalışmıyor.
koen

1

Aşağıdaki KATI ilkeleri bunlar gibi problemlerin her türlü çözecektir.

Sahip olmasını sınıflar istiyorsanız SADECE TEK BİR sorumluluğu, ayrı tanımlamak gerekir DataSourceve Delegatesınıfları ve sadece enjekte bunları tableViewsahibine (olabilir UITableViewControllerya da UIViewControllerbaşka bir ya da bir şey). Bu, endişenin ayrılmasının üstesinden nasıl geldiğinizdir .

Ancak sadece temiz ve okunabilir bir kodunuz olması ve bu büyük viewController dosyasından kurtulmak istiyorsanız ve Swif'deyseniz , bunun için s'yi kullanabilirsiniz extension. Tek bir sınıfın uzantıları farklı dosyalara yazılabilir ve hepsi birbirine erişebilir. Ancak bu nit, SoC konusunu söylediğim gibi gerçekten çözüyor .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.