新主题

 • 

Current Theme
更新了主题,Daring,仿 Daring Fireball。这算是我目前为止找到的最满意的一个 Ghost 主题了,也很类似于 www.livid.cn 的风格。单页,所有的文章一次性铺开,除非需要评论不需要再点进单个文章的页面,非常好用,总算是把我一直以来觉得博客文章需要点击查看全文非常累赘的问题给解决了。
同时给这个主题添加了代码高亮和脚注支持,博客系统也升级到了 Ghost 0.8.0,评论系统也换用了 Disqus。
1

Banner 是仿照 Design Week Portland 做的。如果您想看原本是什么样子,可以去 Eli Schiff 的个人简介页面查看绿色的 Design Week Portland 2014 Banner。
How do you guys feel about it?

  1. 有关 Disqus 的 URL 参数问题以及多说迁移 Disqus 可以参见 http://www.justzht.com/disqusguan-yu-shi-bie-wei-xin-fen-xiang-can-shu-de-jie-jue/

2016.7.14

 • 

现在是周四凌晨两点五十四分,我刚写完这个学期最后一篇实验报告,然后十小时后要动身去北京。
在北京住哪还没确定,只能等到北京找自如看房了。如果一切顺利,应该能办完手续然后周五去公司,Cee 应该也在那个时候入职了,挺好的。
电工实验周 (7.1 - 7.14)中间我一直在写 Cetacea 的 Mac 版本。写的挺顺利,而且因为没有用 NSDocument 而是自己写的一个 Data Model 来做存储,Cetacea 的 iCloud Drive 可以在 iOS 和 macOS 版之间共享一套文件,比较省事。至于 Dropbox,沙盒模式下如何访问到文件还是个问题,所以目前我就只限于用 iCloud Drive 做同步了。


题外话:
实验周中间,广电的手游审批公告公布,并且 iTunes Connect 也同步加上了所谓的“请输入批准号码和日期”。苹果的这个同步的做法我也不知道说什么好,反讽的是截止到目前,iOS App Store 的首页推荐还有一栏叫做”精品中文独立游戏“。
呵呵。
以后不会再有中文精品 独立 游戏了。

NSOutlineView 实现文件管理器层级视图

 • 

效果图

Data Model

JZiCloudFileSystemItem.h

@interface JZiCloudFileSystemItem : NSObject

@property (nonatomic,strong) NSString *relativePath;
@property (nonatomic,weak) JZiCloudFileSystemItem *parent;
@property (nonatomic,strong) NSMutableArray *children;
@property (nonatomic,strong) NSMutableArray *childrenFolderOnly;
@property (nonatomic,strong) NSMutableArray *childrenMDOnly;

+ (JZiCloudFileSystemItem *)rootItem;

- (NSInteger)numberOfChildren;// Returns -1 for leaf nodes
- (JZiCloudFileSystemItem *)childAtIndex:(NSUInteger)n; // Invalid to call on leaf nodes

// 当前 JZiCloudFileSystemItem 文件夹内的 Markdown 数量
- (NSInteger)numberOfChildrenMD;
// 当前 JZiCloudFileSystemItem 文件夹内的 文件夹 数量
- (NSInteger)numberOfChildrenFolder;// Returns -1 for leaf nodes
- (JZiCloudFileSystemItem *)childFolderAtIndex:(NSUInteger)n; // Invalid to call on leaf nodes

- (NSString *)fullPath;
- (NSString *)relativePath;
- (void)refresh;

@end

JZiCloudFileSystemItem.m

@implementation JZiCloudFileSystemItem
@synthesize relativePath,parent,children,childrenFolderOnly,childrenMDOnly;

static JZiCloudFileSystemItem *rootItem = nil;
static NSMutableArray *leafNode = nil;

+ (void)initialize {
    if (self == [JZiCloudFileSystemItem class])
    {
        leafNode = [[NSMutableArray alloc] init];
    }
}

- (id)initWithPath:(NSString *)path parent:(JZiCloudFileSystemItem *)parentItem {
    self = [super init];
    if (self) {
        relativePath = [[path lastPathComponent] copy];
        parent = parentItem;
    }
    return self;
}
- (void)refresh
{
    rootItem = nil;
}

+ (JZiCloudFileSystemItem *)rootItem {
    if (rootItem == nil)
    {
        NSString *icloudRoot = [[[JZiCloudStorageManager sharedManager] ubiquitousDocumentsURL] path];
        rootItem = [[JZiCloudFileSystemItem alloc] initWithPath:icloudRoot parent:nil];
    }
    return rootItem;
}


// Creates, caches, and returns the array of children
// Loads children incrementally
- (NSArray *)children {
    
    if (children == nil)
    {
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSString *fullPath = [self fullPath];
        BOOL isDir, valid;
        
        valid = [fileManager fileExistsAtPath:fullPath isDirectory:&isDir];
        
        if (valid && isDir) {
            NSArray *array = [fileManager contentsOfDirectoryAtURL:[NSURL fileURLWithPath:fullPath isDirectory:YES]
                                        includingPropertiesForKeys:[NSArray arrayWithObject:NSURLNameKey]
                                                           options:NSDirectoryEnumerationSkipsHiddenFiles
                                                             error:nil];

            
            NSUInteger numChildren, i;
            
            numChildren = [array count];
            children = [[NSMutableArray alloc] initWithCapacity:numChildren];
            
            for (i = 0; i < numChildren; i++)
            {
                JZiCloudFileSystemItem *newChild = [[JZiCloudFileSystemItem alloc]
                                            initWithPath:[[array objectAtIndex:i] path] parent:self];
                [children addObject:newChild];
            }
        }
        else {
            children = leafNode;
        }
    }
    return children;
}
- (NSArray *)childrenFolderOnly
{
    
    if (childrenFolderOnly == nil)
    {
        childrenFolderOnly = [[NSMutableArray alloc] initWithCapacity:[[self children] count]];
        for (NSUInteger i = 0; i < [[self children] count]; i++)
        {
            NSString *path = [[[self children] objectAtIndex:i] fullPath];
            NSURL *url = [NSURL fileURLWithPath:path];
            BOOL isDirectory;
            BOOL fileExistsAtPath = [[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDirectory];
            if (isDirectory && fileExistsAtPath)
            {
                [childrenFolderOnly addObject:[[self children] objectAtIndex:i]];
            }
        }
    }
    return childrenFolderOnly;
}

- (NSArray *)childrenMDOnly
{
    if (childrenMDOnly == nil)
    {
        childrenMDOnly = [[NSMutableArray alloc] initWithCapacity:[[self children] count]];
        for (NSUInteger i = 0; i < [[self children] count]; i++)
        {
            NSString *path = [[[self children] objectAtIndex:i] fullPath];
            NSURL *url = [NSURL fileURLWithPath:path];
            BOOL isDirectory;
            BOOL fileExistsAtPath = [[NSFileManager defaultManager] fileExistsAtPath:[url path] isDirectory:&isDirectory];
            BOOL isMarkdown = ([[url pathExtension] compare: @"md"] == NSOrderedSame);
            if (fileExistsAtPath & isMarkdown)
            {
                [childrenMDOnly addObject:[[self children] objectAtIndex:i]];
            }
        }
    }
    return childrenMDOnly;
}
- (NSString *)relativePath {
    return relativePath;
}


- (NSString *)fullPath {
    // If no parent, return our own relative path
    if (parent == nil)
    {
        return [[[JZiCloudStorageManager sharedManager] ubiquitousDocumentsURL] path];
    }
    
    // recurse up the hierarchy, prepending each parent’s path
    return [[parent fullPath] stringByAppendingPathComponent:relativePath];
}


- (JZiCloudFileSystemItem *)childAtIndex:(NSUInteger)n {
    return [[self children] objectAtIndex:n];
}
- (JZiCloudFileSystemItem *)childFolderAtIndex:(NSUInteger)n
{
    return [[self childrenFolderOnly] objectAtIndex:n];
}


- (NSInteger)numberOfChildren
{
    NSArray *tmp = [self children];
    return (tmp == leafNode) ? (-1) : [tmp count];
}
- (NSInteger)numberOfChildrenMD
{
    NSArray *tmp = [self childrenMDOnly];
    
    return (tmp == leafNode) ? (-1) : [tmp count];
}
- (NSInteger)numberOfChildrenFolder
{
    NSArray *tmp = [self childrenFolderOnly];
    return (tmp == leafNode) ? (-1) : [tmp count];
}


@end

NSOutlineViewDataSource

#pragma mark - NSOutlineViewDataSource
- (NSInteger)outlineView:(NSOutlineView *)outlineView
  numberOfChildrenOfItem:(id)item
{
    return (item == nil) ? 1 : [item numberOfChildrenFolder];
}
- (BOOL)outlineView:(NSOutlineView *)outlineView
   isItemExpandable:(id)item
{
    return (item == nil) ? YES : ([item numberOfChildrenFolder] != 0);
}
- (id)outlineView:(NSOutlineView *)outlineView
            child:(NSInteger)index
           ofItem:(id)item
{
    return (item == nil) ? [JZiCloudFileSystemItem rootItem] : [(JZiCloudFileSystemItem *)item childFolderAtIndex:index];
}
- (id)outlineView:(NSOutlineView *)outlineView
objectValueForTableColumn:(NSTableColumn *)tableColumn
           byItem:(id)item
{
    return (item == nil) ? @"/" : [item relativePath];
}

NSOutlineViewDelegate

- (NSView *)outlineView:(NSOutlineView *)outlineView
     viewForTableColumn:(NSTableColumn *)tableColumn
                   item:(id)item
{
    if (NO)
    {}
    else
    {
        JZFolderOutlineCellView *view = [self.outlineView makeViewWithIdentifier:@"DataCell" owner:self];
        view.textField.stringValue = [item relativePath];
        view.childCountButton.title = [NSString stringWithFormat:@"%ld",(long)[item numberOfChildrenMD]];
        return view;
    }
    
}
- (CGFloat)outlineView:(NSOutlineView *)outlineView
     heightOfRowByItem:(id)item
{
    return 30.0f;
}

Storyboard

View Based, Identifier = @“DataCell”

手动实现 NSTabViewController 的 Rect Transition 及 Propagate Title

 • 

不知为何 我在 OS X 10.11.5 及 Xcode 7.3 的 Storyboard 中设置 Tab View Controller 的 Transition 属性时,Tab View Controller 并不能自动根据子 View Controller 的 Preferred Content Size 来动画渐变,因此只能自己实现了(包括 Propagate Title)。

目前的 Storyboard 设置:
记得关掉 Propagate Title,因为在我的 build 上没有任何用处,而且还影响后面通过 Delegate 手动设置标题。

Step 1:
Subclass TabView Controller,这里是JZSettingsTabViewController,加上 NSTabViewDelegate

Step 2:
实现协议,

- (void)updateWindowSizeWithItem:(NSTabViewItem *)item
{
    NSWindow *window = self.view.window;
    NSSize contentSize = item.viewController.preferredMinimumSize;
    NSSize newWindowSize = [window frameRectForContentRect:(CGRect){CGPointZero, contentSize}].size;
    
    NSRect frame = [window frame];
    frame.origin.y += frame.size.height;
    frame.origin.y -= newWindowSize.height;
    frame.size = newWindowSize;
    [self.view.window setFrame:frame display:YES animate:YES];
    window.title = item.label;
}

Step 3:
既然上面是通过 preferredMinimumSize 获取的 Size,那么在子 View Controller 的实现文件里加上这句,这个时候就不要写 Preferred Content Size 了:

- (CGSize)preferredMinimumSize
{
    return CGSizeMake(500, 400);
}

虽然这样 Size 就不是通过 Storyboard 设置了略嫌麻烦,不过考虑到 TabView Controller 的每个子 VC 都肯定要实现的,也还算可以。
我这里是因为要写 设置界面 所以用到了 NSTabViewController 并且保持固定的 Window 大小(通过去除 NSResizableWindowMask),如果需要一个可以 Resize 的 Window 同时保持不同 Tab 的大小切换动画,

NSSize contentSize = item.viewController.preferredMinimumSize;

这句可以适当修改,比如修改为当前拉伸后的预期大小。

2016.7.1

 • 

虽然还有电工实验,但是大二总算是基本完结了。
对于这个大学我一直没有什么感情可言,对于我来说,只不过是一个我要呆四年的地方罢了,仅此而已,社团活动啊科技比赛啊,一点吸引力都没有。
考试周的时候母上去澳大利亚玩,然后微信里面一直和我说这里环境好不如来读书,然后我说你还没见美国是什么样子,如果见了或许又是另外一番情况了。
总之我和家人的关系比很多人想象的好很多 —— 毕竟在大家眼里看起来我不去上课,甚至不去补考,然后在大二就想要出去工作,对于刻板的(大部分)上一辈来说肯定是大逆不道。但是一切都是会变的,对事物的看法也是这样,虽然每次我和家人打完近一个小时的电话后都会被室友调侃如此依赖父母,但是这的确是有用的,每次沟通后家人都会有让步,直到现在他们对我在学校的要求已经基本和我对自己的要求差不多了,也就意味着我可以做自己喜欢的事情,并且父母并不反对。
我是个性格基本温和甚至有些懦弱的人,也想象不出为了理想断绝母子关系的事情,但就是这么慢慢熬着,期间参加 hackathon 拿拿奖父母也看到了,用工资给母上买了 iPhone 教她用 iOS,在家人关系和对我的支持上反倒有了一个我比较满意的结果。
接着继续说说学校。学校其实应该是很多 985 / 211 大学的缩影,庞大的信息院人数,课间的签到表,宿舍的游戏声,路边的小吃摊,年级群里偶尔的校级活动。一切都看起来很正常,但是我总觉得缺少什么令人兴奋的东西。曾经我对这个学校以及校园生涯有一种征战一番的热情,不过参加了几次比赛并和一些人聊过后就再也没有了,毕竟谁也不想和用 Office Word 做 UI 或者用 Office PPT 一帧一帧放截图并宣称做了软件的人说话,他们总是缺少那么一种行业的专业性,并且自以为达到了某了行业的巅峰。没人用正常一些的设计或原型工具,没人在写真正的程序。这种令人惊讶的短视让我感到很不舒服,当然很不舒服的原因主要还是由内而外的三观不合。
三观不合成为了我在学校最大的困扰。当然盲目地把与自己观点不同的人归为三观不合似乎有些粗放及不负责任,但我还是这么说了。
学校分享会有人问我如何平衡学习与自己钻研技术,我当时出于个人顾虑(或是我自己以为的政治正确)说如果不能吊打老师就还是好好上课吧。这句话最近在班群里几乎成了表情包,以及当我被点名没有去上课的时候,就会有人提起这个好玩的梗。但是这句话本身是可以严肃地推敲的,我到现在还在想这句话究竟该如何实践下去,因为这是一句听起来对但是没有力度的话,当时就是因为其表意模棱两可才说的。对于“如何平衡两件事”这类的问题,或许只能回答具体做事的技巧,而不能回答对平衡导向的看法,因为指导别人的导向是愚蠢的,也是毫无意义的,如果别人遵从我的导向失败了,究竟是我导向有误还是他做事不够努力,这太难以量化了。

大二一年

 • 

commit history

➕ Acquired :

  • 🐙 GitHub Stars ✖️ 433
    JustinFincher
  • 💩 Weird Projects ✖️ 5
    Project Crap / Project Fragment / Project EPOCH / Project Cetacea / Project Hyperions
  • 👾 Cocoapod Pods ✖️ 2
    JZMultiChoicesCircleButton / JZtvOSParallaxButton
  • 📱 iOS App Store App On Sale ✖️ 2
  • 💻 Mac App Store App On Sale ✖️ 1
    WebDrop
  • 🏁 Hackathon Prizes ✖️ 4
    hACKbUSTER
    SF Beijing No.1 / UBER Beijing No.3 / CAA Hangzhou No.1 / SF Shenzhen No.1
  • 🏀 Dribbble Player ✖️ 1
    JustZht
  • 🏀 Dribbble Shots ✖️ 13
  • 🐸 New Skills ✖️ 4
    Rails / CSS / MODO / Subtance Painter

🖥 Devices

+ iPad mini 2
+ Dell P2214H
+ GTX 960
+ iPhone 6s
- iPhone 5s
+ Pencil by 53
+ Ten One Design Mountie

👜 Jobs

+ RavenTech : Unity & iOS Intern
+ Fin GameWorks : Indie Game Developer

🙃 Life

+ 18000+ Songs / Music
Xiami / Soundcloud / Spotify
+ 67 Games, 344.2h Playing Time, 66% Games Not Played
Steam

🤖 Next

  • Finish Project EPOCH
  • Finish Project Cetacea & Project Hyperions
  • Learn C4D
  • Make things happen.

在 MAS 软件内调用 Apple Script

 • 

最近写了一个小软件 WebDrop ,尝试了下上架,关于如何在沙盒内调用 Apple Script 写一点相关的内容。
先看一个 Code Snippet

NSAppleScript *script= [[NSAppleScript alloc] initWithSource:@"tell application \"Safari\" to return URL of front document as string"];
    NSDictionary *scriptError = nil;
    NSAppleEventDescriptor *descriptor = [script executeAndReturnError:&scriptError];
    if(scriptError)
    {
        NSLog(@"Error: %@",scriptError);
    } else
    {
        NSAppleEventDescriptor *unicode = [descriptor coerceToDescriptorType:typeUnicodeText];
        NSData *data = [unicode data];
        NSString *result = [[NSString alloc] initWithCharacters:(unichar*)[data bytes] length:[data length] / sizeof(unichar)];
    }

这段代码会返回当前 Safari 最前 Tab 的网页地址,但是只限于没有开启 App Sandbox 的情况 —— 如果开启了 Sandbox,就会出现以下错误:

2016-06-24 10:41:10.022 WebDrop[81536:1628248] Error: {
    NSAppleScriptErrorAppName = Safari;
    NSAppleScriptErrorBriefMessage = "\U5e94\U7528\U7a0b\U5e8f\U6ca1\U6709\U8fd0\U884c\U3002";
    NSAppleScriptErrorMessage = "\U201cSafari\U201d\U9047\U5230\U4e00\U4e2a\U9519\U8bef\Uff1a\U5e94\U7528\U7a0b\U5e8f\U6ca1\U6709\U8fd0\U884c\U3002";
    NSAppleScriptErrorNumber = "-600";
    NSAppleScriptErrorRange = "NSRange: {61, 6}";
}

其中的 Unicode 为:“应用程序没有运行。”,这是因为沙盒阻止 Apple Script 访问 Safari 了,这个时候有两个解决办法。

第一个见 http://objccn.io/issue-14-2/,首先这个方法好像是不能上 MAS 的,因为其中采用的方法是安装 .scpt 到用户文件夹下,我用这个方法上架时审核人员给出以下的理由:

Performance - 2.4.5  
Your app installs an AppleScript in shared locations.  
Specifically the app installs an AppleScript into the Application Scripts folder.

我也不知道为什么审核人员不给用这种方法,而必须要求 Apple Script 储存在 Container 内 (参见 stackoverflow),不过既然如此,就只能用另一个方法了...

第二个就是加 Entitlement key,有两个 key 可以添加,一个是 com.apple.security.scripting-targets (Reference),一个是 com.apple.security.temporary-exception.apple-events (Reference),其中前者更推荐,使用起来是这样的:

<key>com.apple.security.scripting-targets</key>
    <dict>
        <key>com.apple.mail</key>
        <array>
            <string>com.apple.mail.compose</string>
        </array>
    </dict>

后者的话简单一些,不用配置特定的动作,只要写 bundle id 就可以(注意要全小写)

<key>com.apple.security.temporary-exception.apple-events</key>
	<array>
		<string>com.google.chrome.canary</string>
		<string>com.google.chrome</string>
		<string>com.apple.safari</string>
		<string>com.vivaldi.vivaldi</string>
		<string>org.webkit.nightly.webKit</string>
	</array>

2016.6.20

 • 

最近没有怎么写日记,主要是考试周了,然后没有产出什么东西(虽然 Youtube 倒是看了不少)。
WWDC 看了三个 Session,分别是 222,604 和 609。其中 604 是关于 SceneKit 的。一星期前我还在 V2EX 上说 SceneKit 并不适合用来学习做三维游戏,原因还是那么几个,其中包括 SceneKit 不支持 PBR 这个事情。不过今年的 WWDC 上 SceneKit 开始支持 PBR,也支持一些基本的相机效果(Motion Blur, Bloom, Color Grading)了。
至于使用起来的效果,Session 里演示是非常好的,不过我自己用起来还是有些问题,包括 Xcode-beta 在编辑 .scn 时崩溃,导入 .exr 文件必须是 6-sided 不能是 panorama 不然也崩溃(估计是长宽比的问题),以及之前都没有的导入 .dae 层级混乱的问题。等到考试后再细细研究,反正 Hyperions 是需要用到这些新特性的。
升级了 iOS 10,iPad 上的 Swift Playground 挺震撼,因为可以 import Foundation,UIKit,甚至 SceneKit,也就意味着可以做很多事情,虽然还是受限于 Playground,不过有这个开头,以后出现 Xcode for iOS 也不是什么令人惊讶的事情吧。目前我能看到最接近这个目标的是 Dringend,一个在 iOS 上的本地的 Objective-C/Swift 编辑器,可以远程连接 Mac 编译 in-house ipa 到 Dropbox 然后安装。
Swift Playground 和 Working Copy 可以配合使用,Working Copy 是一个 iOS 上的 git 客户端,支持 push 操作,并且可以作为 Document Provider,即 Swift Playground 里对文件所有的修改都是直接在 Working Copy 的目录里实现的。
顺带一说,明年一定要申请 WWDC 奖学金...今年五月份在公司干活耽误申请了,等到明年估计也攒的有些钱了,应付机票什么的都没太大问题了吧。
最近还在用 Gyroscope,一个个人量化的工具,主要是颜值高(逃,其实这类工具本身就是要做的颜值高才有人用,因为用户消费的就是这个高科技的感觉,如果没有一套 FUI,基本上就和这种感觉无缘了。

2016.6.10

 • 
  • New Project : Hyperions

    简而言之,是一个三维版本的 BuildBox,运行在 iPad 和 iPhone 上。
    基于 SceneKit,布局仿照我日常使用 Unity 的布局:左边 Scene 和 Player,右边 Hierarchy 和 Inspector,顶部放置运行/暂停键。当然在竖排的 Size Class (iPhone 和 iPad Slide View)中布局还是改为了上下分栏。
    计划中要写的功能有:支持从 Dropbox 导入贴图和模型,然后提供一个软件内的材质编辑器(虽然 SceneKit 的自带材质还没支持 PBR),提供简单的挂载 Component 功能(这部分我记得 GameplayKit 自带),提供一个和 Keynote 一样的 Remote 功能,让 iPad 里的场景可以实时在 iPhone 里面预览(玩)。

  • Updated Website : FinGameWorks
    网址:http://fingameworks.github.io/ ,拿到工资后续费了 Apple Developer,软件重新上架,就把之前做的几个东西的链接放在了上面。

  • House of the Dying Sun 上架 Steam 了,虽然我没买,不过还是有些感想的。本身我也就是喜欢太空题材的人,做 Epoch 之前就发现了很多开发中的类似主题(太空,战斗,飞行,etc)的游戏,House of the Dying Sun 算是其中一个。这个游戏的开发者在他的 Dev Blog 里面写了快两三年,现在终于上架了。