get a list of posts

This commit is contained in:
Sami Samhuri 2015-03-29 23:42:04 -07:00
parent 8b81a37ec8
commit 905c60d434
26 changed files with 1373 additions and 44 deletions

View file

@ -16,6 +16,14 @@
7B5C4BED19F2606900667D48 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B5C4BEC19F2606900667D48 /* Images.xcassets */; };
7B5C4BF019F2606900667D48 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7B5C4BEE19F2606900667D48 /* LaunchScreen.xib */; };
7B5C4BFC19F2606900667D48 /* BlogTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BFB19F2606900667D48 /* BlogTests.m */; };
7B9E64281A227BFE0072FF42 /* Post.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E64271A227BFE0072FF42 /* Post.m */; };
7B9E642D1A227FA20072FF42 /* NSDate+marshmallows.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E642A1A227FA20072FF42 /* NSDate+marshmallows.m */; };
7B9E642E1A227FA20072FF42 /* NSString+marshmallows.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E642C1A227FA20072FF42 /* NSString+marshmallows.m */; };
7B9E64421A22F3840072FF42 /* BlogService.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E64411A22F3840072FF42 /* BlogService.m */; };
7B9E644C1A230B940072FF42 /* JSONHTTPClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E644B1A230B940072FF42 /* JSONHTTPClient.m */; };
7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B9E644E1A23129B0072FF42 /* BlogStatus.m */; };
7BF029331A27117200E42EDE /* ModelStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BF029321A27117200E42EDE /* ModelStore.m */; };
7BF029381A280CB200E42EDE /* BlogController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BF029371A280CB200E42EDE /* BlogController.m */; };
B8B8958B2AA40812EFE04FEF /* libPods-BlogTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C36BC848680D4F831E4DE23 /* libPods-BlogTests.a */; };
/* End PBXBuildFile section */
@ -46,6 +54,22 @@
7B5C4BF519F2606900667D48 /* BlogTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BlogTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7B5C4BFA19F2606900667D48 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7B5C4BFB19F2606900667D48 /* BlogTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BlogTests.m; sourceTree = "<group>"; };
7B9E64261A227BFE0072FF42 /* Post.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Post.h; sourceTree = "<group>"; };
7B9E64271A227BFE0072FF42 /* Post.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Post.m; sourceTree = "<group>"; };
7B9E64291A227FA20072FF42 /* NSDate+marshmallows.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+marshmallows.h"; sourceTree = "<group>"; };
7B9E642A1A227FA20072FF42 /* NSDate+marshmallows.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+marshmallows.m"; sourceTree = "<group>"; };
7B9E642B1A227FA20072FF42 /* NSString+marshmallows.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+marshmallows.h"; sourceTree = "<group>"; };
7B9E642C1A227FA20072FF42 /* NSString+marshmallows.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+marshmallows.m"; sourceTree = "<group>"; };
7B9E64401A22F3840072FF42 /* BlogService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlogService.h; sourceTree = "<group>"; };
7B9E64411A22F3840072FF42 /* BlogService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlogService.m; sourceTree = "<group>"; };
7B9E644A1A230B940072FF42 /* JSONHTTPClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSONHTTPClient.h; sourceTree = "<group>"; };
7B9E644B1A230B940072FF42 /* JSONHTTPClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSONHTTPClient.m; sourceTree = "<group>"; };
7B9E644D1A23129B0072FF42 /* BlogStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlogStatus.h; sourceTree = "<group>"; };
7B9E644E1A23129B0072FF42 /* BlogStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlogStatus.m; sourceTree = "<group>"; };
7BF029311A27117200E42EDE /* ModelStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModelStore.h; sourceTree = "<group>"; };
7BF029321A27117200E42EDE /* ModelStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModelStore.m; sourceTree = "<group>"; };
7BF029361A280CB200E42EDE /* BlogController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BlogController.h; sourceTree = "<group>"; };
7BF029371A280CB200E42EDE /* BlogController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BlogController.m; sourceTree = "<group>"; };
9C36BC848680D4F831E4DE23 /* libPods-BlogTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-BlogTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
A2EB178BEF4356711B2710AE /* Pods.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.release.xcconfig; path = "Pods/Target Support Files/Pods/Pods.release.xcconfig"; sourceTree = "<group>"; };
AB1234AC662F1A1B7BD87AB0 /* Pods-BlogTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BlogTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-BlogTests/Pods-BlogTests.release.xcconfig"; sourceTree = "<group>"; };
@ -107,6 +131,9 @@
7B5C4BDB19F2606900667D48 /* Blog */ = {
isa = PBXGroup;
children = (
7B9E64331A2281FC0072FF42 /* Categories */,
7BF029341A27118300E42EDE /* Models */,
7BF029351A27119B00E42EDE /* Service */,
7B5C4BE019F2606900667D48 /* AppDelegate.h */,
7B5C4BE119F2606900667D48 /* AppDelegate.m */,
7B5C4BE319F2606900667D48 /* MasterViewController.h */,
@ -147,6 +174,43 @@
name = "Supporting Files";
sourceTree = "<group>";
};
7B9E64331A2281FC0072FF42 /* Categories */ = {
isa = PBXGroup;
children = (
7B9E64291A227FA20072FF42 /* NSDate+marshmallows.h */,
7B9E642A1A227FA20072FF42 /* NSDate+marshmallows.m */,
7B9E642B1A227FA20072FF42 /* NSString+marshmallows.h */,
7B9E642C1A227FA20072FF42 /* NSString+marshmallows.m */,
);
name = Categories;
sourceTree = "<group>";
};
7BF029341A27118300E42EDE /* Models */ = {
isa = PBXGroup;
children = (
7B9E644D1A23129B0072FF42 /* BlogStatus.h */,
7B9E644E1A23129B0072FF42 /* BlogStatus.m */,
7B9E64261A227BFE0072FF42 /* Post.h */,
7B9E64271A227BFE0072FF42 /* Post.m */,
);
name = Models;
sourceTree = "<group>";
};
7BF029351A27119B00E42EDE /* Service */ = {
isa = PBXGroup;
children = (
7B9E64401A22F3840072FF42 /* BlogService.h */,
7B9E64411A22F3840072FF42 /* BlogService.m */,
7B9E644A1A230B940072FF42 /* JSONHTTPClient.h */,
7B9E644B1A230B940072FF42 /* JSONHTTPClient.m */,
7BF029311A27117200E42EDE /* ModelStore.h */,
7BF029321A27117200E42EDE /* ModelStore.m */,
7BF029361A280CB200E42EDE /* BlogController.h */,
7BF029371A280CB200E42EDE /* BlogController.m */,
);
name = Service;
sourceTree = "<group>";
};
F9CC479BA9A49F1EDD27B0AB /* Frameworks */ = {
isa = PBXGroup;
children = (
@ -209,7 +273,6 @@
TargetAttributes = {
7B5C4BD819F2606900667D48 = {
CreatedOnToolsVersion = 6.0.1;
DevelopmentTeam = B2W6993X5Z;
};
7B5C4BF419F2606900667D48 = {
CreatedOnToolsVersion = 6.0.1;
@ -324,10 +387,18 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7B9E64281A227BFE0072FF42 /* Post.m in Sources */,
7B5C4BE219F2606900667D48 /* AppDelegate.m in Sources */,
7B9E644C1A230B940072FF42 /* JSONHTTPClient.m in Sources */,
7B9E642D1A227FA20072FF42 /* NSDate+marshmallows.m in Sources */,
7B5C4BE519F2606900667D48 /* MasterViewController.m in Sources */,
7B9E642E1A227FA20072FF42 /* NSString+marshmallows.m in Sources */,
7B9E64421A22F3840072FF42 /* BlogService.m in Sources */,
7B5C4BDF19F2606900667D48 /* main.m in Sources */,
7BF029331A27117200E42EDE /* ModelStore.m in Sources */,
7BF029381A280CB200E42EDE /* BlogController.m in Sources */,
7B5C4BE819F2606900667D48 /* DetailViewController.m in Sources */,
7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -453,6 +524,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "iPhone Developer";
DEBUG_INFORMATION_FORMAT = dwarf;
INFOPLIST_FILE = Blog/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -6,12 +6,11 @@
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@import UIKit;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic, strong) UIWindow *window;
@end

View file

@ -8,15 +8,19 @@
#import "AppDelegate.h"
#import "DetailViewController.h"
#import "BlogService.h"
@interface AppDelegate () <UISplitViewControllerDelegate>
@property (nonatomic, readonly, strong) BlogService *blogService;
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
_blogService = [BlogService new];
// Override point for customization after application launch.
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
@ -47,10 +51,10 @@
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
#pragma mark - Split view
#pragma mark - UISplitViewDelegate methods
- (BOOL)splitViewController:(UISplitViewController *)splitViewController collapseSecondaryViewController:(UIViewController *)secondaryViewController ontoPrimaryViewController:(UIViewController *)primaryViewController {
if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]] && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] detailItem] == nil)) {
if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[DetailViewController class]] && ([(DetailViewController *)[(UINavigationController *)secondaryViewController topViewController] post] == nil)) {
// Return YES to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
return YES;
} else {
@ -58,4 +62,6 @@
}
}
#pragma mark - AppObjectDelegate methods
@end

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6245" systemVersion="14A389" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="H1p-Uh-vWS">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14B25" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="H1p-Uh-vWS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6238"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
</dependencies>
<scenes>
<!--Master-->
@ -113,9 +113,28 @@
<outlet property="delegate" destination="7bK-jq-Zjz" id="RA6-mI-bju"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Master" id="Zdf-7t-Un8"/>
<navigationItem key="navigationItem" title="Master" id="Zdf-7t-Un8">
<connections>
<outlet property="leftBarButtonItem" destination="8HS-W8-a6l" id="CwH-zD-2Qm"/>
<outlet property="rightBarButtonItem" destination="u2a-vi-nHQ" id="zsl-st-de9"/>
</connections>
</navigationItem>
<connections>
<outlet property="addButton" destination="u2a-vi-nHQ" id="BNL-ge-ZGw"/>
<outlet property="publishButton" destination="8HS-W8-a6l" id="amK-fb-yQq"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Rux-fX-hf1" sceneMemberID="firstResponder"/>
<barButtonItem systemItem="action" id="8HS-W8-a6l">
<connections>
<action selector="publish:" destination="7bK-jq-Zjz" id="zFt-xZ-Pf9"/>
</connections>
</barButtonItem>
<barButtonItem systemItem="add" id="u2a-vi-nHQ">
<connections>
<action selector="insertNewObject:" destination="7bK-jq-Zjz" id="ycC-vI-2WT"/>
</connections>
</barButtonItem>
</objects>
<point key="canvasLocation" x="709" y="-630"/>
</scene>

