mirror of
https://github.com/samsonjs/samhuri.net-ios.git
synced 2026-03-25 09:25:47 +00:00
get a list of posts
This commit is contained in:
parent
8b81a37ec8
commit
905c60d434
26 changed files with 1373 additions and 44 deletions
|
|
@ -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)";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
43
Blog/BlogController.h
Normal 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
138
Blog/BlogController.m
Normal 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
39
Blog/BlogService.h
Normal 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
125
Blog/BlogService.m
Normal 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
18
Blog/BlogStatus.h
Normal 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
19
Blog/BlogStatus.m
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -7,18 +7,21 @@
|
|||
//
|
||||
|
||||
#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
33
Blog/JSONHTTPClient.h
Normal 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
133
Blog/JSONHTTPClient.m
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
37
Blog/ModelStore.h
Normal 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
175
Blog/ModelStore.m
Normal 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
|
||||
21
Blog/NSDate+marshmallows.h
Normal file
21
Blog/NSDate+marshmallows.h
Normal 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
129
Blog/NSDate+marshmallows.m
Normal 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
|
||||
20
Blog/NSString+marshmallows.h
Normal file
20
Blog/NSString+marshmallows.h
Normal 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
|
||||
66
Blog/NSString+marshmallows.m
Normal file
66
Blog/NSString+marshmallows.m
Normal 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
32
Blog/Post.h
Normal 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
158
Blog/Post.m
Normal 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
|
||||
|
|
@ -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[]) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue