iOS에서 크고 서투른 UITableViewController를 피하는 방법은 무엇입니까?


36

iOS에서 MVC 패턴을 구현할 때 문제가 있습니다. 인터넷을 검색했지만이 문제에 대한 좋은 해결책을 찾지 못하는 것 같습니다.

많은 UITableViewController구현이 다소 큰 것 같습니다. 내가 본 대부분의 예제는 UITableViewController구현 <UITableViewDelegate>하고 <UITableViewDataSource>. 이러한 구현은 왜 커지는 큰 이유 UITableViewController입니다. 한 가지 해결책은 <UITableViewDelegate>및 을 구현하는 별도의 클래스를 만드는 것 <UITableViewDataSource>입니다. 물론 이러한 클래스는에 대한 참조가 있어야합니다 UITableViewController. 이 솔루션을 사용하는 데 단점이 있습니까? 일반적으로 델리게이트 패턴을 사용하여 기능을 다른 "Helper"클래스 또는 이와 유사한 것으로 위임해야한다고 생각합니다. 이 문제를 해결하는 잘 확립 된 방법이 있습니까?

모델에 너무 많은 기능이나 뷰가 포함되어 있지 않습니다. 논리가 실제로 컨트롤러 클래스에 있어야한다고 생각합니다. 이는 MVC 패턴의 초석 중 하나이기 때문입니다. 그러나 큰 문제는 다음과 같습니다.

MVC 구현 컨트롤러를 관리하기 쉬운 작은 부분으로 어떻게 나누어야합니까? (이 경우 iOS의 MVC에 적용)

iOS 용 솔루션을 특별히 찾고 있지만이 문제를 해결하기위한 일반적인 패턴이있을 수 있습니다. 이 문제를 해결하기위한 좋은 패턴의 예를 제시하십시오. 솔루션이 왜 멋진 지 논쟁하십시오.


1
"또한이 솔루션이 굉장한 이유는 논쟁입니다." :)
occulus

1
그것은 요점 옆에 약간 있지만 UITableViewController기계공은 나에게 매우 이상해 보이므로 문제와 관련이 있습니다. 사실은 기쁜 I 사용 해요 MonoTouch때문에, MonoTouch.Dialog특별히 만들어 iOS에서 테이블 작업을 훨씬 쉽게. 한편, 나는 ... 다른 지식이 사람들이 여기에 제안 할 수있는 무엇 궁금
Patryk Ćwiek

답변:


43

UITableViewController단일 객체에 많은 책임을 부여하기 때문에를 사용하지 마십시오 . 따라서 UIViewController하위 클래스를 데이터 소스 및 대리자와 분리합니다 . 뷰 컨트롤러의 책임은 테이블 뷰를 준비하고 데이터가 포함 된 데이터 소스를 생성 한 다음 이러한 것들을 연결하는 것입니다. 뷰 컨트롤러를 변경하지 않고 테이블 뷰가 표시되는 방식을 변경할 수 있으며 실제로이 패턴을 따르는 여러 데이터 소스에 동일한 뷰 컨트롤러를 사용할 수 있습니다. 마찬가지로 앱 워크 플로를 변경하면 테이블에 어떤 일이 발생할지 걱정하지 않고 뷰 컨트롤러가 변경됩니다.

나는 프로토콜 UITableViewDataSourceUITableViewDelegate프로토콜을 다른 객체로 분리하려고 시도했지만 델리게이트의 거의 모든 메소드가 데이터 소스를 파헤쳐 야하므로 (예 : 선택시, 델리게이트는 객체가 나타내는 객체를 알아야 함) 선택된 행). 그래서 나는 데이터 소스와 델리게이트 인 단일 객체로 끝납니다. 이 객체는 항상 -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPath데이터 소스와 델리게이트 측면에서 작업중인 내용을 알아야 하는 방법 을 제공합니다.

