improve the look, add refresh, and status in navbar

This commit is contained in:
Sami Samhuri 2015-04-21 19:32:19 -07:00
parent bd704e4241
commit e7b6f54935
14 changed files with 391 additions and 259 deletions

View file

@ -12,8 +12,8 @@
7B4070531AE46BC9000C2E43 /* auth.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B4070521AE46BC9000C2E43 /* auth.json */; };
7B5C4BDF19F2606900667D48 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BDE19F2606900667D48 /* main.m */; };
7B5C4BE219F2606900667D48 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE119F2606900667D48 /* AppDelegate.m */; };
7B5C4BE519F2606900667D48 /* MasterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE419F2606900667D48 /* MasterViewController.m */; };
7B5C4BE819F2606900667D48 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE719F2606900667D48 /* DetailViewController.m */; };
7B5C4BE519F2606900667D48 /* PostsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE419F2606900667D48 /* PostsViewController.m */; };
7B5C4BE819F2606900667D48 /* EditorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE719F2606900667D48 /* EditorViewController.m */; };
7B5C4BEB19F2606900667D48 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B5C4BE919F2606900667D48 /* Main.storyboard */; };
7B5C4BED19F2606900667D48 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7B5C4BEC19F2606900667D48 /* Images.xcassets */; };
7B5C4BF019F2606900667D48 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7B5C4BEE19F2606900667D48 /* LaunchScreen.xib */; };
@ -50,10 +50,10 @@
7B5C4BDE19F2606900667D48 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
7B5C4BE019F2606900667D48 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7B5C4BE119F2606900667D48 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
7B5C4BE319F2606900667D48 /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = "<group>"; };
7B5C4BE419F2606900667D48 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = "<group>"; };
7B5C4BE619F2606900667D48 /* DetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = "<group>"; };
7B5C4BE719F2606900667D48 /* DetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = "<group>"; };
7B5C4BE319F2606900667D48 /* PostsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PostsViewController.h; sourceTree = "<group>"; };
7B5C4BE419F2606900667D48 /* PostsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PostsViewController.m; sourceTree = "<group>"; };
7B5C4BE619F2606900667D48 /* EditorViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EditorViewController.h; sourceTree = "<group>"; };
7B5C4BE719F2606900667D48 /* EditorViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EditorViewController.m; sourceTree = "<group>"; };
7B5C4BEA19F2606900667D48 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
7B5C4BEC19F2606900667D48 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
7B5C4BEF19F2606900667D48 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
@ -144,10 +144,10 @@
7BF029351A27119B00E42EDE /* Service */,
7B5C4BE019F2606900667D48 /* AppDelegate.h */,
7B5C4BE119F2606900667D48 /* AppDelegate.m */,
7B5C4BE319F2606900667D48 /* MasterViewController.h */,
7B5C4BE419F2606900667D48 /* MasterViewController.m */,
7B5C4BE619F2606900667D48 /* DetailViewController.h */,
7B5C4BE719F2606900667D48 /* DetailViewController.m */,
7B5C4BE319F2606900667D48 /* PostsViewController.h */,
7B5C4BE419F2606900667D48 /* PostsViewController.m */,
7B5C4BE619F2606900667D48 /* EditorViewController.h */,
7B5C4BE719F2606900667D48 /* EditorViewController.m */,
7BE3A0331AE461E700E45CCB /* PreviewViewController.h */,
7BE3A0341AE461E700E45CCB /* PreviewViewController.m */,
7B5C4BE919F2606900667D48 /* Main.storyboard */,
@ -406,13 +406,13 @@
7B5C4BE219F2606900667D48 /* AppDelegate.m in Sources */,
7B9E644C1A230B940072FF42 /* JSONHTTPClient.m in Sources */,
7B9E642D1A227FA20072FF42 /* NSDate+marshmallows.m in Sources */,
7B5C4BE519F2606900667D48 /* MasterViewController.m in Sources */,
7B5C4BE519F2606900667D48 /* PostsViewController.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 */,
7B5C4BE819F2606900667D48 /* EditorViewController.m in Sources */,
7BE3A0351AE461E700E45CCB /* PreviewViewController.m in Sources */,
7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */,
1BCFCC637C63C780248D685E /* PostCell.m in Sources */,

View file