43
Blog/BlogController.h Normal file
View file

@ -0,0 +1,43 @@
//
// BlogController.h
// Blog
//
// Created by Sami Samhuri on 2014-11-27.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
@import Foundation;
NSString *BlogStatusChangedNotification;
NSString *BlogDraftsChangedNotification;
NSString *BlogDraftAddedNotification;
NSString *BlogDraftRemovedNotification;
NSString *BlogPublishedPostsChangedNotification;
NSString *BlogPublishedPostAddedNotification;
NSString *BlogPublishedPostRemovedNotification;
NSString *BlogPostChangedNotification;
NSString *BlogPostDeletedNotification;
@class PMKPromise;
@class ModelStore;
@class BlogService;
@interface BlogController : NSObject
- (instancetype)initWithService:(BlogService *)service store:(ModelStore *)store;
- (NSURL *)previewURLForPostWithPath:(NSString *)path;
- (PMKPromise *)requestBlogStatus;
- (PMKPromise *)requestDrafts;
- (PMKPromise *)requestPublishedPosts;
- (PMKPromise *)requestPostWithPath:(NSString *)path;
- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link;
- (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link;
- (PMKPromise *)requestPublishDraftWithPath:(NSString *)path;
- (PMKPromise *)requestUnpublishPostWithPath:(NSString *)path;
- (PMKPromise *)requestDeletePostWithPath:(NSString *)path;
@end

138
Blog/BlogController.m Normal file
View file

@ -0,0 +1,138 @@
//
// BlogController.m
// Blog
//
// Created by Sami Samhuri on 2014-11-27.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import "BlogController.h"
#import <PromiseKit/PromiseKit.h>
#import "BlogService.h"
#import "ModelStore.h"
#import "BlogStatus.h"
#import "Post.h"
NSString *BlogStatusChangedNotification = @"BlogStatusChangedNotification";
NSString *BlogDraftsChangedNotification = @"BlogDraftsChangedNotification";
NSString *BlogDraftAddedNotification = @"BlogDraftAddedNotification";
NSString *BlogDraftRemovedNotification = @"BlogDraftRemovedNotification";
NSString *BlogPublishedPostsChangedNotification = @"BlogPublishedPostsChangedNotification";
NSString *BlogPublishedPostAddedNotification = @"BlogPostAddedNotification";
NSString *BlogPublishedPostRemovedNotification = @"BlogPostRemovedNotification";
NSString *BlogPostChangedNotification = @"BlogPostChangedNotification";
NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
@implementation BlogController {
BlogService *_service;
ModelStore *_store;
}
- (instancetype)initWithService:(BlogService *)service store:(ModelStore *)store {
self = [super init];
_service = service;
_store = store;
return self;
}
- (NSURL *)previewURLForPostWithPath:(NSString *)path {
return [_service previewURLForPostWithPath:path];
}
- (PMKPromise *)requestBlogStatus {
BlogStatus *status = [_store blogStatus];
if (status) {
return [PMKPromise promiseWithValue:status];
}
else {
return [_service requestBlogStatus].then(^(BlogStatus *status) {
[_store saveBlogStatus:status];
return status;
});
}
}
- (PMKPromise *)requestDrafts {
NSArray *posts = [_store drafts];
if (posts) {
return [PMKPromise promiseWithValue:posts];
}
else {
return [_service requestDrafts].then(^(NSArray *posts) {
[_store saveDrafts:posts];
return posts;
});
}
}
- (PMKPromise *)requestPublishedPosts {
NSArray *posts = [_store publishedPosts];
if (posts) {
return [PMKPromise promiseWithValue:posts];
}
else {
return [_service requestPublishedPosts].then(^(NSArray *posts) {
[_store savePublishedPosts:posts];
return posts;
});
}
}
- (PMKPromise *)requestPostWithPath:(NSString *)path {
Post *post = [_store postWithPath:path];
if (post) {
return [PMKPromise promiseWithValue:post];
}
else {
return [_service requestPostWithPath:path].then(^(Post *post) {
[_store savePost:post];
return post;
});
}
}
- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link {
Post *post = [[Post alloc] initWithDictionary:@{@"objectID": draftID ?: [NSNull null],
@"title": title ?: [NSNull null],
@"body": body ?: [NSNull null],
@"link": link ?: [NSNull null],
@"draft": @YES,
} error:nil];
return [_service requestCreateDraftWithID:draftID title:title body:body link:link].then(^(Post *post) {
[_store addDraft:post];
return post;
});
}
- (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link {
return [_service requestUpdatePostWithPath:path title:title body:body link:link].then(^(Post *post) {
[_store savePost:post];
return post;
});
}
- (PMKPromise *)requestPublishDraftWithPath:(NSString *)path {
return [_service requestPublishDraftWithPath:path].then(^(Post *post) {
[_store removeDraftWithPath:path];
[_store addPublishedPost:post];
return post;
});
}
- (PMKPromise *)requestUnpublishPostWithPath:(NSString *)path {
return [_service requestUnpublishPostWithPath:path].then(^(Post *post) {
[_store removePostWithPath:path];
[_store addDraft:post];
return post;
});
}
- (PMKPromise *)requestDeletePostWithPath:(NSString *)path {
return [_service requestDeletePostWithPath:path].then(^(id _) {
[_store removePostWithPath:path];
[_store removeDraftWithPath:path];
return _;
});
}
@end

39
Blog/BlogService.h Normal file
View file

@ -0,0 +1,39 @@
//
// BlogService.h
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
@import Foundation;
#import <PromiseKit/PromiseKit.h>
extern NSString * const BlogServiceErrorDomain;
typedef NS_ENUM(NSUInteger, BlogServiceErrorCode) {
BlogServiceErrorCodeWTF
};
@class JSONHTTPClient;
@interface BlogService : NSObject
- (instancetype)initWithRootURL:(NSString *)rootURL client:(JSONHTTPClient *)client;
- (NSURL *)previewURLForPostWithPath:(NSString *)path;
- (PMKPromise *)requestBlogStatus;
- (PMKPromise *)requestPublishEnvironment:(NSString *)environment;
- (PMKPromise *)requestDrafts;
- (PMKPromise *)requestPublishedPosts;
- (PMKPromise *)requestPostWithPath:(NSString *)path;
- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link;
- (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link;
- (PMKPromise *)requestPublishDraftWithPath:(NSString *)path;
- (PMKPromise *)requestUnpublishPostWithPath:(NSString *)path;
- (PMKPromise *)requestDeletePostWithPath:(NSString *)path;
@end

125
Blog/BlogService.m Normal file
View file

@ -0,0 +1,125 @@
//
// BlogService.m
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import "BlogService.h"
#import <Mantle/MTLJSONAdapter.h>
#import "NSString+marshmallows.h"
#import "JSONHTTPClient.h"
#import "BlogStatus.h"
#import "Post.h"
NSString * const BlogServiceErrorDomain = @"BlogServiceErrorDomain";
@interface BlogService ()
@property (nonatomic, readonly, strong) NSString *rootURL;
@property (nonatomic, readonly, strong) JSONHTTPClient *client;
@end
@implementation BlogService
- (instancetype)initWithRootURL:(NSString *)rootURL client:(JSONHTTPClient *)client {
NSParameterAssert([rootURL length]);
NSParameterAssert(client);
self = [super init];
_rootURL = [rootURL mm_stringByReplacing:@"/$" with:@""];
_client = client;
return self;
}
- (NSURL *)previewURLForPostWithPath:(NSString *)path {
return [self urlFor:@"%@/preview", path];
}
- (NSURL *)urlFor:(NSString *)path, ... {
va_list args;
va_start(args, path);
path = [[NSString alloc] initWithFormat:path arguments:args];
va_end(args);
NSString *slash = [path hasPrefix:@"/"] ? @"" : @"/";
NSString *urlString = [self.rootURL stringByAppendingFormat:@"%@%@", slash, path];
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
@throw [NSException exceptionWithName:@"BlogServiceException" reason:@"Invalid URL" userInfo:@{@"URL": urlString ?: [NSNull null]}];
}
return url;
}
- (id (^)(NSDictionary *))decodePostBlock {
return ^(NSDictionary *root) {
NSError *error = nil;
Post *post = [MTLJSONAdapter modelOfClass:[Post class] fromJSONDictionary:root[@"post"] error:&error];
return post ?: error;
};
}
- (id (^)(NSDictionary *))decodePostsBlock {
return ^(NSDictionary *root) {
NSError *error = nil;
NSArray *posts = [MTLJSONAdapter modelsOfClass:[Post class] fromJSONArray:root[@"posts"] error:&error];
return posts ?: error;
};
}
- (PMKPromise *)requestBlogStatus {
return [self.client get:[self urlFor:@"/status"] headers:nil].then(^(NSDictionary *root) {
NSError *error = nil;
BlogStatus *status = [MTLJSONAdapter modelOfClass:[BlogStatus class] fromJSONDictionary:root[@"status"] error:&error];
return status ?: error;
});
}
- (PMKPromise *)requestPublishEnvironment:(NSString *)environment {
NSDictionary *fields = @{@"env": environment ?: @"staging"};
return [self.client postJSON:[self urlFor:@"/publish"] headers:nil fields:fields];
}
- (PMKPromise *)requestDrafts {
return [self.client get:[self urlFor:@"/drafts"] headers:nil].then([self decodePostsBlock]);
}
- (PMKPromise *)requestPublishedPosts {
return [self.client get:[self urlFor:@"/posts"] headers:nil].then([self decodePostsBlock]);
}
- (PMKPromise *)requestPostWithPath:(NSString *)path {
return [self.client get:[self urlFor:path] headers:nil].then([self decodePostBlock]);
}
- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link {
NSDictionary *fields = @{@"id": draftID,
@"title": title,
@"body": body,
@"link": link,
};
return [self.client postJSON:[self urlFor:@"/drafts"] headers:nil fields:fields].then([self decodePostBlock]);
}
- (PMKPromise *)requestPublishDraftWithPath:(NSString *)path {
return [self.client post:[self urlFor:@"%@/publish", path] headers:nil].then([self decodePostBlock]);
}
- (PMKPromise *)requestUnpublishPostWithPath:(NSString *)path {
return [self.client post:[self urlFor:@"%@/unpublish", path] headers:nil];
}
- (PMKPromise *)requestUpdatePostWithPath:(NSString *)path title:(NSString *)title body:(NSString *)body link:(NSString *)link {
NSDictionary *fields = @{@"title": title,
@"body": body,
@"link": link,
};
return [self.client putJSON:[self urlFor:path] headers:nil fields:fields];
}
- (PMKPromise *)requestDeletePostWithPath:(NSString *)path {
return [self.client delete:[self urlFor:path] headers:nil];
}
@end

18
Blog/BlogStatus.h Normal file
View file

@ -0,0 +1,18 @@
//
// BlogStatus.h
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
@import Foundation;
#import <Mantle/Mantle.h>
@interface BlogStatus : MTLModel <MTLJSONSerializing>
@property (nonatomic, readonly, strong) NSString *localVersion;
@property (nonatomic, readonly, strong) NSString *remoteVersion;
@property (nonatomic, readonly, getter=isDirty) BOOL dirty;
@end

19
Blog/BlogStatus.m Normal file
View file

@ -0,0 +1,19 @@
//
// BlogStatus.m
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import "BlogStatus.h"
@implementation BlogStatus
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{@"localVersion": @"local-version",
@"remoteVersion": @"remote-version",
};
}
@end

View file

@ -6,12 +6,13 @@
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@import UIKit;
@class Post;
@interface DetailViewController : UIViewController
@property (strong, nonatomic) id detailItem;
@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@property (strong, nonatomic) Post *post;
@end

View file

@ -7,19 +7,22 @@
//
#import "DetailViewController.h"
#import "Post.h"
@interface DetailViewController ()
@property (weak, nonatomic) IBOutlet UILabel *detailDescriptionLabel;
@end
@implementation DetailViewController
#pragma mark - Managing the detail item
- (void)setDetailItem:(id)newDetailItem {
if (_detailItem != newDetailItem) {
_detailItem = newDetailItem;
- (void)setPost:(id)newPost {
if (_post != newPost) {
_post = newPost;
// Update the view.
[self configureView];
}
@ -27,8 +30,10 @@
- (void)configureView {
// Update the user interface for the detail item.
if (self.detailItem) {
self.detailDescriptionLabel.text = [self.detailItem description];
if (self.post) {
// FIXME: date, link (edit, open), status (draft, published), delete, preview, publish
self.navigationItem.title = self.post.title ?: @"Untitled";
self.detailDescriptionLabel.text = self.post.body;
}
}

33
Blog/JSONHTTPClient.h Normal file
View file

@ -0,0 +1,33 @@
//
// JSONHTTPClient.h
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <PromiseKit/PromiseKit.h>
extern NSString * const JSONHTTPClientErrorDomain;
typedef enum : NSUInteger {
JSONHTTPClientErrorCodeWTF,
JSONHTTPClientErrorCodeInvalidResponse,
JSONHTTPClientErrorCodeRequestFailed
} JSONHTTPClientErrorCode;
@interface JSONHTTPClient : NSObject
@property (nonatomic, strong) NSDictionary *defaultHeaders;
- (instancetype)initWithSession:(NSURLSession *)session;
- (PMKPromise *)request:(NSURLRequest *)request;
- (PMKPromise *)get:(NSURL *)url headers:(NSDictionary *)headers;
- (PMKPromise *)putJSON:(NSURL *)url headers:(NSDictionary *)headers fields:(NSDictionary *)fields;
- (PMKPromise *)postJSON:(NSURL *)url headers:(NSDictionary *)headers fields:(NSDictionary *)fields;
- (PMKPromise *)post:(NSURL *)url headers:(NSDictionary *)headers;
- (PMKPromise *)delete:(NSURL *)url headers:(NSDictionary *)headers;
@end

133
Blog/JSONHTTPClient.m Normal file
View file

@ -0,0 +1,133 @@
//
// JSONHTTPClient.m
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import "JSONHTTPClient.h"
NSString * const JSONHTTPClientErrorDomain = @"JSONHTTPClientErrorDomain";
@interface JSONHTTPClient ()
@property (nonatomic, readonly, strong) NSURLSession *session;
@end
@implementation JSONHTTPClient
- (instancetype)initWithSession:(NSURLSession *)session {
self = [super init];
_session = session;
return self;
}
- (PMKPromise *)get:(NSURL *)url headers:(NSDictionary *)headers {
return [self request:[self requestWithMethod:@"GET" URL:url headers:headers data:nil]];
}
- (PMKPromise *)putJSON:(NSURL *)url headers:(NSDictionary *)headers fields:(NSDictionary *)fields {
return [self JSONRequestWithMethod:@"PUT" url:url headers:headers fields:fields];
}
- (PMKPromise *)postJSON:(NSURL *)url headers:(NSDictionary *)headers fields:(NSDictionary *)fields {
return [self JSONRequestWithMethod:@"POST" url:url headers:headers fields:fields];
}
- (PMKPromise *)post:(NSURL *)url headers:(NSDictionary *)headers {
return [self request:[self requestWithMethod:@"POST" URL:url headers:headers data:nil]];
}
- (PMKPromise *)delete:(NSURL *)url headers:(NSDictionary *)headers {
return [self request:[self requestWithMethod:@"DELETE" URL:url headers:headers data:nil]];
}
- (PMKPromise *)request:(NSURLRequest *)request {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[[self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = [response isKindOfClass:[NSHTTPURLResponse class]] ? (NSHTTPURLResponse *)response : nil;
if (error) {
reject(error);
}
else if (httpResponse) {
NSDictionary *headers = [httpResponse allHeaderFields];
NSString *type = headers[@"Content-Type"];
if (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
if ([type hasPrefix:@"application/json"]) {
NSError *jsonError = nil;
id root = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (root)
{
fulfill(PMKManifold(root, headers, @(httpResponse.statusCode)));
}
else {
reject(jsonError);
}
}
else if ([data length] > 0) {
NSDictionary *info = @{NSLocalizedDescriptionKey: @"response type is not JSON",
@"type": type ?: [NSNull null],
@"length": headers[@"Content-Length"] ?: [NSNull null],
@"request": request,
@"response": httpResponse,
};
NSError *error = [NSError errorWithDomain:JSONHTTPClientErrorDomain code:JSONHTTPClientErrorCodeInvalidResponse userInfo:info];
reject(error);
}
else {
fulfill(PMKManifold(nil, headers, @(httpResponse.statusCode)));
}
}
else {
NSDictionary *info = @{NSLocalizedDescriptionKey: @"HTTP request failed",
@"status": @(httpResponse.statusCode),
@"request": request,
@"response": httpResponse,
};
NSError *error = [NSError errorWithDomain:JSONHTTPClientErrorDomain code:JSONHTTPClientErrorCodeRequestFailed userInfo:info];
reject(error);
}
}
else {
NSDictionary *info = @{NSLocalizedDescriptionKey: @"response is not an HTTP response"};
NSError *error = [NSError errorWithDomain:JSONHTTPClientErrorDomain code:JSONHTTPClientErrorCodeWTF userInfo:info];
reject(error);
}
}] resume];
}];
}
- (NSURLRequest *)requestWithMethod:(NSString *)method URL:(NSURL *)url headers:(NSDictionary *)headers data:(NSData *)data {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:method];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
for (NSString *key in [self.defaultHeaders allKeys]) {
[request setValue:self.defaultHeaders[key] forHTTPHeaderField:key];
}
for (NSString *key in [headers allKeys]) {
[request setValue:headers[key] forHTTPHeaderField:key];
}
if (data) {
[request setValue:[NSString stringWithFormat:@"%lu", [data length]] forKey:@"Content-Length"];
[request setHTTPBody:data];
}
return request;
}
- (PMKPromise *)JSONRequestWithMethod:(NSString *)method url:(NSURL *)url headers:(NSDictionary *)headers fields:(NSDictionary *)fields {
NSMutableDictionary *newHeaders = [headers ?: @{} mutableCopy];
newHeaders[@"Content-Type"] = @"application/json";
NSError *error = nil;
NSData *data = [NSJSONSerialization dataWithJSONObject:fields options:0 error:&error];
if (data) {
return [self request:[self requestWithMethod:method URL:url headers:newHeaders data:data]];
}
else {
NSLog(@"error: %@ %@", error, [error userInfo]);
return [PMKPromise promiseWithValue:error];
}
}
@end

View file

@ -6,14 +6,9 @@
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@class DetailViewController;
@import UIKit;
@interface MasterViewController : UITableViewController
@property (strong, nonatomic) DetailViewController *detailViewController;
@end

View file

@ -8,10 +8,22 @@
#import "MasterViewController.h"
#import "DetailViewController.h"
#import "Post.h"
#import "BlogController.h"
#import "ModelStore.h"
#import "BlogService.h"
#import "YapDatabaseConnection.h"
#import "YapDatabase.h"
#import "JSONHTTPClient.h"
@interface MasterViewController ()
@property NSMutableArray *objects;
@property (strong, nonatomic) NSMutableArray *posts;
@property (strong, nonatomic) DetailViewController *detailViewController;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton;
@property (strong, nonatomic) BlogController *blogController;
@end
@implementation MasterViewController
@ -22,16 +34,44 @@
self.clearsSelectionOnViewWillAppear = NO;
self.preferredContentSize = CGSizeMake(320.0, 600.0);
}
NSString *cachesPath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).lastObject;
NSString *path = [cachesPath stringByAppendingPathComponent:@"blog.sqlite"];
YapDatabase *database = [[YapDatabase alloc] initWithPath:path];
YapDatabaseConnection *connection = [database newConnection];
ModelStore *store = [[ModelStore alloc] initWithConnection:connection];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
JSONHTTPClient *client = [[JSONHTTPClient alloc] initWithSession:session];
BlogService *service = [[BlogService alloc] initWithRootURL:@"http://ocean.samhuri.net:6706/" client:client];
self.blogController = [[BlogController alloc] initWithService:service store:store];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// self.navigationItem.leftBarButtonItem = self.editButtonItem;
UINavigationController *detailNavController = self.splitViewController.viewControllers.lastObject;
self.detailViewController = (DetailViewController *)detailNavController.topViewController;
}
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;
self.detailViewController = (DetailViewController *)[[self.splitViewController.viewControllers lastObject] topViewController];
- (void)viewWillAppear:(BOOL)animated;
{
[super viewWillAppear:animated];
if (!self.posts) {
[self requestDrafts];
}
}
- (void)requestDrafts {
// TODO: show a spinner
[self.blogController requestDrafts].then(^(NSArray *drafts) {
return [self.blogController requestPublishedPosts].then(^(NSArray *posts) {
NSLog(@"drafts = %@", drafts);
NSLog(@"posts = %@", posts);
self.posts = [drafts mutableCopy];
[self.posts addObjectsFromArray:posts];
[self.tableView reloadData];
});
});
}
- (void)didReceiveMemoryWarning {
@ -39,23 +79,25 @@
// Dispose of any resources that can be recreated.
}
- (void)insertNewObject:(id)sender {
if (!self.objects) {
self.objects = [[NSMutableArray alloc] init];
}
[self.objects insertObject:[NSDate date] atIndex:0];
- (IBAction)insertNewObject:(id)sender {
Post *post = [[Post alloc] initWithDictionary:@{@"draft": @(YES)} error:nil];
[self.posts insertObject:post atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
- (IBAction)publish:(id)sender {
NSLog(@"publish");
}
#pragma mark - Segues
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
NSDate *object = self.objects[indexPath.row];
Post *post = self.posts[indexPath.row];
DetailViewController *controller = (DetailViewController *)[[segue destinationViewController] topViewController];
[controller setDetailItem:object];
[controller setPost:post];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
@ -68,14 +110,16 @@
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.objects.count;
return self.posts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
NSDate *object = self.objects[indexPath.row];
cell.textLabel.text = [object description];
Post *post = self.posts[indexPath.row];
// FIXME: unique title
cell.textLabel.text = post.title ?: @"Untitled";
cell.detailTextLabel.text = post.draft ? @"Draft" : post.formattedDate;
return cell;
}
@ -86,9 +130,11 @@
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.objects removeObjectAtIndex:indexPath.row];
[self.posts removeObjectAtIndex:indexPath.row];
// TODO: delete from server
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
}
else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}

37
Blog/ModelStore.h Normal file
View file

@ -0,0 +1,37 @@
//
// ModelStore.h
// Blog
//
// Created by Sami Samhuri on 2014-11-26.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
@class PMKPromise;
@class YapDatabaseConnection;
@class BlogStatus;
@class Post;
@interface ModelStore : NSObject
- (instancetype)initWithConnection:(YapDatabaseConnection *)connection;
- (BlogStatus *)blogStatus;
- (NSArray *)drafts;
- (NSArray *)publishedPosts;
- (Post *)postWithPath:(NSString *)path;
- (PMKPromise *)saveBlogStatus:(BlogStatus *)blogStatus;
- (PMKPromise *)savePost:(Post *)post;
- (PMKPromise *)saveDrafts:(NSArray *)posts;
- (PMKPromise *)savePublishedPosts:(NSArray *)posts;
- (PMKPromise *)addDraft:(Post *)post;
- (PMKPromise *)addPublishedPost:(Post *)post;
- (PMKPromise *)removeDraftWithPath:(NSString *)path;
- (PMKPromise *)removePostWithPath:(NSString *)path;
@end

175
Blog/ModelStore.m Normal file
View file

@ -0,0 +1,175 @@
//
// ModelStore.m
// Blog
//
// Created by Sami Samhuri on 2014-11-26.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import "ModelStore.h"
#import <PromiseKit/PromiseKit.h>
#import <YapDatabase/YapDatabase.h>
#import "BlogStatus.h"
#import "Post.h"
@implementation ModelStore {
YapDatabaseConnection *_connection;
}
- (instancetype)initWithConnection:(YapDatabaseConnection *)connection {
self = [super init];
_connection = connection;
return self;
}
- (BlogStatus *)blogStatus {
__block BlogStatus *status = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
status = [transaction objectForKey:@"status" inCollection:@"BlogStatus"];
}];
return status;
}
- (NSArray *)drafts {
__block NSMutableArray *posts = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSArray *postPaths = [transaction objectForKey:@"drafts" inCollection:@"PostCollection"];
if (postPaths) {
[transaction enumerateObjectsForKeys:postPaths inCollection:@"Post" unorderedUsingBlock:^(NSUInteger keyIndex, id object, BOOL *stop) {
[posts addObject:object];
}];
}
}];
return posts;
}
- (NSArray *)publishedPosts {
__block NSMutableArray *posts = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSArray *postPaths = [transaction objectForKey:@"published" inCollection:@"PostCollection"];
if (postPaths) {
[transaction enumerateObjectsForKeys:postPaths inCollection:@"Post" unorderedUsingBlock:^(NSUInteger keyIndex, id object, BOOL *stop) {
[posts addObject:object];
}];
}
}];
return posts;
}
- (Post *)postWithPath:(NSString *)path {
__block Post *post = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
post = [transaction objectForKey:path inCollection:@"Post"];
}];
return post;
}
- (PMKPromise *)saveBlogStatus:(BlogStatus *)blogStatus {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:blogStatus forKey:@"status" inCollection:@"BlogStatus"];
} completionBlock:^{
fulfill(blogStatus);
}];
}];
}
- (PMKPromise *)savePost:(Post *)post {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:post forKey:post.path inCollection:@"Post"];
} completionBlock:^{
fulfill(post);
}];
}];
}
- (PMKPromise *)saveDrafts:(NSArray *)posts {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [NSMutableArray array];
for (Post *post in posts) {
[transaction setObject:post forKey:post.path inCollection:@"Post"];
[postIDs addObject:post.objectID];
}
[transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"];
} completionBlock:^{
fulfill(posts);
}];
}];
}
- (PMKPromise *)savePublishedPosts:(NSArray *)posts {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [NSMutableArray array];
for (Post *post in posts) {
[transaction setObject:post forKey:post.path inCollection:@"Post"];
[postIDs addObject:post.objectID];
}
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"];
} completionBlock:^{
fulfill(posts);
}];
}];
}
- (PMKPromise *)addDraft:(Post *)post {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:post forKey:post.path inCollection:@"Post"];
NSMutableArray *postIDs = [[transaction objectForKey:@"drafts" inCollection:@"PostCollection"] mutableCopy];
if (![postIDs containsObject:post.path]) {
[postIDs addObject:post.path];
[transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"];
}
} completionBlock:^{
fulfill(post);
}];
}];
}
- (PMKPromise *)addPublishedPost:(Post *)post {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:post forKey:post.path inCollection:@"Post"];
NSMutableArray *postIDs = [[transaction objectForKey:@"published" inCollection:@"PostCollection"] mutableCopy];
if (![postIDs containsObject:post.path]) {
[postIDs addObject:post.path];
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"];
}
} completionBlock:^{
fulfill(post);
}];
}];
}
- (PMKPromise *)removeDraftWithPath:(NSString *)path {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [[transaction objectForKey:@"drafts" inCollection:@"PostCollection"] mutableCopy];
if ([postIDs containsObject:path]) {
[postIDs removeObject:path];
[transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"];
}
} completionBlock:^{
fulfill(path);
}];
}];
}
- (PMKPromise *)removePostWithPath:(NSString *)path {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [[transaction objectForKey:@"published" inCollection:@"PostCollection"] mutableCopy];
if ([postIDs containsObject:path]) {
[postIDs removeObject:path];
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"];
}
} completionBlock:^{
fulfill(path);
}];
}];
}
@end