그것은 나의 "수준 0"관심사 분리입니다. 동일한 테이블 뷰에서 다른 종류의 객체를 나타내야 할 경우 레벨 1이 사용됩니다. 예를 들어 연락처 앱을 작성해야한다고 가정합니다. 단일 연락처의 경우 전화 번호를 나타내는 행, 주소를 나타내는 다른 행, 전자 메일 주소를 나타내는 다른 행 등이있을 수 있습니다. 이 접근법을 피하고 싶습니다 :

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

지금까지 두 가지 해결책이 제시되었습니다. 하나는 선택기를 동적으로 구성하는 것입니다.

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

이 방법에서는 if()새로운 유형을 지원하기 위해 서사시 트리 를 편집 할 필요가 없습니다 . 새로운 클래스를 지원하는 메소드를 추가하기 만하면됩니다. 이 테이블 뷰가 이러한 객체를 나타내거나 특별한 방식으로 제시해야하는 유일한 방법 인 경우이 방법이 유용합니다. 동일한 객체가 서로 다른 데이터 소스가있는 다른 테이블에 표시되는 경우, 셀 작성 방법이 데이터 소스에서 공유해야하므로이 방법이 세분화됩니다. 이러한 방법을 제공하는 공통 수퍼 클래스를 정의하거나 다음을 수행 할 수 있습니다.

@interface PhoneNumber (TableViewRepresentation)

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

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

그런 다음 데이터 소스 클래스에서

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

즉 , 전화 번호, 주소 등을 표시해야하는 모든 데이터 소스 는 테이블 뷰 셀에 표시되는 모든 개체를 요청할 수 있습니다 . 데이터 소스 자체는 더 이상 표시되는 오브젝트에 대해 아무것도 알 필요가 없습니다.

"하지만 잠깐만 요." MVC 가 깨지지 않습니까? 모델 클래스에 뷰 세부 사항을 넣지 않습니까?

아니요, MVC를 중단하지 않습니다. 이 경우 카테고리를 Decorator 구현으로 생각할 수 있습니다 . 그래서 PhoneNumber모델 클래스입니다 만 PhoneNumber(TableViewRepresentation)뷰 범주이다. 데이터 소스 (컨트롤러 객체)는 모델과 뷰 사이를 중재하므로 MVC 아키텍처는 계속 유지됩니다.

Apple 프레임 워크에서이 범주를 장식으로 사용하는 것도 볼 수 있습니다. NSAttributedString텍스트와 속성을 포함하는 모델 클래스입니다. AppKit은 이러한 모델 클래스에 그리기 동작을 추가하는 데코레이터 범주를 제공 NSAttributedString(AppKitAdditions)하고 UIKit을 제공합니다 NSAttributedString(NSStringDrawing).


데이터 소스 및 테이블 뷰 대리자로 작동하는 클래스의 이름은 무엇입니까?
Johan Karlsson

1
@JohanKarlsson 종종 데이터 소스라고 부릅니다. 어쩌면 조금 부끄럽지만 필자의 두 가지 "데이터 소스"가 Apple의 더 제한된 정의에 대한 적응이라는 것을 알기에 충분히 두 가지를 결합합니다.

1
이 기사 : objc.io/issue-1/table-views.htmlcellForPhotoAtIndexPath 에서는 데이터 소스 의 메소드 에서 셀 클래스를 처리 한 다음 적절한 팩토리 메소드를 호출하는 여러 셀 유형을 처리하는 방법을 제안합니다 . 물론 특정 클래스가 특정 행을 예측 가능하게 차지하는 경우에만 가능합니다. 뷰 생성 카테고리 온 모델 시스템은 실제로 훨씬 더 우아하지만 MVC에 대한 정통 접근 방식 일 수도 있습니다. :)
Benji XVI

1
github.com/yonglam/TableViewPattern 에서이 패턴을 데모하려고했습니다 . 누군가에게 유용하기를 바랍니다.
앤드류

