Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MVVM #3

Merged
merged 7 commits into from Feb 7, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 32 additions & 4 deletions Source/OctoJobs.xcodeproj/project.pbxproj
Expand Up @@ -13,6 +13,8 @@
2FA59BCC179DB55A0048130E /* OCJPositionDetailsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA59BCB179DB55A0048130E /* OCJPositionDetailsViewController.m */; };
2FA59BCF179DB7F10048130E /* OCJPositionCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FA59BCE179DB7F10048130E /* OCJPositionCell.m */; };
2FB33310179097BD002F2076 /* OCJJobListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FB3330F179097BD002F2076 /* OCJJobListViewController.m */; };
2FC654771A7B40D4009078A2 /* OCJPositionViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FC654761A7B40D4009078A2 /* OCJPositionViewModel.m */; };
2FC6547A1A7B4AC9009078A2 /* OCJPositionListViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 2FC654791A7B4AC9009078A2 /* OCJPositionListViewModel.m */; };
2FD413A71780CD13008A67BB /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD413A61780CD13008A67BB /* Foundation.framework */; };
2FD413A91780CD13008A67BB /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD413A81780CD13008A67BB /* CoreGraphics.framework */; };
2FD413AB1780CD13008A67BB /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2FD413AA1780CD13008A67BB /* UIKit.framework */; };
Expand Down Expand Up @@ -50,6 +52,10 @@
2FA59BCE179DB7F10048130E /* OCJPositionCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OCJPositionCell.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
2FB3330E179097BD002F2076 /* OCJJobListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = OCJJobListViewController.h; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; };
2FB3330F179097BD002F2076 /* OCJJobListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OCJJobListViewController.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
2FC654751A7B40D4009078A2 /* OCJPositionViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCJPositionViewModel.h; sourceTree = "<group>"; };
2FC654761A7B40D4009078A2 /* OCJPositionViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCJPositionViewModel.m; sourceTree = "<group>"; };
2FC654781A7B4AC9009078A2 /* OCJPositionListViewModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OCJPositionListViewModel.h; sourceTree = "<group>"; };
2FC654791A7B4AC9009078A2 /* OCJPositionListViewModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCJPositionListViewModel.m; sourceTree = "<group>"; };
2FD413A31780CD13008A67BB /* OctoJobs.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OctoJobs.app; sourceTree = BUILT_PRODUCTS_DIR; };
2FD413A61780CD13008A67BB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
2FD413A81780CD13008A67BB /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -98,15 +104,13 @@
children = (
2FD413B51780CD13008A67BB /* OCJAppDelegate.h */,
2FD413B61780CD13008A67BB /* OCJAppDelegate.m */,
2FB3330E179097BD002F2076 /* OCJJobListViewController.h */,
2FB3330F179097BD002F2076 /* OCJJobListViewController.m */,
2FA59BCD179DB7F10048130E /* OCJPositionCell.h */,
2FA59BCE179DB7F10048130E /* OCJPositionCell.m */,
2FC654731A7B3FA1009078A2 /* List View */,
2FA59BCA179DB55A0048130E /* OCJPositionDetailsViewController.h */,
2FA59BCB179DB55A0048130E /* OCJPositionDetailsViewController.m */,
2F924A031791F476003B30DC /* OCJAPIClient.h */,
2F924A041791F476003B30DC /* OCJAPIClient.m */,
2F924A061791F7D7003B30DC /* Models */,
2FC654741A7B40C4009078A2 /* View Models */,
);
path = Classes;
sourceTree = "<group>";
Expand All @@ -130,6 +134,28 @@
path = Models;
sourceTree = "<group>";
};
2FC654731A7B3FA1009078A2 /* List View */ = {
isa = PBXGroup;
children = (
2FB3330E179097BD002F2076 /* OCJJobListViewController.h */,
2FB3330F179097BD002F2076 /* OCJJobListViewController.m */,
2FA59BCD179DB7F10048130E /* OCJPositionCell.h */,
2FA59BCE179DB7F10048130E /* OCJPositionCell.m */,
);
path = "List View";
sourceTree = "<group>";
};
2FC654741A7B40C4009078A2 /* View Models */ = {
isa = PBXGroup;
children = (
2FC654781A7B4AC9009078A2 /* OCJPositionListViewModel.h */,
2FC654791A7B4AC9009078A2 /* OCJPositionListViewModel.m */,
2FC654751A7B40D4009078A2 /* OCJPositionViewModel.h */,
2FC654761A7B40D4009078A2 /* OCJPositionViewModel.m */,
);
path = "View Models";
sourceTree = "<group>";
};
2FD4139A1780CD12008A67BB = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -301,8 +327,10 @@
2FA59BCC179DB55A0048130E /* OCJPositionDetailsViewController.m in Sources */,
2FD413B31780CD13008A67BB /* main.m in Sources */,
2F924A091791F7E9003B30DC /* OCJPosition.m in Sources */,
2FC6547A1A7B4AC9009078A2 /* OCJPositionListViewModel.m in Sources */,
2FD413B71780CD13008A67BB /* OCJAppDelegate.m in Sources */,
2FA59BCF179DB7F10048130E /* OCJPositionCell.m in Sources */,
2FC654771A7B40D4009078A2 /* OCJPositionViewModel.m in Sources */,
2FB33310179097BD002F2076 /* OCJJobListViewController.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Expand Up @@ -7,15 +7,14 @@

#import "OCJJobListViewController.h"

#import "OCJAPIClient.h"
#import "OCJPosition.h"
#import "OCJPositionCell.h"
#import "OCJPositionDetailsViewController.h"
#import "OCJPositionListViewModel.h"
#import "OCJPositionViewModel.h"

@implementation OCJJobListViewController
{
@private
NSArray *_positions;
OCJPositionListViewModel *_listViewModel;
OCJPositionCell *_offscreenCell;
}

Expand All @@ -29,6 +28,8 @@ - (id)initWithCoder:(NSCoder *)aDecoder
selector:@selector(preferredContentSizeChanged:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];

_listViewModel = [[OCJPositionListViewModel alloc] init];
}

return self;
Expand All @@ -54,11 +55,11 @@ - (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

if (_positions.count == 0)
if (!_listViewModel.positionsLoaded)
{
[self.refreshControl beginRefreshing];
[self.tableView setContentOffset:CGPointMake(0.0f, -CGRectGetHeight(self.refreshControl.bounds)) animated:animated];
[self reloadJobs:self.refreshControl];
[self.refreshControl sendActionsForControlEvents:UIControlEventValueChanged];
}
}

Expand All @@ -73,39 +74,23 @@ - (void)preferredContentSizeChanged:(NSNotification *)notification

- (void)reloadJobs:(id)sender
{
// TODO: Do we want Markdown instead of HTML?
// If so, append "?markdown=1"
[[OCJAPIClient sharedClient] getPath:@"positions.json"
completionHandler:^(NSData *data, NSError *error) {
if (!error)
{
NSArray *rawPositions = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];

NSMutableArray *newPositions = [NSMutableArray arrayWithCapacity:rawPositions.count];

for (NSDictionary *positionInfo in rawPositions)
{
OCJPosition *position = [OCJPosition positionWithDictionary:positionInfo];

if (position)
{
[newPositions addObject:position];
}
}

_positions = [newPositions copy];

dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
});
}
else
{
// TODO: Show the error
NSLog(@"An error occurred while fetching postings: %@", error.localizedDescription);
}
}];
[_listViewModel reloadPositionsWithCompletionHandler:^(NSError *error) {
if (error)
{
// TODO: Show the error
NSLog(@"An error occurred while fetching postings: %@", error.localizedDescription);
}

dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.refreshControl endRefreshing];
});
}];
}

- (OCJPositionViewModel *)viewModelAtIndexPath:(NSIndexPath *)indexPath
{
return _listViewModel.positions[indexPath.row];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
Expand All @@ -115,9 +100,9 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
if ([segue.identifier isEqualToString:@"PositionDetailsSegue"])
{
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
OCJPosition *selectedPosition = _positions[selectedIndexPath.row];
OCJPositionViewModel *selectedPosition = [self viewModelAtIndexPath:selectedIndexPath];

((OCJPositionDetailsViewController *)segue.destinationViewController).position = selectedPosition;
((OCJPositionDetailsViewController *)segue.destinationViewController).viewModel = selectedPosition;
}
}

Expand All @@ -126,15 +111,16 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _positions.count;
return _listViewModel.positions.count;
}

static NSString *CellIdentifier = @"JobCell";

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
OCJPositionCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
cell.position = _positions[indexPath.row];
OCJPositionViewModel *viewModel = [self viewModelAtIndexPath:indexPath];
cell.viewModel = viewModel;

return cell;
}
Expand All @@ -146,7 +132,8 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa
_offscreenCell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
}

_offscreenCell.position = _positions[indexPath.row];
OCJPositionViewModel *viewModel = [self viewModelAtIndexPath:indexPath];
_offscreenCell.viewModel = viewModel;

// Let Auto Layout figure out how tall the cell needs to be
CGSize cellSize = [_offscreenCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
Expand Down
Expand Up @@ -7,10 +7,10 @@

@import UIKit;

@class OCJPosition;
@class OCJPositionViewModel;

@interface OCJPositionCell : UITableViewCell

@property (nonatomic) OCJPosition *position;
@property (nonatomic) OCJPositionViewModel *viewModel;

@end
Expand Up @@ -8,6 +8,9 @@
#import "OCJPositionCell.h"

#import "OCJPosition.h"
#import "OCJPositionViewModel.h"

static void *OCJPositionCellKVOContext = &OCJPositionCellKVOContext;

@interface OCJPositionCell()

Expand All @@ -30,6 +33,10 @@ - (id)initWithCoder:(NSCoder *)aDecoder
selector:@selector(preferredContentSizeChanged:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];

[self addObserver:self forKeyPath:@"viewModel.position.title" options:0 context:OCJPositionCellKVOContext];
[self addObserver:self forKeyPath:@"viewModel.subtitle" options:0 context:OCJPositionCellKVOContext];
[self addObserver:self forKeyPath:@"viewModel.position.location" options:0 context:OCJPositionCellKVOContext];
}

return self;
Expand All @@ -47,6 +54,30 @@ - (void)awakeFromNib
[self setFonts];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == OCJPositionCellKVOContext)
{
// Update the labels, etc
if ([keyPath isEqualToString:@"viewModel.position.title"])
{
self.titleLabel.text = self.viewModel.position.title;
}
else if ([keyPath isEqualToString:@"viewModel.subtitle"])
{
self.detailLabel.text = self.viewModel.subtitle;
}
else if ([keyPath isEqualToString:@"viewModel.position.location"])
{
self.locationLabel.text = self.viewModel.position.location;
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}


#pragma mark -

Expand All @@ -67,14 +98,4 @@ - (void)setFonts
self.locationLabel.font = [UIFont fontWithDescriptor:subheadDescriptor size:0.0f];
}

- (void)setPosition:(OCJPosition *)position
{
_position = position;

// Update the labels, etc
self.titleLabel.text = position.title;
self.detailLabel.text = [NSString stringWithFormat:@"%@ — %@", position.company, position.type];
self.locationLabel.text = position.location;
}

@end
4 changes: 2 additions & 2 deletions Source/OctoJobs/Classes/OCJPositionDetailsViewController.h
Expand Up @@ -7,10 +7,10 @@

@import UIKit;

@class OCJPosition;
@class OCJPositionViewModel;

@interface OCJPositionDetailsViewController : UIViewController

@property (nonatomic) OCJPosition *position;
@property (nonatomic) OCJPositionViewModel *viewModel;

@end
51 changes: 4 additions & 47 deletions Source/OctoJobs/Classes/OCJPositionDetailsViewController.m
Expand Up @@ -7,7 +7,7 @@

#import "OCJPositionDetailsViewController.h"

#import "OCJPosition.h"
#import "OCJPositionViewModel.h"

@interface OCJPositionDetailsViewController () <UIWebViewDelegate>

Expand All @@ -30,9 +30,9 @@ - (void)viewDidLoad

#pragma mark -

- (void)setPosition:(OCJPosition *)position
- (void)setViewModel:(OCJPositionViewModel *)viewModel
{
_position = position;
_viewModel = viewModel;

if ([self isViewLoaded])
{
Expand All @@ -42,50 +42,7 @@ - (void)setPosition:(OCJPosition *)position

- (void)updateUI
{
// Show the position details in our formatted template
NSString *templatePath = [[NSBundle mainBundle] pathForResource:@"PositionTemplate" ofType:@"html"];
NSString *template = [NSString stringWithContentsOfFile:templatePath encoding:NSUTF8StringEncoding error:NULL];

NSMutableString *content = [NSMutableString stringWithString:template];
[content replaceOccurrencesOfString:@"<%= position_title %>"
withString:self.position.title
options:0
range:NSMakeRange(0, content.length)];

// Logo needs to be injected instead of populated (in case posting doesn't have a logo)
NSString *logoContents;
if (![self.position.companyLogoURL isKindOfClass:[NSNull class]])
{
logoContents = [NSString stringWithFormat:@"<div class=\"logo\"><img src=\"%@\"></div>",
self.position.companyLogoURL];
}
else
{
logoContents = [NSString string];
}
[content replaceOccurrencesOfString:@"<%= company_logo %>"
withString:logoContents
options:0
range:NSMakeRange(0, content.length)];

[content replaceOccurrencesOfString:@"<%= position_description %>"
withString:self.position.HTMLDescription
options:0
range:NSMakeRange(0, content.length)];
[content replaceOccurrencesOfString:@"<%= how_to_apply %>"
withString:self.position.howToApply
options:0
range:NSMakeRange(0, content.length)];
[content replaceOccurrencesOfString:@"<%= position_type %>"
withString:self.position.type
options:0
range:NSMakeRange(0, content.length)];
[content replaceOccurrencesOfString:@"<%= position_location %>"
withString:self.position.location
options:0
range:NSMakeRange(0, content.length)];

[self.webView loadHTMLString:content baseURL:nil];
[self.webView loadHTMLString:self.viewModel.positionDetailsHTML baseURL:nil];
}


Expand Down