View file

@ -0,0 +1,21 @@
//
// NSDate+marshmallows.h
// Marshmallows
//
// Created by Sami Samhuri on 11-06-18.
// Copyright 2011 Sam1 Samhuri. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSDate (Marshmallows)
@property (nonatomic, readonly) NSInteger mm_year;
@property (nonatomic, readonly) NSInteger mm_month;
@property (nonatomic, readonly) NSInteger mm_day;
+ (NSDate *)mm_dateWithYear:(NSInteger)year month:(NSInteger)month day:(NSInteger)day;
- (NSString *)mm_relativeToNow;
@end

129
Blog/NSDate+marshmallows.m Normal file
View file

@ -0,0 +1,129 @@
//
// NSDate+marshmallows.m
// Marshmallows
//
// Created by Sami Samhuri on 11-06-18.
// Copyright 2011 Sami Samhuri. All rights reserved.
//
#import "NSDate+marshmallows.h"
#define MINUTE 60.0
#define HOUR (60.0 * MINUTE)
#define DAY (24.0 * HOUR)
#define WEEK (7.0 * DAY)
#define MONTH (30.0 * DAY)
#define YEAR (365.25 * DAY)
@implementation NSDate (Marshmallows)
+ (NSDate *)mm_dateWithYear:(NSInteger)year month:(NSInteger)month day:(NSInteger)day {
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDateComponents *components = [NSDateComponents new];
[components setYear:year];
[components setMonth:month];
[components setDay:day];
return [calendar dateFromComponents:components];
}
- (NSString *)mm_relativeToNow {
double diff = [[NSDate date] timeIntervalSinceDate:self];
NSString *result = nil;
// future
if (diff < -2 * YEAR) {
result = [NSString stringWithFormat:@"in %d years", abs(diff / YEAR)];
}
else if (diff < -YEAR) {
result = @"next year";
}
else if (diff < -8 * WEEK) {
result = [NSString stringWithFormat:@"in %d months", abs(diff / MONTH)];
}
else if (diff < -4 * WEEK) {
result = @"next month";
}
else if (diff < -2 * WEEK) {
result = [NSString stringWithFormat:@"in %d weeks", abs(diff / WEEK)];
}
else if (diff < -WEEK) {
result = @"next week";
}
else if (diff < -2 * DAY) {
result = [NSString stringWithFormat:@"in %d days", abs(diff / DAY)];
}
else if (diff < -DAY) {
result = @"tomorrow";
}
else if (diff < -2 * HOUR) {
result = [NSString stringWithFormat:@"in %d hours", abs(diff / HOUR)];
}
else if (diff < -HOUR) {
result = @"in an hour";
}
else if (diff < -2 * MINUTE) {
result = [NSString stringWithFormat:@"in %d minutes", abs(diff / MINUTE)];
}
else if (diff < -MINUTE) {
result = @"in a minute";
}
// present
else if (diff < MINUTE) {
result = @"right now";
}
// past
else if (diff < 2 * MINUTE) {
result = @"a minute ago";
}
else if (diff < HOUR) {
result = [NSString stringWithFormat:@"%d minutes ago", (int)(diff / MINUTE)];
}
else if (diff < 2 * HOUR) {
result = @"an hour ago";
}
else if (diff < DAY) {
result = [NSString stringWithFormat:@"%d hours ago", (int)(diff / HOUR)];
}
else if (diff < 2 * DAY) {
result = @"yesterday";
}
else if (diff < WEEK) {
result = [NSString stringWithFormat:@"%d days ago", (int)(diff / DAY)];
}
else if (diff < 2 * WEEK) {
result = @"last week";
}
else if (diff < 4 * WEEK) {
result = [NSString stringWithFormat:@"%d weeks ago", (int)(diff / WEEK)];
}
else if (diff < 8 * WEEK) {
result = @"last month";
}
else if (diff < YEAR) {
result = [NSString stringWithFormat:@"%d months ago", (int)(diff / MONTH)];
}
else if (diff < 2 * YEAR) {
result = @"last year";
}
else {
result = [NSString stringWithFormat:@"%d years ago", (int)(diff / YEAR)];
}
return result;
}
- (NSInteger)mm_year {
return [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:self].year;
}
- (NSInteger)mm_month {
return [[NSCalendar currentCalendar] components:NSCalendarUnitMonth fromDate:self].month;
}
- (NSInteger)mm_day {
return [[NSCalendar currentCalendar] components:NSCalendarUnitDay fromDate:self].day;
}
@end