1
다이나믹 셀렉터 접근 방식에 대해 결정적인 찬성 투표를합니다 . 런타임시에만 문제가 발생하므로 매우 위험합니다. 주어진 셀렉터가 존재 하고 올바르게 입력되었는지 확인하는 방법은 자동화되어 있지 않으며 이러한 접근 방식은 결국 분리되어 유지해야하는 악몽입니다. 그러나 다른 접근 방식은 매우 영리합니다.
mkko

3

사람들은 UIViewController / UITableViewController에 많은 것을 넣는 경향이 있습니다.

뷰 컨트롤러가 아닌 다른 클래스에 대한 위임은 일반적으로 잘 작동합니다. 모든 대리자 메서드는에 대한 참조를 전달하기 때문에 대리자에게 반드시 뷰 컨트롤러에 대한 참조 UITableView가 필요하지는 않지만 위임하는 데이터에 어떻게 든 액세스해야합니다.

길이를 줄이기 위해 재구성을위한 몇 가지 아이디어 :

  • 코드에서 테이블 뷰 셀을 구성하는 경우 펜촉 파일 또는 스토리 보드에서 대신로드하는 것이 좋습니다. 스토리 보드는 프로토 타입 및 정적 테이블 셀을 허용합니다. 익숙하지 않은 경우 해당 기능을 확인하십시오.

  • 델리게이트 메소드에 많은 'if'문 (또는 switch 문)이 포함되어 있다면 리팩토링을 할 수 있다는 전형적인 신호입니다.

그것은 항상이 나에게 재미 약간을 느꼈다 UITableViewDataSource데이터의 정확한 비트에 대한 핸들을 얻기를위한 책임 그것을 보여주는 뷰를 구성. 하나의 좋은 리팩토링 포인트 cellForRowAtIndexPath는 셀에 표시 해야하는 데이터에 대한 핸들을 얻 도록 변경 한 다음 셀 뷰 작성을 다른 데이터 대리인에게 위임 CellViewDelegate하여 적절한 데이터 항목에 전달됩니다.


이것은 좋은 대답입니다. 그러나 내 머릿속에 몇 가지 질문이 있습니다. 많은 if 문 (또는 switch 문)이 잘못된 디자인 인 이유는 무엇입니까? 실제로 많은 중첩 된 if 및 switch 문을 의미합니까? if 또는 switch 문을 피하기 위해 어떻게 리팩터링합니까?
Johan Karlsson

@JohanKarlsson의 한 기술은 다형성을 통한 것입니다. 한 가지 유형의 객체로 하나의 작업을 수행하고 다른 유형으로 다른 작업을 수행해야하는 경우 해당 객체를 다른 클래스로 만들고 작업을 선택하도록하십시오.

@GrahamLee 네, 다형성을 알고 있습니다. ;-) 그러나이 맥락에서 어떻게 적용 할 지 잘 모르겠습니다. 이것에 대해 자세히 설명하십시오.
Johan Karlsson

@JohanKarlsson done;)

2