@ -7,8 +7,8 @@
//
#import "AppDelegate.h"
#import "MasterViewController.h"
#import "DetailViewController.h"
#import "PostsViewController.h"
#import "EditorViewController.h"
#import "BlogService.h"
#import "YapDatabase.h"
#import "ModelStore.h"
@ -30,11 +30,12 @@
return YES;
}
- (MasterViewController *)masterViewController {
- (PostsViewController *)postsViewController
{
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController = splitViewController.viewControllers.firstObject;
MasterViewController *masterViewController = (MasterViewController *)navigationController.viewControllers.firstObject;
return masterViewController;
PostsViewController *postsViewController = (PostsViewController *)navigationController.viewControllers.firstObject;
return postsViewController;
}
- (void)setupBlogController {
@ -43,7 +44,7 @@
ModelStore *store = [self newModelStoreWithPath:dbPath];
BlogController *blogController = [self newBlogControllerWithModelStore:store rootURL:@"http://ocean.samhuri.net:6706/"];
[self masterViewController].blogController = blogController;
[self postsViewController].blogController = blogController;
}
- (ModelStore *)newModelStoreWithPath:(NSString *)dbPath {
@ -104,7 +105,7 @@
#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] post] == nil)) {
if ([secondaryViewController isKindOfClass:[UINavigationController class]] && [[(UINavigationController *)secondaryViewController topViewController] isKindOfClass:[EditorViewController class]] && ([(EditorViewController *)[(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 {

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7531" systemVersion="14D136" 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="7702" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="H1p-Uh-vWS">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7520"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7701"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
</dependencies>
<scenes>
<!--Master-->
<!--Posts Navigation-->
<scene sceneID="pY4-Hu-kfo">
<objects>
<navigationController storyboardIdentifier="Master Nav Controller" title="Master" useStoryboardIdentifierAsRestorationIdentifier="YES" id="RMx-3f-FxP" sceneMemberID="viewController">
<navigationController storyboardIdentifier="Master Nav Controller" title="Posts Navigation" useStoryboardIdentifierAsRestorationIdentifier="YES" id="RMx-3f-FxP" sceneMemberID="viewController">
<simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" barStyle="black" translucent="NO" id="Pmd-2v-anx">
<autoresizingMask key="autoresizingMask"/>
@ -23,10 +23,10 @@
</objects>
<point key="canvasLocation" x="-38" y="-630"/>
</scene>
<!--Detail-->
<!--Editor-->
<scene sceneID="yUG-lL-AsK">
<objects>
<viewController storyboardIdentifier="Detail View Controller" title="Detail" useStoryboardIdentifierAsRestorationIdentifier="YES" id="JEX-9P-axG" customClass="DetailViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="Detail View Controller" title="Editor" useStoryboardIdentifierAsRestorationIdentifier="YES" id="JEX-9P-axG" customClass="EditorViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="SYR-Wa-9uf"/>
<viewControllerLayoutGuide type="bottom" id="GAO-Cl-Wes"/>
@ -46,8 +46,8 @@
</items>
<color key="barTintColor" red="0.1333333333" green="0.1333333333" blue="0.1333333333" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</toolbar>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" misplaced="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wrG-1y-ZY3">
<rect key="frame" x="0.0" y="64" width="600" height="492"/>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wrG-1y-ZY3">
<rect key="frame" x="0.0" y="0.0" width="600" height="492"/>
<color key="backgroundColor" red="0.1333333333" green="0.1333333333" blue="0.1333333333" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
@ -76,10 +76,10 @@
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="VlL-zs-EVE"/>
<exclude reference="8IO-mb-1kq"/>
<exclude reference="ORx-N8-ZLB"/>
<exclude reference="ecu-u5-SXU"/>
<exclude reference="VlL-zs-EVE"/>
<exclude reference="dwI-2s-ERv"/>
</mask>
</variation>
@ -101,7 +101,7 @@
<!--Preview-->
<scene sceneID="MEA-t1-FD6">
<objects>
<viewController storyboardIdentifier="Preview View Controller" useStoryboardIdentifierAsRestorationIdentifier="YES" id="ixd-IL-hNy" customClass="PreviewViewController" sceneMemberID="viewController">
<viewController storyboardIdentifier="Preview View Controller" title="Preview" useStoryboardIdentifierAsRestorationIdentifier="YES" id="ixd-IL-hNy" customClass="PreviewViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="VoQ-Bk-8rs"/>
<viewControllerLayoutGuide type="bottom" id="qQh-ht-rVd"/>
@ -153,10 +153,10 @@
</objects>
<point key="canvasLocation" x="-856" y="-330"/>
</scene>
<!--Master-->
<!--Posts-->
<scene sceneID="smW-Zh-WAh">
<objects>
<tableViewController storyboardIdentifier="Master View Controller" title="Master" useStoryboardIdentifierAsRestorationIdentifier="YES" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterViewController" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="Master View Controller" title="Posts" useStoryboardIdentifierAsRestorationIdentifier="YES" id="7bK-jq-Zjz" customClass="PostsViewController" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="r7i-6Z-zg0">
<rect key="frame" x="0.0" y="0.0" width="600" height="536"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -219,6 +219,12 @@
</connections>
</navigationItem>
<simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
<refreshControl key="refreshControl" opaque="NO" multipleTouchEnabled="YES" contentMode="center" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="ywL-K4-nCx">
<autoresizingMask key="autoresizingMask"/>
<connections>
<action selector="refresh:" destination="7bK-jq-Zjz" eventType="valueChanged" id="N7M-8B-qbe"/>
</connections>
</refreshControl>
<connections>
<outlet property="addButton" destination="u2a-vi-nHQ" id="BNL-ge-ZGw"/>
<outlet property="publishButton" destination="8HS-W8-a6l" id="amK-fb-yQq"/>
@ -238,10 +244,10 @@
</objects>
<point key="canvasLocation" x="709" y="-630"/>
</scene>
<!--Navigation Controller-->
<!--Editor Navigation-->
<scene sceneID="r7l-gg-dq7">
<objects>
<navigationController storyboardIdentifier="Detail Nav Controller" useStoryboardIdentifierAsRestorationIdentifier="YES" id="vC3-pB-5Vb" sceneMemberID="viewController">
<navigationController storyboardIdentifier="Detail Nav Controller" title="Editor Navigation" useStoryboardIdentifierAsRestorationIdentifier="YES" id="vC3-pB-5Vb" sceneMemberID="viewController">
<simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" barStyle="black" translucent="NO" id="DjV-YW-jjY">
<autoresizingMask key="autoresizingMask"/>
@ -258,6 +264,6 @@
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="Tll-UG-LXB"/>
<segue reference="6S0-TO-JiA"/>
</inferredMetricsTieBreakers>
</document>

View file

@ -28,10 +28,12 @@ extern NSString *BlogPostDeletedNotification;
- (NSMutableURLRequest *)previewRequestWithPath:(NSString *)path;
- (PMKPromise *)requestBlogStatus;
- (PMKPromise *)requestBlogStatusWithCaching:(BOOL)useCache;
- (PMKPromise *)requestDrafts;
- (PMKPromise *)requestPublishedPosts;
- (PMKPromise *)requestDraftsWithCaching:(BOOL)useCache;
- (PMKPromise *)requestPublishedPostsWithCaching:(BOOL)useCache;
- (PMKPromise *)requestAllPostsWithCaching:(BOOL)useCache;
- (PMKPromise *)requestPostWithPath:(NSString *)path;
- (PMKPromise *)requestCreateDraftWithID:(NSString *)draftID title:(NSString *)title body:(NSString *)body link:(NSString *)link;

View file

@ -42,8 +42,9 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
return request;
}
- (PMKPromise *)requestBlogStatus {
BlogStatus *status = [_store blogStatus];
- (PMKPromise *)requestBlogStatusWithCaching:(BOOL)useCache;
{
BlogStatus *status = useCache ? [_store blogStatus] : nil;
if (status) {
return [PMKPromise promiseWithValue:status];
}
@ -55,14 +56,13 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
}
}
- (PMKPromise *)requestDrafts {
NSArray *posts = [_store drafts];
- (PMKPromise *)requestDraftsWithCaching:(BOOL)useCache;
{
NSArray *posts = useCache ? [_store drafts] : nil;
if (posts) {
NSLog(@"returning %@ cached drafts", @(posts.count));
return [PMKPromise promiseWithValue:posts];
}
else {
NSLog(@"requesting drafts from server");
return [_service requestDrafts].then(^(NSArray *posts) {
[_store saveDrafts:posts];
return posts;
@ -70,8 +70,9 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
}
}
- (PMKPromise *)requestPublishedPosts {
NSArray *posts = [_store publishedPosts];
- (PMKPromise *)requestPublishedPostsWithCaching:(BOOL)useCache;
{
NSArray *posts = useCache ? [_store publishedPosts] : nil;
if (posts) {
return [PMKPromise promiseWithValue:posts];
}
@ -83,6 +84,13 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
}
}
- (PMKPromise *)requestAllPostsWithCaching:(BOOL)useCache;
{
return [PMKPromise when:@[[self requestDraftsWithCaching:useCache], [self requestPublishedPostsWithCaching:useCache]]].then(^(NSArray *results) {
return [results.firstObject arrayByAddingObjectsFromArray:results.lastObject];
});
}
- (PMKPromise *)requestPostWithPath:(NSString *)path {
Post *post = [_store postWithPath:path];
if (post) {

View file

@ -11,6 +11,7 @@
@interface BlogStatus : MTLModel <MTLJSONSerializing>
@property (nonatomic, readonly, strong) NSDate *date;
@property (nonatomic, readonly, strong) NSString *localVersion;
@property (nonatomic, readonly, strong) NSString *remoteVersion;
@property (nonatomic, readonly, getter=isDirty) BOOL dirty;

View file

@ -16,4 +16,14 @@
};
}
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
{
self = [super initWithDictionary:dictionaryValue error:error];
if (self)
{
_date = [NSDate date];
}
return self;
}
@end

View file

@ -1,5 +1,5 @@
//
// DetailViewController.h
// EditorViewController.h
// Blog
//
// Created by Sami Samhuri on 2014-10-18.
@ -11,7 +11,7 @@
@class BlogController;
@class Post;
@interface DetailViewController : UIViewController
@interface EditorViewController : UIViewController
@property (strong, nonatomic) BlogController *blogController;
@property (strong, nonatomic) Post *post;

View file

@ -1,5 +1,5 @@
//
// DetailViewController.m
// EditorViewController.m
// Blog
//
// Created by Sami Samhuri on 2014-10-18.
@ -7,19 +7,19 @@
//
#import <PromiseKit/Promise.h>
#import "DetailViewController.h"
#import "EditorViewController.h"
#import "BlogController.h"
#import "Post.h"
#import "PreviewViewController.h"
@interface DetailViewController () <UITextViewDelegate>
@interface EditorViewController () <UITextViewDelegate>
@property (nonatomic, weak) IBOutlet UITextView *textView;
@property (nonatomic, weak) IBOutlet UIToolbar *toolbar;
@end
@implementation DetailViewController
@implementation EditorViewController
#pragma mark - Managing the detail item

View file

@ -1,130 +0,0 @@
//
// MasterViewController.m
// Blog
//
// Created by Sami Samhuri on 2014-10-18.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <PromiseKit/Promise.h>
#import "MasterViewController.h"
#import "DetailViewController.h"
#import "Post.h"
#import "BlogController.h"
#import "PostCell.h"
@interface MasterViewController ()
@property (strong, nonatomic) NSMutableArray *posts;
@property (strong, nonatomic) DetailViewController *detailViewController;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton;
@end
@implementation MasterViewController
- (void)awakeFromNib {
[super awakeFromNib];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
self.clearsSelectionOnViewWillAppear = NO;
self.preferredContentSize = CGSizeMake(320.0, 600.0);
}
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UINavigationController *detailNavController = self.splitViewController.viewControllers.lastObject;
self.detailViewController = (DetailViewController *)detailNavController.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) {
self.posts = [drafts mutableCopy];
for (Post *post in [posts reverseObjectEnumerator]) {
[self.posts addObject:post];
}
[self.tableView reloadData];
});
});
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (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];
Post *post = self.posts[indexPath.row];
DetailViewController *controller = (DetailViewController *)[[segue destinationViewController] topViewController];
controller.blogController = self.blogController;
[controller setPost:post];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
#pragma mark - Table View
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.posts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PostCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
Post *post = self.posts[indexPath.row];
// FIXME: unique title
NSString *title = post.title ?: @"Untitled";
NSString *date = post.draft ? @"Draft" : post.formattedDate;
[cell configureWithTitle:title date:date];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.posts removeObjectAtIndex:indexPath.row];
// TODO: delete from server
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
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.
}
}
@end

View file

@ -24,12 +24,51 @@
- (BlogStatus *)blogStatus {
__block BlogStatus *status = nil;
__block NSDictionary *metadata = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
status = [transaction objectForKey:@"status" inCollection:@"BlogStatus"];
[transaction getObject:&status metadata:&metadata forKey:@"status" inCollection:@"BlogStatus"];
if (status && metadata)
{
NSNumber *timestamp = metadata[@"timestamp"];
NSTimeInterval age = [NSDate date].timeIntervalSince1970 - [timestamp unsignedIntegerValue];
if (age > 300)
{
NSLog(@"Blog status is stale (%@s old)", @(age));
status = nil;
}
}
}];
return status;
}
- (PMKPromise *)saveBlogStatus:(BlogStatus *)blogStatus {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:blogStatus forKey:@"status" inCollection:@"BlogStatus" withMetadata:@{@"timestamp": @([NSDate date].timeIntervalSince1970)}];
} completionBlock:^{
fulfill(blogStatus);
}];
}];
}
- (Post *)postWithPath:(NSString *)path {
__block Post *post = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
post = [transaction objectForKey:path inCollection:@"Post"];
}];
return post;
}
- (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);
}];
}];
}
- (NSArray *)drafts {
__block NSMutableArray *posts = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
@ -48,52 +87,6 @@
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) {
if (object) {
if (!posts) {
posts = [NSMutableArray new];
}
[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) {
@ -109,21 +102,6 @@
}];
}
- (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.path];
}
[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) {
@ -139,6 +117,53 @@
}];
}
- (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);
}];
}];
}
- (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) {
if (object) {
if (!posts) {
posts = [NSMutableArray new];
}
[posts addObject:object];
}
}];
}
}];
return 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.path];
}
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"];
} completionBlock:^{
fulfill(posts);
}];
}];
}
- (PMKPromise *)addPublishedPost:(Post *)post {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
@ -154,20 +179,6 @@
}];
}
- (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) {

View file

@ -53,6 +53,7 @@
@"body": body ?: [NSNull null],
@"path": self.path ?: [NSNull null],
@"url": self.url ?: [NSNull null],
@"draft": @(self.draft),
} error:nil];
}
@ -65,6 +66,7 @@
@"body": self.body ?: [NSNull null],
@"path": self.path ?: [NSNull null],
@"url": self.url ?: [NSNull null],
@"draft": @(self.draft),
} error:nil];
}
@ -77,6 +79,7 @@
@"body": self.body ?: [NSNull null],
@"path": self.path ?: [NSNull null],
@"url": url ?: [NSNull null],
@"draft": @(self.draft),
} error:nil];
}