View file

@ -0,0 +1,20 @@
//
// NSString+marshmallows.h
// Marshmallows
//
// Created by Sami Samhuri on 11-09-03.
// Copyright 2011 Sami Samhuri. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSString (Marshmallows)
- (NSString *)mm_firstMatch:(NSString *)pattern;
- (NSString *)mm_stringByReplacing:(NSString *)pattern with:(NSString *)replacement;
- (NSString *)mm_stringByReplacingFirst:(NSString *)pattern with:(NSString *)replacement;
- (NSString *)mm_stringByTrimmingWhitespace;
- (NSString *)mm_stringByURLEncoding;
- (NSString *)mm_stringByURLDecoding;
@end

View file

@ -0,0 +1,66 @@
//
// NSString+marshmallows.m
// Marshmallows
//
// Created by Sami Samhuri on 11-09-03.
// Copyright 2011 Sami Samhuri. All rights reserved.
//
#import "NSString+marshmallows.h"
@implementation NSString (Marshmallows)
- (NSString *)mm_stringByTrimmingWhitespace {
return [self stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
- (NSString *)mm_firstMatch:(NSString *)pattern {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:0
error:NULL];
NSRange match = [regex rangeOfFirstMatchInString:self
options:NSMatchingReportCompletion
range:NSMakeRange(0, self.length)];
return match.location == NSNotFound ? nil : [self substringWithRange:match];
}
- (NSString *)mm_stringByReplacing:(NSString *)pattern with:(NSString *)replacement {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:0
error:NULL];
return [regex stringByReplacingMatchesInString:self
options:NSMatchingReportCompletion
range:NSMakeRange(0, [self length])
withTemplate:@""];
}
- (NSString *)mm_stringByReplacingFirst:(NSString *)pattern with:(NSString *)replacement {
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern
options:0
error:NULL];
NSRange match = [regex rangeOfFirstMatchInString:self
options:NSMatchingReportCompletion
range:NSMakeRange(0, self.length)];
if (match.location != NSNotFound) {
NSString *rest = [self substringFromIndex:match.location + match.length];
return [[[self substringToIndex:match.location]
stringByAppendingString:replacement]
stringByAppendingString:rest];
}
return [self copy];
}
- (NSString *) mm_stringByURLEncoding {
return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL,
(CFStringRef)self,
NULL,
(CFStringRef)@"!*'();:@&=+$,/?%#[]",
kCFStringEncodingUTF8));
}
- (NSString *)mm_stringByURLDecoding {
return [[self stringByReplacingOccurrencesOfString:@"+" withString:@" "]
stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
}
@end

32
Blog/Post.h Normal file
View file

@ -0,0 +1,32 @@
//
// Post.h
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <Mantle/Mantle.h>
@interface Post : MTLModel <MTLJSONSerializing>
@property (nonatomic, readonly, strong) NSString *objectID;
@property (nonatomic, readonly, strong) NSString *slug;
@property (nonatomic, readonly, strong) NSString *author;
@property (nonatomic, readonly, strong) NSString *title;
@property (nonatomic, readonly, strong) NSString *date;
@property (nonatomic, readonly, strong) NSDate *time;
@property (nonatomic, readonly) NSTimeInterval timestamp;
@property (nonatomic, readonly, strong) NSString *body;
@property (nonatomic, readonly, strong) NSString *path;
@property (nonatomic, readonly, strong) NSURL *url;
@property (nonatomic, readonly, getter=isDraft) BOOL draft;
@property (nonatomic, readonly, getter=isLink) BOOL link;
@property (nonatomic, readonly) NSString *formattedDate;
- (instancetype)copyWithBody:(NSString *)body;
- (instancetype)copyWithTitle:(NSString *)title;
- (instancetype)copyWithURL:(NSURL *)url;
- (BOOL)isEqualToPost:(Post *)other;
@end

158
Blog/Post.m Normal file
View file

@ -0,0 +1,158 @@
//
// Post.m
// Blog
//
// Created by Sami Samhuri on 2014-11-23.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import "Post.h"
#import "NSDate+marshmallows.h"
#import "NSString+marshmallows.h"
#import <Mantle/MTLValueTransformer.h>
@implementation Post
@synthesize objectID = _objectID;
@synthesize slug = _slug;
@synthesize author = _author;
@synthesize title = _title;
@synthesize date = _date;
@synthesize time = _time;
@synthesize body = _body;
@synthesize path = _path;
@synthesize url = _url;
@synthesize formattedDate = _formattedDate;
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{@"objectID": @"id",
@"path": @"url",
@"url": @"link",
@"time": @"", // ignore
};
}
+ (NSValueTransformer *)urlJSONTransformer {
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^NSURL *(NSString *str) {
return [NSURL URLWithString:str];
} reverseBlock:^NSString *(NSURL *url) {
return [url absoluteString];
}];
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
- (instancetype)copyWithBody:(NSString *)body {
return [[Post alloc] initWithDictionary:@{@"objectID": self.objectID ?: [NSNull null],
@"slug": self.slug ?: [NSNull null],
@"author": self.author ?: [NSNull null],
@"title": self.title ?: [NSNull null],
@"date": self.date ?: [NSNull null],
@"body": body ?: [NSNull null],
@"path": self.path ?: [NSNull null],
@"url": self.url ?: [NSNull null],
} error:nil];
}
- (instancetype)copyWithTitle:(NSString *)title {
return [[Post alloc] initWithDictionary:@{@"objectID": self.objectID ?: [NSNull null],
@"slug": self.slug ?: [NSNull null],
@"author": self.author ?: [NSNull null],
@"title": title ?: [NSNull null],
@"date": self.date ?: [NSNull null],
@"body": self.body ?: [NSNull null],
@"path": self.path ?: [NSNull null],
@"url": self.url ?: [NSNull null],
} error:nil];
}
- (instancetype)copyWithURL:(NSURL *)url {
return [[Post alloc] initWithDictionary:@{@"objectID": self.objectID ?: [NSNull null],
@"slug": self.slug ?: [NSNull null],
@"author": self.author ?: [NSNull null],
@"title": self.title ?: [NSNull null],
@"date": self.date ?: [NSNull null],
@"body": self.body ?: [NSNull null],
@"path": self.path ?: [NSNull null],
@"url": url ?: [NSNull null],
} error:nil];
}
- (BOOL)isEqualToPost:(Post *)other {
return [self.objectID isEqual:other.objectID];
}
- (BOOL)isEqual:(id)object {
return self == object || ([object isMemberOfClass:[Post class]] && [self isEqualToPost:object]);
}
- (NSUInteger)hash {
return [(self.objectID ?: self.slug) hash];
}
- (NSString *)objectID {
if (!_objectID && _draft) {
CFUUIDRef uuid = CFUUIDCreate(NULL);
CFStringRef uuidString = CFUUIDCreateString(NULL, uuid);
CFRelease(uuid);
_objectID = (__bridge NSString *)uuidString;
}
return _objectID;
}
- (NSString *)author {
if (!_author) {
_author = @"Sami Samhuri";
}
return _author;
}
- (NSString *)slug {
if (!_slug && !self.draft && self.title) {
_slug = [[[[[self.title lowercaseString]
mm_stringByReplacing:@"'" with:@""]
mm_stringByReplacing:@"[^[:alpha:]\\d_]" with:@"-"]
mm_stringByReplacing:@"^-+|-+$" with:@""]
mm_stringByReplacing:@"-+" with:@"-"];
}
return _slug;
}
- (BOOL)isLink {
return self.url != nil;
}
- (NSDate *)time {
if (!_time && self.timestamp) {
_time = [NSDate dateWithTimeIntervalSince1970:self.timestamp];
}
return _time;
}
- (NSString *)formattedDate {
if (!_formattedDate && self.time) {
_formattedDate = [NSString stringWithFormat:@"%ld-%02ld-%02ld", (long)self.time.mm_year, (long)self.time.mm_month, (long)self.time.mm_day];
}
return _formattedDate;
}
- (NSString *)path {
if (!_path && self.slug) {
if (self.draft) {
_path = [NSString stringWithFormat:@"/drafts/%@", self.slug];
}
else if (self.date) {
NSString *paddedMonth = [self paddedMonthForDate:self.date];
_path = [NSString stringWithFormat:@"/posts/%ld/%@/%@", (long)self.time.mm_year, paddedMonth, self.slug];
}
}
return _path;
}
- (NSString *)paddedMonthForDate:(NSDate *)date {
return [NSString stringWithFormat:@"%02ld", (long)date.mm_month];
}
@end

View file

@ -6,7 +6,7 @@
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@import UIKit;
#import "AppDelegate.h"
int main(int argc, char * argv[]) {

View file

@ -6,7 +6,7 @@
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <UIKit/UIKit.h>
@import UIKit;
#import <XCTest/XCTest.h>
@interface BlogTests : XCTestCase