비슷한 문제에 직면했을 때 현재하고있는 대략적인 내용은 다음과 같습니다.

  • 데이터 관련 작업을 XXXDataSource 클래스 (BaseDataSource : NSObject에서 상 속됨)로 이동하십시오. BaseDataSource과 같은 몇 가지 편리한 방법을 제공하는 - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;응용 프로그램은 일반적으로 방법의 모습이 좋아 offlie 캐시 부하의 일종 가지고 (서브 클래스 재정의 데이터로드 방법을, - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;우리는 우리가 네트워크에서 정보를 업데이트하는 동안 LoadProgressBlock 수신 캐시 된 데이터와 UI를 업데이트 할 수 있도록, 그리고 완료 블록 새로운 데이터로 UI를 새로 고치고 진행률 표시기를 제거합니다 (있는 경우). 해당 클래스는 UITableViewDataSource프로토콜을 준수하지 않습니다 .

  • BaseTableViewController ( UITableViewDataSourceUITableViewDelegate프로토콜 을 준수 )에서 컨트롤러 초기화 중에 만드는 BaseDataSource에 대한 참조가 있습니다. 에서 UITableViewDataSource컨트롤러의 일부 단순히는 dataSource (등으로부터 값을 반환 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

다음은 기본 클래스의 cellForRow입니다 (하위 클래스에서 재정의 할 필요가 없음).

- (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은 서브 클래스에 의해 대체되어야하며 createCell은 UITableViewCell을 리턴하므로 사용자 정의 셀을 원하는 경우이를 재정의하십시오.

  • 기본 사항이 구성된 후 (실제로 이러한 구성표를 사용하는 첫 번째 프로젝트 에서이 부분을 재사용 할 수있는 후) BaseTableViewController서브 클래스에 남겨진 것은 다음과 같습니다.

    • configureCell 재정의 (일반적으로 인덱스 경로에 대한 객체의 dataSource를 요청하여 셀의 configureWithXXX : 메서드에 공급하거나 user4051의 답변과 같이 객체의 UITableViewCell 표현을 가져옵니다)

    • didSelectRowAtIndexPath 재정의 :( 분명히)

    • 모델의 필요한 부분 작업을 담당 BaseDataSource 서브 클래스를 작성 (2 개 클래스가 가정 AccountLanguage서브 클래스가 AccountDataSource 및 LanguageDataSource 수 있도록,).

그리고 이것이 테이블 뷰 부분입니다. 필요한 경우 GitHub에 코드를 게시 할 수 있습니다.

편집 : 일부 권장 사항은 http://www.objc.io/issue-1/lighter-view-controllers.html (이 질문에 대한 링크가 있음)과 tableviewcontrollers에 대한 기사를 참조하십시오.


2

이것에 대한 나의 견해는 모델이 cellConfigurator에 캡슐화 된 ViewModel 또는 viewData라는 객체의 배열을 제공해야한다는 것입니다. CellConfigurator는이를 제거하고 셀을 구성하는 데 필요한 CellInfo를 보유합니다. 셀에 자체 데이터를 구성 할 수 있도록 셀에 데이터를 제공합니다. CellConfigurators를 보유하는 SectionConfigurator 객체를 추가하면 section 과도 작동합니다. 나는 처음에 셀에 viewData를 제공하고 ViewController가 셀을 대기열에서 빼내는 것을 처리하면서 잠시 이것을 사용하기 시작했습니다. 그러나이 gitHub 저장소를 가리키는 기사를 읽었습니다.

https://github.com/fastred/ConfigurableTableViewController

이것은 당신이 이것에 접근하는 방식을 바꿀 수 있습니다.


2

최근에 UITableView에 대리자와 데이터 소스를 구현하는 방법에 대한 기사를 작성했습니다. http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

주요 아이디어는 책임을 셀 팩토리, 섹션 팩토리와 같은 별도의 클래스로 분할하고 UITableView가 표시 할 모델에 대한 일반적인 인터페이스를 제공하는 것입니다. 아래 다이어그램은 모든 것을 설명합니다.

여기에 이미지 설명을 입력하십시오


이 링크는 더 이상 작동하지 않습니다.
Koen

1

다음 SOLID 원칙을 다음과 같은 문제의 어떤 종류를 해결합니다.

당신이 당신의 클래스로 원하는 경우 단지 하나의 책임, 별도의 정의해야합니다 DataSourceDelegate클래스를 단순히 주입 받는 사람을 tableView소유자 (수 UITableViewController또는 UIViewController다른 사람이나 뭐). 이것은 당신 이 관심 분리 를 극복하는 방법 입니다.

그러나 깨끗하고 읽을 수있는 코드를 원하고 거대한 viewController 파일을 제거 하고 Swif있다면extension s를 사용할 수 있습니다 . 단일 클래스의 확장은 다른 파일로 작성 될 수 있으며 모두 서로 액세스 할 수 있습니다. 그러나 이것은 내가 언급했듯이 SoC 문제를 진정으로 해결 합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.