จะหลีกเลี่ยง UITableViewController ขนาดใหญ่และเงอะงะบน iOS ได้อย่างไร


36

ฉันมีปัญหาเมื่อใช้รูปแบบ MVC บน iOS ฉันค้นหาทางอินเทอร์เน็ต แต่ดูเหมือนจะไม่พบทางออกที่ดีสำหรับปัญหานี้

UITableViewControllerการใช้งานหลายอย่างดูเหมือนจะค่อนข้างใหญ่ ตัวอย่างส่วนใหญ่ที่ฉันได้เห็นช่วยให้UITableViewControllerการดำเนินการและ<UITableViewDelegate> <UITableViewDataSource>การติดตั้งใช้งานเหล่านี้เป็นเหตุผลสำคัญว่าทำไมจึงUITableViewControllerเริ่มมีขนาดใหญ่ ทางออกหนึ่งที่จะสร้างการเรียนที่แยกต่างหากที่ดำเนินการและ<UITableViewDelegate> ของหลักสูตรการเรียนเหล่านี้จะต้องมีการอ้างอิงถึง<UITableViewDataSource> UITableViewControllerมีข้อเสียใด ๆ ในการใช้โซลูชันนี้หรือไม่? โดยทั่วไปฉันคิดว่าคุณควรมอบสิทธิ์การใช้งานให้กับชั้นเรียนอื่น ๆ "ผู้ช่วย" หรืออื่น ๆ ที่คล้ายกันโดยใช้รูปแบบตัวแทน มีวิธีใดในการแก้ปัญหานี้เป็นที่ยอมรับหรือไม่?

ฉันไม่ต้องการให้แบบจำลองมีฟังก์ชั่นมากเกินไปหรือมุมมอง ฉันเชื่อว่าตรรกะควรอยู่ในคลาสตัวควบคุมเนื่องจากนี่เป็นหนึ่งในเสาหลักของรูปแบบ MVC แต่คำถามใหญ่คือ:

คุณควรแบ่งตัวควบคุมของการใช้ MVC เป็นชิ้นส่วนที่จัดการได้อย่างไร (ใช้กับ MVC ใน iOS ในกรณีนี้)

อาจมีรูปแบบทั่วไปสำหรับการแก้ปัญหานี้แม้ว่าฉันกำลังมองหาโซลูชันสำหรับ iOS โดยเฉพาะ โปรดยกตัวอย่างรูปแบบที่ดีสำหรับการแก้ไขปัญหานี้ โปรดระบุเหตุผลที่โซลูชันของคุณยอดเยี่ยม


1
"เป็นเหตุผลว่าทำไมโซลูชั่นนี้ยอดเยี่ยม" :)
occulus

1
นั่นช่างนิดหน่อยข้างๆจุดนั้น แต่UITableViewControllerกลไกช่างดูแปลก ๆ สำหรับฉันดังนั้นฉันสามารถเกี่ยวข้องกับปัญหาได้ ผมใช้ผมดีใจจริงMonoTouchเพราะMonoTouch.Dialogเฉพาะทำที่ง่ายมากที่จะทำงานกับตารางบน iOS ในระหว่างนี้ฉันอยากรู้ว่าคนอื่น ๆ ผู้ที่มีความรู้มากกว่านี้อาจแนะนำที่นี่ ...
Patryk Ćwiek

คำตอบ:


43

ฉันหลีกเลี่ยงการใช้UITableViewControllerเพราะมันทำให้ความรับผิดชอบจำนวนมากเป็นวัตถุชิ้นเดียว ดังนั้นฉันจึงแยกUIViewControllerคลาสย่อยออกจากแหล่งข้อมูลและผู้รับมอบสิทธิ์ ความรับผิดชอบของตัวควบคุมมุมมองคือการเตรียมมุมมองตารางสร้างแหล่งข้อมูลด้วยข้อมูลและเชื่อมโยงสิ่งเหล่านั้นเข้าด้วยกัน การเปลี่ยนวิธีการแสดง tableview นั้นสามารถทำได้โดยไม่ต้องเปลี่ยนคอนโทรลเลอร์มุมมองและแน่นอนว่าสามารถใช้คอนโทรลเลอร์มุมมองเดียวกันสำหรับแหล่งข้อมูลหลายแหล่งที่เป็นไปตามรูปแบบนี้ การเปลี่ยนเวิร์กโฟลว์ของแอปหมายถึงการเปลี่ยนแปลงตัวควบคุมมุมมองโดยไม่ต้องกังวลว่าจะเกิดอะไรขึ้นกับตาราง

