use a grouped table view for posts

This commit is contained in:
Sami Samhuri 2015-04-24 20:14:09 -07:00
parent 4cd295e18f
commit 78fee7d4b3
8 changed files with 251 additions and 110 deletions

View file

@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
1BCFCC637C63C780248D685E /* PostCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFCB5C36F301B2B93F8069 /* PostCell.m */; }; 1BCFCC637C63C780248D685E /* PostCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFCB5C36F301B2B93F8069 /* PostCell.m */; };
1BCFCD7E8EEBFAA97226B0BF /* UIColor+Hex.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFC23988387A5CAE551C90 /* UIColor+Hex.m */; }; 1BCFCD7E8EEBFAA97226B0BF /* UIColor+Hex.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFC23988387A5CAE551C90 /* UIColor+Hex.m */; };
1BCFCF6DB93786CC2EDB8F69 /* PostCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1BCFC3B62AA92DB07923F7C1 /* PostCollection.m */; };
30089596C2F733D451A454E8 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1613DC56A86AFA7E50460A37 /* libPods.a */; }; 30089596C2F733D451A454E8 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1613DC56A86AFA7E50460A37 /* libPods.a */; };
7B4070531AE46BC9000C2E43 /* auth.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B4070521AE46BC9000C2E43 /* auth.json */; }; 7B4070531AE46BC9000C2E43 /* auth.json in Resources */ = {isa = PBXBuildFile; fileRef = 7B4070521AE46BC9000C2E43 /* auth.json */; };
7B5C4BDF19F2606900667D48 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BDE19F2606900667D48 /* main.m */; }; 7B5C4BDF19F2606900667D48 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B5C4BDE19F2606900667D48 /* main.m */; };
@ -44,6 +45,8 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
1613DC56A86AFA7E50460A37 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; }; 1613DC56A86AFA7E50460A37 /* libPods.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPods.a; sourceTree = BUILT_PRODUCTS_DIR; };
1BCFC23988387A5CAE551C90 /* UIColor+Hex.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Hex.m"; sourceTree = "<group>"; }; 1BCFC23988387A5CAE551C90 /* UIColor+Hex.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIColor+Hex.m"; sourceTree = "<group>"; };
1BCFC3B62AA92DB07923F7C1 /* PostCollection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostCollection.m; sourceTree = "<group>"; };
1BCFCB18C8C8304D67E6462E /* PostCollection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostCollection.h; sourceTree = "<group>"; };
1BCFCB5C36F301B2B93F8069 /* PostCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostCell.m; sourceTree = "<group>"; }; 1BCFCB5C36F301B2B93F8069 /* PostCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PostCell.m; sourceTree = "<group>"; };
1BCFCCF30594E0E2DCC32116 /* PostCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostCell.h; sourceTree = "<group>"; }; 1BCFCCF30594E0E2DCC32116 /* PostCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PostCell.h; sourceTree = "<group>"; };
1BCFCFA1E7D4AFDA984693E1 /* UIColor+Hex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+Hex.h"; sourceTree = "<group>"; }; 1BCFCFA1E7D4AFDA984693E1 /* UIColor+Hex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIColor+Hex.h"; sourceTree = "<group>"; };
@ -159,6 +162,8 @@
7B5C4BDC19F2606900667D48 /* Supporting Files */, 7B5C4BDC19F2606900667D48 /* Supporting Files */,
1BCFCB5C36F301B2B93F8069 /* PostCell.m */, 1BCFCB5C36F301B2B93F8069 /* PostCell.m */,
1BCFCCF30594E0E2DCC32116 /* PostCell.h */, 1BCFCCF30594E0E2DCC32116 /* PostCell.h */,
1BCFC3B62AA92DB07923F7C1 /* PostCollection.m */,
1BCFCB18C8C8304D67E6462E /* PostCollection.h */,
); );
path = Blog; path = Blog;
sourceTree = "<group>"; sourceTree = "<group>";
@ -422,6 +427,7 @@
7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */, 7B9E644F1A23129B0072FF42 /* BlogStatus.m in Sources */,
1BCFCC637C63C780248D685E /* PostCell.m in Sources */, 1BCFCC637C63C780248D685E /* PostCell.m in Sources */,
1BCFCD7E8EEBFAA97226B0BF /* UIColor+Hex.m in Sources */, 1BCFCD7E8EEBFAA97226B0BF /* UIColor+Hex.m in Sources */,
1BCFCF6DB93786CC2EDB8F69 /* PostCollection.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View file

@ -87,14 +87,33 @@
<toolbarItems/> <toolbarItems/>
<navigationItem key="navigationItem" title="Article Title" id="mOI-FS-AaM"> <navigationItem key="navigationItem" title="Article Title" id="mOI-FS-AaM">
<barButtonItem key="backBarButtonItem" title=" " id="g7z-xe-9m6"/> <barButtonItem key="backBarButtonItem" title=" " id="g7z-xe-9m6"/>
<connections>
<outlet property="titleView" destination="udr-9h-BhX" id="t9J-lg-ow1"/>
</connections>
</navigationItem> </navigationItem>
<simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/> <simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/>
<connections> <connections>
<outlet property="textView" destination="wrG-1y-ZY3" id="lvo-lm-t7Z"/> <outlet property="textView" destination="wrG-1y-ZY3" id="lvo-lm-t7Z"/>
<outlet property="titleView" destination="udr-9h-BhX" id="fju-wx-M92"/>
<outlet property="toolbar" destination="YUD-Xe-6Cc" id="SIv-cT-WDD"/> <outlet property="toolbar" destination="YUD-Xe-6Cc" id="SIv-cT-WDD"/>
</connections> </connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="FJe-Yq-33r" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="FJe-Yq-33r" sceneMemberID="firstResponder"/>
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Article Title" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="udr-9h-BhX">
<rect key="frame" x="0.0" y="0.0" width="42" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="HelveticaNeue-Bold" family="Helvetica Neue" pointSize="18"/>
<color key="textColor" red="0.96862745100000003" green="0.96862745100000003" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
<connections>
<outletCollection property="gestureRecognizers" destination="Q2W-ie-6Yt" appends="YES" id="tLA-f1-v4Z"/>
</connections>
</label>
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="Q2W-ie-6Yt">
<connections>
<action selector="presentChangeTitle:" destination="JEX-9P-axG" id="WUd-1z-k2N"/>
</connections>
</pongPressGestureRecognizer>
</objects> </objects>
<point key="canvasLocation" x="709" y="129"/> <point key="canvasLocation" x="709" y="129"/>
</scene> </scene>
@ -157,10 +176,10 @@
<scene sceneID="smW-Zh-WAh"> <scene sceneID="smW-Zh-WAh">
<objects> <objects>
<tableViewController storyboardIdentifier="Master View Controller" title="Posts" useStoryboardIdentifierAsRestorationIdentifier="YES" id="7bK-jq-Zjz" customClass="PostsViewController" 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="singleLineEtched" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="r7i-6Z-zg0"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="singleLineEtched" rowHeight="44" sectionHeaderHeight="10" sectionFooterHeight="10" id="r7i-6Z-zg0">
<rect key="frame" x="0.0" y="0.0" width="600" height="536"/> <rect key="frame" x="0.0" y="0.0" width="600" height="536"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/> <color key="backgroundColor" red="0.66666666669999997" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="0.7953414352" green="0.0" blue="0.013255690590000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="tintColor" red="0.7953414352" green="0.0" blue="0.013255690590000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<prototypes> <prototypes>
<tableViewCell contentMode="scaleToFill" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" id="WCw-Qf-5nD" customClass="PostCell"> <tableViewCell contentMode="scaleToFill" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="Cell" id="WCw-Qf-5nD" customClass="PostCell">

View file

@ -81,10 +81,7 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
} }
- (PMKPromise *)requestAllPostsWithCaching:(BOOL)useCache { - (PMKPromise *)requestAllPostsWithCaching:(BOOL)useCache {
return [PMKPromise when:@[[self requestDraftsWithCaching:useCache], [self requestPublishedPostsWithCaching:useCache]]] return [PMKPromise when:@[[self requestDraftsWithCaching:useCache], [self requestPublishedPostsWithCaching:useCache]]];
.then(^(NSArray *results) {
return [results.firstObject arrayByAddingObjectsFromArray:results.lastObject];
});
} }
- (PMKPromise *)requestPostWithPath:(NSString *)path { - (PMKPromise *)requestPostWithPath:(NSString *)path {
@ -110,7 +107,7 @@ NSString *BlogPostDeletedNotification = @"BlogPostDeletedNotification";
- (PMKPromise *)requestUpdatePost:(Post *)post { - (PMKPromise *)requestUpdatePost:(Post *)post {
return [_service requestUpdatePostWithPath:post.path title:post.title body:post.body link:post.url.absoluteString] return [_service requestUpdatePostWithPath:post.path title:post.title body:post.body link:post.url.absoluteString]
.then(^(Post *post) { .then(^{
[_store savePost:post]; [_store savePost:post];
return post; return post;
}); });

View file

@ -14,8 +14,11 @@
@interface EditorViewController () <UITextViewDelegate> @interface EditorViewController () <UITextViewDelegate>
@property (nonatomic, weak) IBOutlet UILabel *titleView;
@property (nonatomic, weak) IBOutlet UITextView *textView; @property (nonatomic, weak) IBOutlet UITextView *textView;
@property (nonatomic, weak) IBOutlet UIToolbar *toolbar; @property (nonatomic, weak) IBOutlet UIToolbar *toolbar;
@property (strong, nonatomic) Post *modifiedPost;
@property (strong, nonatomic) PMKPromise *savePromise;
@end @end
@ -26,30 +29,36 @@
- (void)setPost:(id)newPost { - (void)setPost:(id)newPost {
if (_post != newPost) { if (_post != newPost) {
_post = newPost; _post = newPost;
self.modifiedPost = newPost;
[self configureView]; [self configureView];
} }
} }
- (void)configureView { - (void)configureView {
NSString *title = nil; NSString *title = nil;
NSString *text = nil; NSString *body = nil;
CGPoint scrollOffset = CGPointZero; CGPoint scrollOffset = CGPointZero;
if (self.post) { Post *post = self.modifiedPost;
if (post) {
// FIXME: date, status (draft, published) // FIXME: date, status (draft, published)
title = self.post.title.length ? self.post.title : @"Untitled"; body = post.body;
text = self.post.body;
// TODO: restore scroll offset for this post ... user defaults? // TODO: restore scroll offset for this post ... user defaults?
} }
self.navigationItem.title = title; [self configureTitleView];
self.textView.text = text; self.textView.text = body;
self.textView.contentOffset = scrollOffset; self.textView.contentOffset = scrollOffset;
// TODO: url
BOOL toolbarEnabled = self.post != nil; BOOL toolbarEnabled = post != nil;
[self.toolbar.items enumerateObjectsUsingBlock:^(UIBarButtonItem *item, NSUInteger idx, BOOL *stop) { [self.toolbar.items enumerateObjectsUsingBlock:^(UIBarButtonItem *item, NSUInteger idx, BOOL *stop) {
item.enabled = toolbarEnabled; item.enabled = toolbarEnabled;
}]; }];
} }
- (void)configureTitleView {
self.titleView.text = self.modifiedPost.title.length ? self.modifiedPost.title : @"Untitled";
}
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
[self configureView]; [self configureView];
@ -57,63 +66,87 @@
- (void)viewWillAppear:(BOOL)animated { - (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated]; [super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(savePostBody) name:UIApplicationWillResignActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(savePost) name:UIApplicationWillResignActiveNotification object:nil];
} }
- (void)viewWillDisappear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated]; [super viewWillDisappear:animated];
[self savePostBody]; [self savePost];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
} }
- (PMKPromise *)savePostBody {
NSString *body = self.textView.text;
if (!self.post || !body.length) {
return [PMKPromise promiseWithValue:nil];
}
Post *newPost = [self.post copyWithBody:body];
if ([newPost isEqual:self.post]) {
return [PMKPromise promiseWithValue:self.post];
}
self.post = newPost;
NSString *path = self.post.path;
PMKPromise *savePromise;
NSString *verb;
if (self.post.new) {
verb = @"create";
savePromise = [self.blogController requestCreateDraft:self.post];
}
else {
verb = @"update";
savePromise = [self.blogController requestUpdatePost:self.post];
}
return savePromise.then(^(Post *post) {
NSLog(@"%@ post at path %@", verb, path);
// TODO: something better than this
// update our post because "new" may have changed, which is essential to correct operation
self.post = post;
[self configureView];
if (self.postUpdatedBlock) {
self.postUpdatedBlock(self.post);
}
return post;
}).catch(^(NSError *error) {
NSLog(@"Falied to %@ post at path %@: %@ %@", verb, path, error.localizedDescription, error.userInfo);
return error;
});
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
[super prepareForSegue:segue sender:sender]; [super prepareForSegue:segue sender:sender];
if ([segue.identifier isEqualToString:@"showPreview"]) { if ([segue.identifier isEqualToString:@"showPreview"]) {
PreviewViewController *previewViewController = segue.destinationViewController; PreviewViewController *previewViewController = segue.destinationViewController;
previewViewController.promise = [self savePostBody]; previewViewController.promise = [self savePost];
previewViewController.initialRequest = [self.blogController previewRequestWithPath:self.post.path]; previewViewController.initialRequest = [self.blogController previewRequestWithPath:self.modifiedPost.path];
return;
} }
} }
- (PMKPromise *)savePost {
if (self.savePromise) {
return self.savePromise;
}
// TODO: persist on disk before going to the network
NSAssert(self.post, @"post is required");
[self updatePostBody];
if (!self.post.new && [self.modifiedPost isEqualToPost:self.post]) {
return [PMKPromise promiseWithValue:self.post];
}
Post *newPost = self.modifiedPost;
NSString *path = newPost.path;
PMKPromise *savePromise;
NSString *verb;
if (newPost.new) {
verb = @"create";
savePromise = [self.blogController requestCreateDraft:newPost];
}
else {
verb = @"update";
savePromise = [self.blogController requestUpdatePost:newPost];
}
self.savePromise = savePromise;
return savePromise.then(^{
NSLog(@"%@ post at path %@", verb, path);
// TODO: something better than this
// update our post because "new" may have changed, which is essential to correct operation
if ([self.modifiedPost isEqualToPost:newPost]) {
self.post = newPost;
}
else {
Post *modified = self.modifiedPost;
self.post = newPost;
self.modifiedPost = modified;
[self configureView];
}
if (self.postUpdatedBlock) {
self.postUpdatedBlock(self.post);
}
return newPost;
}).catch(^(NSError *error) {
NSLog(@"Failed to %@ post at path %@: %@ %@", verb, path, error.localizedDescription, error.userInfo);
return error;
}).finally(^{
self.savePromise = nil;
});
}
- (void)updatePostBody {
self.modifiedPost = [self.modifiedPost copyWithBody:self.textView.text];
}
- (void)updatePostTitle:(NSString *)title {
self.modifiedPost = [self.modifiedPost copyWithTitle:title];
[self configureTitleView];
}
- (void)updatePostURL:(NSURL *)url {
self.modifiedPost = [self.modifiedPost copyWithURL:url];
}
@end @end

View file

@ -11,6 +11,7 @@
#import <YapDatabase/YapDatabase.h> #import <YapDatabase/YapDatabase.h>
#import "BlogStatus.h" #import "BlogStatus.h"
#import "Post.h" #import "Post.h"
#import "NSArray+ObjectiveSugar.h"
@implementation ModelStore { @implementation ModelStore {
YapDatabaseConnection *_connection; YapDatabaseConnection *_connection;
@ -88,12 +89,12 @@
- (PMKPromise *)saveDrafts:(NSArray *)posts { - (PMKPromise *)saveDrafts:(NSArray *)posts {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [NSMutableArray array]; NSMutableArray *postPaths = [NSMutableArray array];
for (Post *post in posts) { for (Post *post in posts) {
[transaction setObject:post forKey:post.path inCollection:@"Post"]; [transaction setObject:post forKey:post.path inCollection:@"Post"];
[postIDs addObject:post.path]; [postPaths addObject:post.path];
} }
[transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"]; [transaction setObject:postPaths forKey:@"drafts" inCollection:@"PostCollection"];
} completionBlock:^{ } completionBlock:^{
fulfill(posts); fulfill(posts);
}]; }];
@ -104,10 +105,10 @@
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:post forKey:post.path inCollection:@"Post"]; [transaction setObject:post forKey:post.path inCollection:@"Post"];
NSMutableArray *postIDs = [[transaction objectForKey:@"drafts" inCollection:@"PostCollection"] mutableCopy]; NSMutableArray *postPaths = [[transaction objectForKey:@"drafts" inCollection:@"PostCollection"] mutableCopy];
if (![postIDs containsObject:post.path]) { if (![postPaths containsObject:post.path]) {
[postIDs addObject:post.path]; [postPaths addObject:post.path];
[transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"]; [transaction setObject:postPaths forKey:@"drafts" inCollection:@"PostCollection"];
} }
} completionBlock:^{ } completionBlock:^{
fulfill(post); fulfill(post);
@ -118,10 +119,10 @@
- (PMKPromise *)removeDraftWithPath:(NSString *)path { - (PMKPromise *)removeDraftWithPath:(NSString *)path {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [[transaction objectForKey:@"drafts" inCollection:@"PostCollection"] mutableCopy]; NSMutableArray *postPaths = [[transaction objectForKey:@"drafts" inCollection:@"PostCollection"] mutableCopy];
if ([postIDs containsObject:path]) { if ([postPaths containsObject:path]) {
[postIDs removeObject:path]; [postPaths removeObject:path];
[transaction setObject:postIDs forKey:@"drafts" inCollection:@"PostCollection"]; [transaction setObject:postPaths forKey:@"drafts" inCollection:@"PostCollection"];
} }
} completionBlock:^{ } completionBlock:^{
fulfill(path); fulfill(path);
@ -130,32 +131,33 @@
} }
- (NSArray *)publishedPosts { - (NSArray *)publishedPosts {
__block NSMutableArray *posts = nil; __block NSArray *posts = nil;
[_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) { [_connection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
NSArray *postPaths = [transaction objectForKey:@"published" inCollection:@"PostCollection"]; NSArray *postPaths = [transaction objectForKey:@"published" inCollection:@"PostCollection"];
NSMutableDictionary *postsByPath = [NSMutableDictionary dictionaryWithCapacity:postPaths.count];
if (postPaths) { if (postPaths) {
[transaction enumerateObjectsForKeys:postPaths inCollection:@"Post" unorderedUsingBlock:^(NSUInteger keyIndex, id object, BOOL *stop) { [transaction enumerateObjectsForKeys:postPaths inCollection:@"Post" unorderedUsingBlock:^(NSUInteger keyIndex, Post *post, BOOL *stop) {
if (object) { if (post) {
if (!posts) { postsByPath[post.path] = post;
posts = [NSMutableArray new];
}
[posts addObject:object];
} }
}]; }];
posts = [postPaths map:^id(NSString *path) {
return postsByPath[path];
}];
} }
}]; }];
return posts; return posts.count ? posts : nil;
} }
- (PMKPromise *)savePublishedPosts:(NSArray *)posts { - (PMKPromise *)savePublishedPosts:(NSArray *)posts {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [NSMutableArray array]; NSMutableArray *postPaths = [NSMutableArray array];
for (Post *post in posts) { for (Post *post in posts) {
[transaction setObject:post forKey:post.path inCollection:@"Post"]; [transaction setObject:post forKey:post.path inCollection:@"Post"];
[postIDs addObject:post.path]; [postPaths addObject:post.path];
} }
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"]; [transaction setObject:postPaths forKey:@"published" inCollection:@"PostCollection"];
} completionBlock:^{ } completionBlock:^{
fulfill(posts); fulfill(posts);
}]; }];
@ -166,10 +168,10 @@
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
[transaction setObject:post forKey:post.path inCollection:@"Post"]; [transaction setObject:post forKey:post.path inCollection:@"Post"];
NSMutableArray *postIDs = [[transaction objectForKey:@"published" inCollection:@"PostCollection"] mutableCopy]; NSMutableArray *postPaths = [[transaction objectForKey:@"published" inCollection:@"PostCollection"] mutableCopy];
if (![postIDs containsObject:post.path]) { if (![postPaths containsObject:post.path]) {
[postIDs addObject:post.path]; [postPaths addObject:post.path];
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"]; [transaction setObject:postPaths forKey:@"published" inCollection:@"PostCollection"];
} }
} completionBlock:^{ } completionBlock:^{
fulfill(post); fulfill(post);
@ -180,10 +182,10 @@
- (PMKPromise *)removePostWithPath:(NSString *)path { - (PMKPromise *)removePostWithPath:(NSString *)path {
return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { return [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
[_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { [_connection asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
NSMutableArray *postIDs = [[transaction objectForKey:@"published" inCollection:@"PostCollection"] mutableCopy]; NSMutableArray *postPaths = [[transaction objectForKey:@"published" inCollection:@"PostCollection"] mutableCopy];
if ([postIDs containsObject:path]) { if ([postPaths containsObject:path]) {
[postIDs removeObject:path]; [postPaths removeObject:path];
[transaction setObject:postIDs forKey:@"published" inCollection:@"PostCollection"]; [transaction setObject:postPaths forKey:@"published" inCollection:@"PostCollection"];
} }
} completionBlock:^{ } completionBlock:^{
fulfill(path); fulfill(path);

15
Blog/PostCollection.h Normal file
View file

@ -0,0 +1,15 @@
//
// Created by Sami Samhuri on 15-04-24.
// Copyright (c) 2015 Guru Logic Inc. All rights reserved.
//
@import Foundation;
@interface PostCollection : NSObject
@property (nonatomic, readonly, copy) NSString *title;
@property (nonatomic, readonly, copy) NSMutableArray *posts;
+ (instancetype)postCollectionWithTitle:(NSString *)title posts:(NSArray *)posts;
- (instancetype)initWithTitle:(NSString *)title posts:(NSArray *)posts;
@end

22
Blog/PostCollection.m Normal file
View file

@ -0,0 +1,22 @@
//
// Created by Sami Samhuri on 15-04-24.
// Copyright (c) 2015 Guru Logic Inc. All rights reserved.
//
#import "PostCollection.h"
@implementation PostCollection
+ (instancetype)postCollectionWithTitle:(NSString *)title posts:(NSMutableArray *)posts {
return [[self alloc] initWithTitle:title posts:posts];
}
- (instancetype)initWithTitle:(NSString *)title posts:(NSMutableArray *)posts {
self = [super init];
if (self) {
_title = [title copy];
_posts = [posts mutableCopy];
}
return self;
}
@end

View file

@ -14,10 +14,14 @@
#import "PostCell.h" #import "PostCell.h"
#import "BlogStatus.h" #import "BlogStatus.h"
#import "NSDate+marshmallows.h" #import "NSDate+marshmallows.h"
#import "UIColor+Hex.h"
#import "PostCollection.h"
@interface PostsViewController () @interface PostsViewController ()
@property (strong, nonatomic) NSMutableArray *posts; @property (strong, nonatomic) NSArray *postCollections;
@property (strong, readonly, nonatomic) NSMutableArray *drafts;
@property (strong, readonly, nonatomic) NSMutableArray *posts;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton; @property (strong, nonatomic) IBOutlet UIBarButtonItem *publishButton;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton; @property (strong, nonatomic) IBOutlet UIBarButtonItem *addButton;
@property (weak, nonatomic) UILabel *titleLabel; @property (weak, nonatomic) UILabel *titleLabel;
@ -28,8 +32,13 @@
@end @end
static const NSUInteger SectionDrafts = 0;
static const NSUInteger SectionPublished = 1;
@implementation PostsViewController @implementation PostsViewController
@dynamic drafts, posts;
- (void)awakeFromNib { - (void)awakeFromNib {
[super awakeFromNib]; [super awakeFromNib];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
@ -103,7 +112,7 @@
[super viewWillAppear:animated]; [super viewWillAppear:animated];
[self setupBlogStatusTimer]; [self setupBlogStatusTimer];
[self requestStatusWithCaching:YES]; [self requestStatusWithCaching:YES];
if (!self.posts) { if (!self.postCollections) {
[self requestPostsWithCaching:YES]; [self requestPostsWithCaching:YES];
} }
} }
@ -145,13 +154,32 @@
} }
- (PMKPromise *)requestPostsWithCaching:(BOOL)useCache { - (PMKPromise *)requestPostsWithCaching:(BOOL)useCache {
return [self.blogController requestAllPostsWithCaching:useCache].then(^(NSArray *posts) { return [self.blogController requestAllPostsWithCaching:useCache].then(^(NSArray *results) {
self.posts = [posts mutableCopy]; self.postCollections = @[
[PostCollection postCollectionWithTitle:@"Drafts" posts:results.firstObject],
[PostCollection postCollectionWithTitle:@"Published" posts:results.lastObject],
];
[self.tableView reloadData]; [self.tableView reloadData];
return posts; return results;
}); });
} }
- (PostCollection *)postCollectionForSection:(NSInteger)section {
return self.postCollections[section];
}
- (Post *)postForIndexPath:(NSIndexPath *)indexPath {
return [self postCollectionForSection:indexPath.section].posts[indexPath.row];
}
- (NSMutableArray *)drafts {
return [self postCollectionForSection:SectionDrafts].posts;
}
- (NSMutableArray *)posts {
return [self postCollectionForSection:SectionPublished].posts;
}
- (void)didReceiveMemoryWarning { - (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning]; [super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated. // Dispose of any resources that can be recreated.
@ -162,8 +190,8 @@
NSURL *url = [UIPasteboard generalPasteboard].URL; NSURL *url = [UIPasteboard generalPasteboard].URL;
// TODO: image, anything else interesting // TODO: image, anything else interesting
Post *post = [Post newDraftWithTitle:title body:nil url:url]; Post *post = [Post newDraftWithTitle:title body:nil url:url];
[self.posts insertObject:post atIndex:0]; [self.drafts insertObject:post atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0]; NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:SectionDrafts];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop]; [self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
[self performSegueWithIdentifier:@"showDetail" sender:sender]; [self performSegueWithIdentifier:@"showDetail" sender:sender];
@ -178,19 +206,26 @@
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"showDetail"]) { if ([segue.identifier isEqualToString:@"showDetail"]) {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow]; NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
Post *post = self.posts[indexPath.row]; Post *post = [self postForIndexPath:indexPath];
EditorViewController *controller = (EditorViewController *)[[segue destinationViewController] topViewController]; EditorViewController *controller = (EditorViewController *)[[segue destinationViewController] topViewController];
controller.blogController = self.blogController; controller.blogController = self.blogController;
[controller setPost:post]; controller.post = post;
controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem; controller.navigationItem.leftBarButtonItem = self.splitViewController.displayModeButtonItem;
controller.navigationItem.leftItemsSupplementBackButton = YES; controller.navigationItem.leftItemsSupplementBackButton = YES;
controller.postUpdatedBlock = ^(Post *post) { controller.postUpdatedBlock = ^(Post *post) {
NSUInteger row = [self.posts indexOfObjectPassingTest:^BOOL(Post *p, NSUInteger idx, BOOL *stop) { BOOL (^isThisPost)(Post *, NSUInteger, BOOL *) = ^BOOL(Post *p, NSUInteger idx, BOOL *stop) {
return [p.objectID isEqualToString:post.objectID]; return [p.objectID isEqualToString:post.objectID];
}]; };
NSUInteger section = SectionDrafts;
NSUInteger row = [self.drafts indexOfObjectPassingTest:isThisPost];
if (row == NSNotFound) {
section = SectionPublished;
row = [self.posts indexOfObjectPassingTest:isThisPost];
}
if (row != NSNotFound) { if (row != NSNotFound) {
[self.posts replaceObjectAtIndex:row withObject:post]; PostCollection *collection = [self postCollectionForSection:section];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; [collection.posts replaceObjectAtIndex:row withObject:post];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
} }
}; };
@ -200,20 +235,31 @@
#pragma mark - Table View #pragma mark - Table View
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1; return self.postCollections.count;
} }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.posts.count; return [self postCollectionForSection:section].posts.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [self postCollectionForSection:section].title;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return [super tableView:tableView viewForHeaderInSection:section];
}
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section {
UITableViewHeaderFooterView *headerView = [view isKindOfClass:[UITableViewHeaderFooterView class]] ? (UITableViewHeaderFooterView *)view : nil;
headerView.textLabel.textColor = [UIColor mm_colorFromInteger:0xF7F7F7];
} }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
PostCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; PostCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
Post *post = [self postForIndexPath:indexPath];
Post *post = self.posts[indexPath.row];
// FIXME: unique title
NSString *title = post.title.length ? post.title : @"Untitled"; NSString *title = post.title.length ? post.title : @"Untitled";
NSString *date = post.draft ? @"Draft" : post.formattedDate; NSString *date = post.draft ? @"" : post.formattedDate;
[cell configureWithTitle:title date:date]; [cell configureWithTitle:title date:date];
return cell; return cell;
} }
@ -225,7 +271,8 @@
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) { if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.posts removeObjectAtIndex:indexPath.row]; PostCollection *collection = [self postCollectionForSection:indexPath.section];
[collection.posts removeObjectAtIndex:indexPath.row];
// TODO: delete from server // TODO: delete from server
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
} }