UITableViewController
:MasterViewController和DetailViewController。 MasterViewController通过一个链接链接到DetailViewController。MasterViewController有两个
UITableViewCell
。具有indexPath [0,0]的单元格具有一个detailTextLabel
,其中显示一个String
。每次单击该单元格,我都会转到DetailViewController,在这种情况下,该列表将显示String
的列表。带有indexPath [0,1]的单元格具有一个detailTextLabel
,显示一个Int
。每次单击该单元格,我都会转到DetailViewController,在这种情况下,该列表将显示Int
的列表。此外,当我在DetailViewController中选择一个单元格时,它会显示一个选中标记并更新
这是项目不同场景的图片:
这是我的代码:
MasterViewController
class MasterViewController: UITableViewController {
var myString = "Yellow"
var myInt = 16
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "attributeChangeMethod:", name: "attributeChange", object: nil)
}
func attributeChangeMethod(notif: NSNotification) {
if let passedString: AnyObject = notif.userInfo?["String"] {
myString = passedString as String
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: 0)], withRowAnimation: .Automatic)
}
if let passedInt: AnyObject = notif.userInfo?["Int"] {
myInt = passedInt as Int
tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: 1, inSection: 0)], withRowAnimation: .Automatic)
}
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self, name: "attributeChange", object:nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
if indexPath.row == 0 {
cell.textLabel?.text = "My string"
cell.detailTextLabel?.text = myString
}
if indexPath.row == 1 {
cell.textLabel?.text = "My int"
cell.detailTextLabel?.text = "\(myInt)"
}
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let controller = segue.destinationViewController as DetailViewController
if sender as? UITableViewCell == tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0)) {
controller.identifier = "String"
controller.passedString = myString
}
if sender as? UITableViewCell == tableView.cellForRowAtIndexPath(NSIndexPath(forRow: 1, inSection: 0)) {
controller.identifier = "Int"
controller.passedInt = myInt
}
}
}
DetailViewController
class DetailViewController: UITableViewController {
var identifier: String!
var passedString: String!
var passedInt: Int!
var array: [Any]!
override func viewDidLoad() {
super.viewDidLoad()
if identifier == "String" {
title = "Select My String"
array = ["Yellow", "Green", "Blue", "Red"]
}
if identifier == "Int" {
title = "Select My Int"
array = [8, 16, 32, 64]
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return array.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as UITableViewCell
if identifier == "String" {
cell.textLabel?.text = array[indexPath.row] as? String
cell.accessoryType = (array[indexPath.row] as String) == passedString ? .Checkmark : .None
}
if identifier == "Int" {
cell.textLabel?.text = "\(array[indexPath.row] as Int)"
cell.accessoryType = (array[indexPath.row] as Int) == passedInt ? .Checkmark : .None
}
return cell
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
for cell in tableView.visibleCells() as [UITableViewCell] {
cell.accessoryType = .None
}
tableView.deselectRowAtIndexPath(indexPath, animated: true)
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell!.accessoryType = .Checkmark
//Return the new value back to MasterViewController
var dict: [String : AnyObject]!
if identifier == "String" {
passedString = array[indexPath.row] as? String
dict = ["String" : array[indexPath.row] as String]
}
if identifier == "Int" {
passedInt = array[indexPath.row] as? Int
dict = ["Int" : array[indexPath.row] as Int]
}
NSNotificationCenter.defaultCenter().postNotificationName("attributeChange", object: nil, userInfo: dict)
}
}
此代码可以正常工作但我有一个问题:我不喜欢它。它看起来不像Swift风格的现代代码,它看起来像是从旧版本的Objective-C到Swift的复制粘贴。我不喜欢在DetailViewController中具有
identifier
属性的事实:我总是必须检查identifier
语句中的if
才能执行操作。我还不喜欢具有passedString
和passedInt
属性的事实,其中一半情况下为nil
。而且,最糟糕的是,我不喜欢array
类型为[Any]!
的事实:它导致许多垂头丧气。还有没有更好的方法来编写此代码? Swift提供了诸如gerenic结构和类或协议一致性类型数组之类的工具。他们不能在这里帮助编写更多精通的代码吗?
#1 楼
这里的问题不是您的Swift看起来像Objective-C。问题是,因为您正在开发玩具应用程序,所以您使用了玩具设计,而现在您不喜欢该玩具设计。可能您遵循了在Internet上(甚至在Apple的文档中)找到的教程,并且其中大多数用于玩具应用程序和玩具设计。在那些教程中,作者讨厌添加类,并最终在整个代码中乱扔了if
语句。但是较少的类并不一定会带来更好的设计。这就是我设计此应用程序的方式,就好像它不是玩具一样。请注意,在下面的设计中,几乎没有
if
语句。在严肃的应用程序中,我尝试依靠消息分发来选择代码路径。Objective-C
我知道您的问题是关于Swift的,但我将从用Objective-C证明问题出在玩具设计上,而不是语言选择上。
首先,让我们创建一个真实的模型层。我们有一个包含详细信息的主模型:
MasterModel.h
#import <Foundation/Foundation.h>
@interface MasterModel : NSObject
/** An array of `DetailModel`. */
@property (nonatomic, strong, readonly) NSArray *details;
@end
我们有一个详细模型:
DetailModel.h
#import <Foundation/Foundation.h>
@interface DetailModel : NSObject
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, copy, readonly) NSArray *options;
@property (nonatomic) NSUInteger selectedOptionIndex;
- (instancetype)initWithTitle:(NSString *)title options:(NSArray *)options;
@end
在真实应用中,您可能会从数据库或文件中加载模型。在这个玩具应用中,我将对其进行硬编码:
MasterModel.m
#import "MasterModel.h"
#import "DetailModel.h"
@implementation MasterModel
- (instancetype)init {
if (self = [super init]) {
_details = @[ [self newStringDetailModel], [self newIntDetailModel] ];
}
return self;
}
- (DetailModel *)newStringDetailModel {
NSArray *options = @[ @"Yellow", @"Green", @"Blue", @"Red" ];
return [[DetailModel alloc] initWithTitle:@"My String" options:options];
}
- (DetailModel *)newIntDetailModel {
NSArray *options = @[ @8, @16, @32, @64 ];
return [[DetailModel alloc] initWithTitle:@"My Int" options:options];
}
@end
DetailModel.m
#import "DetailModel.h"
@implementation DetailModel
- (instancetype)initWithTitle:(NSString *)title options:(NSArray *)options {
if (self = [super init]) {
_title = [title copy];
_options = [options copy];
}
return self;
}
@end
现在我们需要一个主视图控制器来显示主模型:
MasterViewController.h
#import <UIKit/UIKit.h>
@class MasterModel;
@interface MasterViewController : UITableViewController
@property (nonatomic, strong, readonly) MasterModel *model;
@end
/> MasterViewController.m
#import "MasterViewController.h"
#import "MasterModel.h"
#import "MasterCell.h"
#import "DetailViewController.h"
@interface MasterViewController ()
@end
@implementation MasterViewController
- (void)awakeFromNib {
[super awakeFromNib];
_model = [[MasterModel alloc] init];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
主表视图在主模型中为每个细节模型显示一行:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _model.details.count;
}
我为每行使用一个自定义单元格类。我只将单元格的模型交给单元格,单元格负责细节:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MasterCell *cell = [tableView dequeueReusableCellWithIdentifier:@"detail" forIndexPath:indexPath];
cell.model = _model.details[indexPath.row];
return cell;
}
在真实的应用中,您可能会有很多问题,因此我们分派给另一种方法根据segue标识符:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"ShowDetail"]) {
[self prepareForShowDetailSegue:segue sender:sender];
}
}
因为每个主单元都知道它正在显示哪个详细模型,所以我们只需要询问该单元的模型并将其交给详细视图控制器即可:
- (void)prepareForShowDetailSegue:(UIStoryboardSegue *)segue sender:(id)sender {
DetailViewController *destination = segue.destinationViewController;
MasterCell *cell = sender;
destination.model = cell.model;
}
@end
MasterCell.h
#import <UIKit/UIKit.h>
@class DetailModel;
@interface MasterCell : UITableViewCell
@property (nonatomic, strong) DetailModel *model;
@end
MasterCell.m
#import "MasterCell.h"
#import "DetailModel.h"
@implementation MasterCell
请记住,单元可以重复使用。在此应用程序中,它们不会存在(因为仅存在
MasterCell
的两个实例,并且它们一起显示在屏幕上),但这是我期望单元重用时遵循的一般模式:- (void)setModel:(DetailModel *)model {
[self disconnectFromModel];
_model = model;
[self connectToModel];
[self update];
}
- (void)update {
if (self.model != nil) {
self.textLabel.text = self.model.title;
self.detailTextLabel.text = [self.model.options[self.model.selectedOptionIndex] description];
}
}
当用户进入局部视图并更改所选选项时,该局部的主单元需要更新。我们将使用键值观察(KVO)来检测对所选内容的更改。使用单元格),则上述方法无效,如果将模型设置回
self.model
,则以下方法将无效。static char kSelectedOptionIndexContext;
- (void)disconnectFromModel {
[self.model removeObserver:self forKeyPath:@"selectedOptionIndex" context:&kSelectedOptionIndexContext];
}
通常,重要的是在释放观察者时注销他们的注册。否则,如果观察到的属性以后发生更改,则会崩溃。
- (void)connectToModel {
[self.model addObserver:self forKeyPath:@"selectedOptionIndex" options:0 context:&kSelectedOptionIndexContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == &kSelectedOptionIndexContext) {
[self update];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
DetailViewController.h
- (void)dealloc {
[self disconnectFromModel];
}
@end
DetailViewController .m
#import <UIKit/UIKit.h>
@class DetailModel;
@interface DetailViewController : UITableViewController
@property (nonatomic, strong) DetailModel *model;
@end
由于我们不重用
nil
实例,并且DetailViewController
不使用KVO,所以DetailViewController
更简单:#import "DetailViewController.h"
#import "DetailModel.h"
@interface DetailViewController ()
@end
@implementation DetailViewController
在这种情况下,我不需要在表视图单元格中存储模型引用,并且仅使用通用
setModel:
就可以摆脱困境:- (void)setModel:(DetailModel *)model {
_model = model;
self.navigationItem.title = [@"Select " stringByAppendingString:_model.title];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _model.options.count;
}
Swift
现在让我们使用Swift来实现此设计。该模型几乎相同:
MasterModel.swift
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"option" forIndexPath:indexPath];
cell.textLabel.text = [_model.options[indexPath.row] description];
if (_model.selectedOptionIndex == indexPath.row) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *priorSelectedCell = [tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:_model.selectedOptionIndex inSection:0]];
priorSelectedCell.accessoryType = UITableViewCellAccessoryNone;
_model.selectedOptionIndex = indexPath.row;
UITableViewCell *selectedCell = [tableView cellForRowAtIndexPath:indexPath];
selectedCell.accessoryType = UITableViewCellAccessoryCheckmark;
}
@end
DetailModel.swift
import UIKit
class MasterModel: NSObject {
let details: Array<DetailModel>
override init() {
details = [ MasterModel.newStringDetailModel(), MasterModel.newIntDetailModel() ]
super.init()
}
private class func newStringDetailModel() -> DetailModel {
return DetailModel(title: "My String", options: [ "Yellow", "Green", "Blue", "Red" ])
}
private class func newIntDetailModel() -> DetailModel {
let options: Array<Printable> = [ 8, 16, 32, 64 ]
return DetailModel(title: "My Int", options:options)
}
}
/>
实际上,我已经通过使用
UITableViewCell
作为数组元素类型(以及在Object-C版本中依赖Printable
作为数组元素类型)作弊了一点。在一个真实的应用程序中,我可能会为不同的选项类型创建自己的类层次结构,或者至少将类别添加到NSObject
和NSString
以对其进行适当的格式化,而不是依赖于NSNumber
/ description
提供的Printable
。MasterViewController.swift
import UIKit
class DetailModel: NSObject {
let title: String
let options: Array<Printable>
dynamic var selectedOptionIndex: Int
init(title: String, options: Array<Printable>) {
self.title = title
self.options = options
self.selectedOptionIndex = 0
}
}
MasterCell.swift
import UIKit
class MasterViewController: UITableViewController {
let model = MasterModel()
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.details.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("detail", forIndexPath: indexPath) as MasterCell
cell.model = model.details[indexPath.row]
return cell
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "ShowDetail") {
prepareForShowDetailSegue(segue, sender:sender)
}
}
private func prepareForShowDetailSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let destination = segue.destinationViewController as DetailViewController
let cell = sender! as MasterCell
destination.model = cell.model
}
}
以下实际上是最棘手的部分在Swift实施中,因为使用了KVO。 KVO方法的
NSObject
参数在Objective-C中是context
,在Swift中是void *
。我发现,除非我显式创建一个UnsafeMutablePointer<Void>
实例,而不是依靠Swift的UnsafeMutablePointer
运算符,否则它不起作用,该运算符在某些情况下会转换为&
。但是编译器不支持这些“。”请注意,为避免内存泄漏,我们需要显式释放我们创建的所有KVO上下文。 br /> import UIKit
class MasterCell: UITableViewCell {
var model: DetailModel? {
willSet {
disconnectFromModel()
}
didSet {
connectToModel()
update()
}
}
DetailViewController.swift
// Swift doesn't support class variables yet.
private lazy var selectedOptionIndexContext = UnsafeMutablePointer<Void>.alloc(1)
private func disconnectFromModel() {
model?.removeObserver(self, forKeyPath: "selectedOptionIndex", context: selectedOptionIndexContext)
}
private func connectToModel() {
model?.addObserver(self, forKeyPath: "selectedOptionIndex", options: NSKeyValueObservingOptions(), context: selectedOptionIndexContext)
}
private class func toUnsafeMutablePointer(p: UnsafeMutablePointer<Void>) -> UnsafeMutablePointer<Void> {
return p
}
override func observeValueForKeyPath(keyPath: String!, ofObject object: AnyObject!, change: [NSObject : AnyObject]!, context: UnsafeMutablePointer<Void>) {
if (context == selectedOptionIndexContext) {
update()
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
private func update() {
if (model != nil) {
textLabel?.text = model?.title;
detailTextLabel?.text = (model?.options[model!.selectedOptionIndex])?.description
}
}
我已经将两个版本都上传到了这个github仓库。 >
评论
\ $ \ begingroup \ $
哇,答案何在! -但是有一个问题:我看不到在哪里使用私有类函数toUnsafeMutablePointer,并且您的代码似乎可以在没有它的情况下工作。真的需要吗?
\ $ \ endgroup \ $
–马丁R
2014年9月15日18:05
\ $ \ begingroup \ $
没必要。这是进化死角的遗迹。
\ $ \ endgroup \ $
–罗布·梅奥夫
2014年9月15日在18:16
\ $ \ begingroup \ $
我无语。很好的解释。很棒的Objective-C / Swift代码。我什至笑着读了你的答案(“玩具设计”,大声笑)。但是现在我有希望:我们甚至可以为简单的事情做更好的代码。谢谢!
\ $ \ endgroup \ $
–user53113
2014-09-15 19:34
\ $ \ begingroup \ $
很好的答案,但是将模型传递到主表视图单元会违反MVC。我将使用设置textLabel和detailTextLabel标题的设置器来公开描述和选项属性。然后,MasterViewController设置这些属性。这样可以使模型知识远离视图(表格单元格)。我会更进一步,并使用具有VC使用的属性的自定义详细信息单元格。这将隐藏从VC使用textLabel和附件视图的实现细节。代码更多,但耦合更少,关注点分离更好。
\ $ \ endgroup \ $
–杰夫·哈克沃思(Geoff Hackworth)
2014-09-17 6:20
\ $ \ begingroup \ $
有些人认为视图不应该直接读取模型。有人认为应该。我属于后一组。我不希望“ MVC”代表“ Massive View Controller”。将会有自定义代码,以使特定于应用程序的模型适应通用视图。该代码是否应该存在于控制器,特定于应用程序的自定义视图或其他对象中,这是观点,口味和辩论的问题。
\ $ \ endgroup \ $
–罗布·梅奥夫
2014-09-17 18:25