HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMajor

Rewrite code from Objective-C to conform with Swift power tools and concise style

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
swiftwithandrewritetoolspowerconcisestyleconformobjective

Problem

I started a project with Objective-C and rewrote it with Swift. The project contains two UITableViewControllers: MasterViewController and DetailViewController. MasterViewController is linked to DetailViewController with a segue.

MasterViewController has two UITableViewCells. The cell with indexPath [0, 0] has a detailTextLabel that displays a String. Each time I click on this cell, I go to the DetailViewController which, in this case, displays a list of Strings. The cell with indexPath [0, 1] has a detailTextLabel that displays an Int. Each time I click on this cell, I go to the DetailViewController, which in this case, displays a list of Ints.

Furthermore, when I select a cell in DetailViewController, it displays a checkmark and updates the MasterViewController related cell which triggered the segue.

Here is a picture of the different scenes of the project:

And here is my code:

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

overr

Solution

The problem here isn't that your Swift looks like Objective-C. The problem is that, because you're developing a toy app, you've used a toy design, and now you don't like the toy design. Probably you followed a tutorial you found on the Internet (or even in Apple's docs), and most of those are for toy apps and toy designs. In those tutorials, the author hates adding classes, and ends up littering if statements all over the code. But fewer classes does not necessarily lead to better design.

Here's how I would design this app as if it were not a toy. Notice that in the design below, there are almost no if statements. In serious apps, I try to rely on message dispatch to choose the code path.

Objective-C

I know your question was about Swift, but I'll start with Objective-C to demonstrate that the problem is with the toy design, not the language choice.

First, let's create a real model layer. We have a master model, which is an array of details:

MasterModel.h

#import 

@interface MasterModel : NSObject

/** An array of `DetailModel`. */
@property (nonatomic, strong, readonly) NSArray *details;

@end


and we have a detail model:

DetailModel.h

#import 

@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


In a real app, you'd probably load the model from a database or a file. In this toy app, I'll hard-code it:

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


Now we need a master view controller to display the master model:

MasterViewController.h

#import 

@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;
}


The master table view shows one row per detail model in the master model:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _model.details.count;
}


I use a custom cell class for each row. I just hand the cell its model and the cell takes care of the details:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MasterCell *cell = [tableView dequeueReusableCellWithIdentifier:@"detail" forIndexPath:indexPath];
    cell.model = _model.details[indexPath.row];
    return cell;
}


In a real app, you might have many segues, so we dispatch to another method based on the segue identifier:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"ShowDetail"]) {
        [self prepareForShowDetailSegue:segue sender:sender];
    }
}


Because each master cell knows which detail model it's displaying, we can just ask the cell for its model and hand that off to the detail view controller:

- (void)prepareForShowDetailSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    DetailViewController *destination = segue.destinationViewController;
    MasterCell *cell = sender;
    destination.model = cell.model;
}

@end


MasterCell.h

#import 

@class DetailModel;

@interface MasterCell : UITableViewCell

@property (nonatomic, strong) DetailModel *model;

@end


MasterCell.m

#import "MasterCell.h"
#import "DetailModel.h"

@implementation MasterCell


Remember that cells can be reused. In this app, they won't be (because there are only going to be two instances of MasterCell and they fit on the screen together), but this is the general pattern I follow when I expect cell reuse:

```
  • (void)setModel:(DetailModel *)model {


Code Snippets

#import <Foundation/Foundation.h>

@interface MasterModel : NSObject

/** An array of `DetailModel`. */
@property (nonatomic, strong, readonly) NSArray *details;

@end
#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
#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
#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
#import <UIKit/UIKit.h>

@class MasterModel;

@interface MasterViewController : UITableViewController

@property (nonatomic, strong, readonly) MasterModel *model;

@end

Context

StackExchange Code Review Q#62958, answer score: 32

Revisions (0)

No revisions yet.