我用Objective-C开始了一个项目,然后用Swift重写了它。该项目包含两个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才能执行操作。我还不喜欢具有passedStringpassedInt属性的事实,其中一半情况下为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作为数组元素类型)作弊了一点。在一个真实的应用程序中,我可能会为不同的选项类型创建自己的类层次结构,或者至少将类别添加到NSObjectNSString以对其进行适当的格式化,而不是依赖于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