ฉันได้ลองแยกUITableViewDataSourceและUITableViewDelegateโปรโตคอลเป็นวัตถุที่แตกต่างกัน แต่ที่มักจะกลายเป็นการแบ่งที่ผิดพลาดเพราะเกือบทุกวิธีในผู้รับมอบสิทธิ์ต้องขุดลงในแหล่งข้อมูล (เช่นในการเลือกผู้เข้าร่วมประชุมต้องรู้ว่า แถวที่เลือก) ดังนั้นฉันจึงลงเอยด้วยวัตถุเดียวนั่นคือทั้งแหล่งข้อมูลและตัวแทน วัตถุนี้จะให้วิธีการ-(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];
}

ซึ่งหมายความว่าแหล่งข้อมูลใด ๆที่ต้องการแสดงหมายเลขโทรศัพท์ที่อยู่ ฯลฯ สามารถถามวัตถุใดก็ได้ที่แสดงแทนเซลล์มุมมองตาราง แหล่งข้อมูลเองไม่จำเป็นต้องรู้อะไรเกี่ยวกับวัตถุที่กำลังแสดงอีกต่อไป

"แต่เดี๋ยวก่อน" ฉันได้ยิน interject ของ interlocutor สมมุติ "นั่นไม่ทำลาย MVC ใช่ไหมคุณไม่ใส่รายละเอียดการดูลงในคลาสโมเดล"

ไม่ไม่ทำลาย MVC คุณสามารถคิดประเภทในกรณีนี้เป็นการดำเนินการของมัณฑนากร ; เพื่อให้PhoneNumberเป็นชั้นรูปแบบ แต่PhoneNumber(TableViewRepresentation)เป็นหมวดหมู่มุมมอง แหล่งข้อมูล (วัตถุคอนโทรลเลอร์) เป็นสื่อกลางระหว่างโมเดลและมุมมองดังนั้นสถาปัตยกรรม MVC จึงยังคงมีอยู่

คุณสามารถเห็นการใช้หมวดหมู่นี้เป็นของตกแต่งในกรอบงานของ Apple เช่นกัน NSAttributedStringเป็น class model โดยมีข้อความและคุณสมบัติ AppKit ให้บริการNSAttributedString(AppKitAdditions)และ UIKit ให้บริการNSAttributedString(NSStringDrawing)หมวดหมู่มัณฑนากรที่เพิ่มพฤติกรรมการวาดภาพให้กับคลาสรุ่นเหล่านี้


ชื่อที่ดีสำหรับคลาสที่ทำงานเป็นแหล่งข้อมูลและผู้แทนมุมมองตารางคืออะไร
Johan Karlsson

1
@JohanKarlsson ฉันมักจะเรียกมันว่าแหล่งข้อมูล บางทีมันอาจจะเลอะเทอะเล็กน้อย แต่ฉันรวมสองอย่างเข้าด้วยกันเพื่อให้รู้ว่า "แหล่งข้อมูล" ของฉันเป็นการปรับตัวให้เข้ากับคำจำกัดความที่ จำกัด ยิ่งขึ้นของ Apple

1
บทความนี้: objc.io/issue-1/table-views.htmlเสนอวิธีการจัดการเซลล์หลายชนิดโดยที่คุณทำงานคลาสเซลล์ในcellForPhotoAtIndexPathวิธีการของแหล่งข้อมูลจากนั้นเรียกวิธีการของโรงงานที่เหมาะสม ซึ่งแน่นอนว่าเป็นไปได้เฉพาะในกรณีที่คลาสที่คาดเดาได้ครอบครองแถวที่เฉพาะเจาะจง ระบบของคุณในการสร้างหมวดหมู่ต่อโมเดลที่ดูดีมีความสง่างามมากขึ้นในทางปฏิบัติฉันคิดว่าแม้ว่ามันอาจจะเป็นวิธีนอกรีตสำหรับ MVC! :)
Benji XVI

1
ฉันได้พยายามที่จะสาธิตรูปแบบนี้ที่github.com/yonglam/TableViewPattern หวังว่ามันจะมีประโยชน์สำหรับใครบางคน
Andrew

1
ฉันจะลงคะแนนไม่แน่นอนสำหรับวิธีการเลือกแบบไดนามิก มันอันตรายมากเนื่องจากปัญหาจะปรากฏเฉพาะในเวลาทำงาน ไม่มีวิธีอัตโนมัติเพื่อให้แน่ใจว่ามีตัวเลือกที่ระบุอยู่และพิมพ์อย่างถูกต้องและวิธีการเช่นนี้จะพังทลายในที่สุดและเป็นฝันร้ายที่ต้องรักษา อย่างไรก็ตามวิธีการอื่นนั้นฉลาดมาก
mkko