View file

@ -1,5 +1,5 @@
//
// MasterViewController.h
// PostsViewController.h
// Blog
//
// Created by Sami Samhuri on 2014-10-18.
@ -10,7 +10,7 @@
@class BlogController;
@interface MasterViewController : UITableViewController
@interface PostsViewController : UITableViewController
@property (strong, nonatomic) BlogController *blogController;

220
Blog/PostsViewController.m Normal file
View file

@ -0,0 +1,220 @@
//
// PostsViewController.m
// Blog
//
// Created by Sami Samhuri on 2014-10-18.
// Copyright (c) 2014 Guru Logic Inc. All rights reserved.
//
#import <PromiseKit/Promise.h>
#import "PostsViewController.h"
#import "EditorViewController.h"
#import "Post.h"
#import "BlogController.h"
#import "PostCell.h"
#import "BlogStatus.h"
#import "NSDate+marshmallows.h"
@interface PostsViewController ()
@property (strong, nonatomic) NSMutableArray *posts;
@property (strong, nonatomic) EditorViewController *editorViewController;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton;
@property (weak, nonatomic) UILabel *titleLabel;
@property (weak, nonatomic) UILabel *statusLabel;
@property (copy, nonatomic) NSString *blogStatusText;
@property (strong, nonatomic) NSDate *blogStatusDate;
@property (strong, nonatomic) NSTimer *blogStatusTimer;
@end
@implementation PostsViewController
- (void)awakeFromNib {
[super awakeFromNib];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
self.clearsSelectionOnViewWillAppear = NO;
self.preferredContentSize = CGSizeMake(320.0, 600.0);
}
[self setupTitleView];
}
- (void)setupTitleView;
{
UIView *titleView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 44)];
titleView.userInteractionEnabled = YES;
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(requestStatusWithoutCaching)];
recognizer.numberOfTapsRequired = 2;
[titleView addGestureRecognizer:recognizer];
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
titleLabel.font = [UIFont boldSystemFontOfSize:16];
titleLabel.textColor = [UIColor whiteColor];
titleLabel.text = self.navigationItem.title;
[titleLabel sizeToFit];
titleLabel.center = CGPointMake(150, 3 + (CGRectGetHeight(titleLabel.bounds) / 2));
[titleView addSubview:titleLabel];
self.titleLabel = titleLabel;
UILabel *subtitleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
subtitleLabel.font = [UIFont systemFontOfSize:11];
subtitleLabel.textColor = [UIColor whiteColor];
[titleView addSubview:subtitleLabel];
self.statusLabel = subtitleLabel;
self.navigationItem.titleView = titleView;
}
- (void)setupBlogStatusTimer
{
self.blogStatusTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(updateBlogStatus) userInfo:nil repeats:YES];
}
- (void)teardownBlogStatusTimer
{
[self.blogStatusTimer invalidate];
self.blogStatusTimer = nil;
}
- (void)viewDidLoad {
[super viewDidLoad];
UINavigationController *detailNavController = self.splitViewController.viewControllers.lastObject;
self.editorViewController = (EditorViewController *)detailNavController.topViewController;
}
- (void)updateStatusLabel:(NSString *)blogStatus;
{
if (self.statusLabel && ![self.statusLabel.text isEqualToString:blogStatus]) {
self.statusLabel.text = blogStatus;
[self.statusLabel sizeToFit];
self.statusLabel.center = CGPointMake(150, CGRectGetMaxY(self.titleLabel.frame) + 3 + (CGRectGetHeight(self.statusLabel.bounds) / 2));
}
}
- (void)updateBlogStatus;
{
[self updateStatusLabel:[NSString stringWithFormat:@"%@ as of %@", self.blogStatusText, [self.blogStatusDate mm_relativeToNow]]];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupBlogStatusTimer];
[self requestStatusWithCaching:YES];
if (!self.posts) {
[self requestPostsWithCaching:YES];
}
}
- (void)viewWillDisappear:(BOOL)animated;
{
[super viewWillDisappear:animated];
[self teardownBlogStatusTimer];
}
- (IBAction)refresh:(id)sender {
[self requestStatusWithCaching:NO];
[self requestPostsWithCaching:NO].finally(^{
[self.refreshControl endRefreshing];
});
}
- (PMKPromise *)requestStatusWithoutCaching {
return [self requestStatusWithCaching:NO];
}
- (PMKPromise *)requestStatusWithCaching:(BOOL)useCache {
[self teardownBlogStatusTimer];
[self updateStatusLabel:@"Checking status"];
return [self.blogController requestBlogStatusWithCaching:useCache].then(^(BlogStatus *status) {
self.blogStatusDate = status.date;
if (status.dirty) {
self.blogStatusText = @"Dirty";
}
else {
self.blogStatusText = @"Everything published";
}
[self setupBlogStatusTimer];
[self updateBlogStatus];
return status;
}).catch(^(NSError *error) {
[self updateStatusLabel:@"Failed to check status"];
return error;
});
}
- (PMKPromise *)requestPostsWithCaching:(BOOL)useCache;
{
return [self.blogController requestAllPostsWithCaching:useCache].then(^(NSArray *posts) {
self.posts = [posts mutableCopy];
[self.tableView reloadData];
return posts;
});
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (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];
Post *post = self.posts[indexPath.row];
EditorViewController *controller = (EditorViewController *)[[segue destinationViewController] topViewController];
controller.blogController = self.blogController;
[controller setPost:post];
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES;
}
}
#pragma mark - Table View
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.posts.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PostCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
Post *post = self.posts[indexPath.row];
// FIXME: unique title
NSString *title = post.title ?: @"Untitled";
NSString *date = post.draft ? @"Draft" : post.formattedDate;
[cell configureWithTitle:title date:date];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.posts removeObjectAtIndex:indexPath.row];
// TODO: delete from server
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
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.
}
}
@end