3

คนมักจะแพ็คจำนวนมากลงใน UIViewController / UITableViewController

การมอบหมายไปยังคลาสอื่น (ไม่ใช่ตัวควบคุมมุมมอง) โดยปกติแล้วจะทำงานได้ดี ผู้รับมอบสิทธิ์ไม่จำเป็นต้องมีการอ้างอิงกลับไปยังตัวควบคุมมุมมองเนื่องจากวิธีการมอบหมายทั้งหมดได้รับการส่งผ่านการอ้างอิงถึงUITableViewแต่พวกเขาจะต้องเข้าถึงข้อมูลที่พวกเขากำลังมอบหมาย

แนวคิดบางประการสำหรับการจัดระเบียบใหม่เพื่อลดความยาว:

  • หากคุณกำลังสร้างเซลล์มุมมองตารางในโค้ดให้ลองโหลดเซลล์เหล่านั้นแทนจากไฟล์ปลายปากกาหรือจากกระดานเรื่องราว บอร์ดโครงเรื่องอนุญาตให้ใช้เซลล์ต้นแบบและเซลล์ตารางแบบคงที่ - ตรวจสอบคุณสมบัติเหล่านั้นหากคุณไม่คุ้นเคย

  • หากวิธีการมอบหมายของคุณมีคำสั่ง 'if' จำนวนมาก (หรือสลับคำสั่ง) นั่นเป็นสัญญาณแบบคลาสสิกที่คุณสามารถทำการปรับเปลี่ยนบางอย่างได้

มันรู้สึกตลกสำหรับฉันเสมอว่าผู้UITableViewDataSourceรับผิดชอบในการจัดการข้อมูลที่ถูกต้องและกำหนดค่ามุมมองเพื่อแสดงมัน จุดเปลี่ยนโครงสร้างที่ดีจุดหนึ่งอาจเปลี่ยนของคุณcellForRowAtIndexPathให้เป็นจุดจับบนข้อมูลที่ต้องการแสดงในเซลล์จากนั้นมอบหมายการสร้างมุมมองเซลล์ให้กับผู้รับมอบสิทธิ์รายอื่น (เช่นสร้างCellViewDelegateหรือคล้ายกัน) ซึ่งได้รับการส่งผ่านในรายการข้อมูลที่เหมาะสม


นี่เป็นคำตอบที่ดี อย่างไรก็ตามมีคำถามสองข้อเกิดขึ้นในหัวของฉัน ทำไมคุณถึงพบว่า if-statement (หรือ statement-switch) จำนวนมากมีการออกแบบที่ไม่ดี? จริงๆแล้วคุณหมายถึงมี if- และ switch-statement จำนวนมากซ้อนกันอยู่หรือไม่? คุณจะพิจารณาปัจจัยอีกครั้งเพื่อหลีกเลี่ยง if- หรือ switch-statement ได้อย่างไร
Johan Karlsson

@JohanKarlsson เทคนิคหนึ่งคือผ่าน polymorphism หากคุณต้องการทำสิ่งหนึ่งกับวัตถุประเภทหนึ่งและอย่างอื่นที่มีประเภทที่แตกต่างกันให้ทำให้วัตถุเหล่านั้นมีคลาสที่แตกต่างกันและให้พวกเขาเลือกงานให้คุณ

@ GrahamLee ใช่ฉันรู้จัก polymorphism ;-) อย่างไรก็ตามฉันไม่แน่ใจว่าจะใช้มันอย่างไรในบริบทนี้ โปรดอธิบายรายละเอียดเกี่ยวกับสิ่งนี้
Johan Karlsson

@JohanKarlsson เสร็จแล้ว;)

2

นี่คือสิ่งที่ฉันกำลังทำอยู่เมื่อเผชิญกับปัญหาที่คล้ายกัน:

  • ย้ายการดำเนินการที่เกี่ยวข้องกับข้อมูลไปยังคลาส XXXDataSource (ซึ่งสืบทอดมาจาก BaseDataSource: NSObject) BaseDataSource มีวิธีการที่สะดวกเช่น- (NSUInteger)rowsInSection:(NSUInteger)sectionNum;subclass แทนที่วิธีการโหลดข้อมูล (เนื่องจากแอปมักจะมีวิธีการโหลดแคชแบบออฟไลน์บางอย่างดูเหมือน- (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;ว่าเราสามารถอัปเดต UI ด้วยข้อมูลแคชที่ได้รับใน LoadProgressBlock ในขณะที่เรากำลังอัพเดทข้อมูลจากเครือข่าย เรารีเฟรช UI ด้วยข้อมูลใหม่และลบตัวบ่งชี้ progess (ถ้ามี) คลาสเหล่านั้นไม่สอดคล้องกับUITableViewDataSourceโปรโตคอล

  • ใน BaseTableViewController (ซึ่งเป็นไปตามUITableViewDataSourceและUITableViewDelegateโปรโตคอล) ฉันมีการอ้างอิงถึง 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 ของเซลล์: เมธอดหรือรับการแทนค่า UITableViewCell ของวัตถุเช่นเดียวกับในคำตอบของผู้ใช้ 4051)

    • แทนที่ didSelectRowAtIndexPath: (ชัด)

    • เขียนคลาสย่อย BaseDataSource ซึ่งดูแลการทำงานกับส่วนที่จำเป็นของโมเดล (สมมติว่ามี 2 คลาสAccountและLanguageดังนั้นคลาสย่อยจะเป็น AccountDataSource และ LanguageDataSource)

และนั่นคือทั้งหมดสำหรับส่วนมุมมองตาราง ฉันสามารถโพสต์รหัสไปที่ GitHub ได้ถ้าต้องการ

แก้ไข: การแนะนำบางอย่างสามารถดูได้ที่http://www.objc.io/issue-1/lighter-view-controllers.html (ซึ่งมีลิงก์ไปยังคำถามนี้) และบทความเกี่ยวกับ tableviewcontrollers


2

มุมมองของฉันเกี่ยวกับเรื่องนี้คือรูปแบบต้องให้อาร์เรย์ของวัตถุที่เรียกว่า ViewModel หรือ viewData encapsulated ใน cellConfigurator CellConfigurator เก็บ CellInfo ที่จำเป็นในการ deque และเพื่อกำหนดค่าเซลล์ มันให้ข้อมูลบางส่วนแก่เซลล์เพื่อให้เซลล์สามารถกำหนดค่าตัวเองได้ สิ่งนี้ใช้ได้กับส่วนถ้าคุณเพิ่มวัตถุ SectionConfigurator บางอย่างที่ถือ CellConfigurators ฉันเริ่มใช้สิ่งนี้สักพักในตอนแรกแค่ให้ viewData กับเซลล์และให้ ViewController จัดการกับ dequeuing เซลล์ แต่ฉันอ่านบทความที่ชี้ไปยัง repo gitHub นี้

https://github.com/fastred/ConfigurableTableViewController

นี่อาจเปลี่ยนวิธีที่คุณเข้าใกล้นี้


2

ฉันเพิ่งเขียนบทความเกี่ยวกับวิธีการใช้ผู้รับมอบสิทธิ์และแหล่งข้อมูลสำหรับ UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

แนวคิดหลักคือการแบ่งความรับผิดชอบออกเป็นคลาสแยกเช่นโรงงานเซลล์ส่วนโรงงานและจัดเตรียมอินเทอร์เฟซทั่วไปสำหรับโมเดลที่ UITableView กำลังแสดงอยู่ แผนภาพด้านล่างอธิบายมันทั้งหมด:

ป้อนคำอธิบายรูปภาพที่นี่


ลิงค์นี้ใช้งานไม่ได้อีกต่อไป
koen

1

การปฏิบัติตามหลักการSOLIDจะช่วยแก้ปัญหาใด ๆ เช่นนี้

หากคุณต้องการที่จะเรียนของคุณมีเพียงครั้งเดียวที่รับผิดชอบคุณควรกำหนดแยกต่างหากDataSourceและDelegateชั้นเรียนและก็ฉีดให้กับtableViewเจ้าของ (อาจจะมีUITableViewControllerหรือUIViewControllerหรือสิ่งอื่น) นี่คือวิธีที่คุณเอาชนะการแยกของความกังวล

แต่ถ้าคุณต้องการมีโค้ดที่สะอาดและอ่านได้และต้องการกำจัดไฟล์ viewController ขนาดใหญ่นั้นและคุณอยู่ในSwifคุณสามารถใช้extensions สำหรับสิ่งนั้นได้ ส่วนขยายของคลาสเดี่ยวนั้นสามารถเขียนเป็นไฟล์ต่างกันและทั้งหมดสามารถเข้าถึงซึ่งกันและกัน แต่นี่เป็นเรื่องเล็กน้อยที่จะแก้ปัญหาSoCอย่างที่ฉันพูดถึง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.