RSS

Author Archives: dreamaryzone

Twitter的Chris Fry教你如何构建一个稳定而高效的工程团队

 36氪 11/2/14 8:43 am tips+Kryptoners@36kr.com(Kryptoners)

编者按:本文来自First Round Review,他们准备的文章既讲故事,还同时向创业者提供可操作的建议,以助力打造优秀的公司。本文作者Chris Fry先后在Salesforce和Twitter担任要职,他在这篇文章中分享了“构建高效工程团队”的秘诀。

“罗马军团是人类历史上最具机动性的部队。他们由8个人组成一个最小单位的战斗组,而8个刚好是当时一个帐篷可以容纳的人数。”这是Twitter的SVP Chris Fry在谈到稳定的团队建设时所提到的例子。

Fry先后在Salesforce和Twitter担任要职,他深刻认识到团队建设的重要性。“依靠罗马军团特有的组织架构,罗马帝国主导历史500年,雄霸欧洲大陆。而如果你能使用好类似的架构,世界也会在你的掌控中。”

Fry进一步解释道:“当你手下的工程师发展到40个,你就该在心中敲响警钟了。公司还小的时候,员工们都在同一个办公室里工作,一切运转正常。可随着公司的成长,问题就一个接一个地来了。这时候你该使出看家的工程管理本领,运用模式化的系统来管理你的团队。这样,随着企业的扩展,生产效率才不会降低。”

有着认知科学博士学位的Fry一直在思考如何才能使团队成员有良好的互动,以达到最高的团队工作效率。他在First Round最新一次的CTO峰会上分享了“构建高效工程团队”的秘诀:

·先建好队伍,再分派任务

·保持团队成员在一起工作

·模式化的工作,而不依赖个人

·建立短而规律的出货周期

(一)何为稳定的团队

Fry十分肯定团队稳定的重要性,但稳定性不代表一成不变,它包括了应对突变、合理分配任务等多个方面,目的都是为了让团队发挥最大能量。

Fry入主Twitter工程部时就强调过团队面临的一大基础性难题:世界各地发生的事是很难事先预料的。比方说,“超级碗”期间,Twitter每秒新增消息可达百万条。再比如说,去年,一部电影登陆日本院线并广受好评,Twitter上关于这部电影的推文数量立马暴增。这类突发情况是无法事先预料到的,但得益于Fry对组织结构的改善,Twitter一次次地渡过难关。

Fry说,大多数科技公司(包括Twitter)刚起步时,每个员工都会被分配到一些工作,但这都是被动进行的,项目来了,成员才会被快速地集结起来,这让作为管理者的你形成了一个在短期里快速分配人手的习惯。在公司发展早期,这或许是好的,但长久来看,则需要改变这一习惯。

因为,在公司达到一定规模以后,会出现两件事:1)会有太多人在同一时间为不止一个团队工作,导致战线拉得太长;2)作为分配人员的领导,你已经成为了信息的瓶颈,待处理的事物越来越多,没有时间让你慢下来去一一解决一些小问题,同时,你发现自己已没有能力再去考虑创新这件事了。

“要解决这两个问题,唯一的方法是创建一个稳定的团队。在美国,我们强调个体的表现,但我觉得优秀的小团队更有价值。”

“当人们协同合作时,会创造出超乎想象的共同输出。”

“作为一个管理者,你的重点应该首先在于创建出有良好化学反应的、能把事情做好的团队。然后再考虑分配什么任务给他们。”

Fry观察到,一些管理人员在有新的项目出现时,只是单纯地把一个个人名快速地填写到Excel表格里,但这不可避免地造成了上下级在汇报方面的混乱,浪费了大量时间,因为员工不停地在为不同领导工作,而且需要适应新的环境、新的团队。Fry认为必须改变这一状况。

(二)如何让团队高效运转

Fry认为,保持相对固定的团队成员有利于建立信任,促进良好的沟通,并保证团队的高效运转。“想象一下,你得解决一个难题,但与你共事的都是陌生人,这会有多难。但如果你认识这些人,你曾与他们共事过,你了解他们的长处和能力,并信任他们的话,那你肯定会工作得更加得心应手。”

“好的团队,不仅可以很好地协同工作,还能彼此互相学习。”

若要整个团队的运作达到行云流水的境界,Fry建议在至少半年(最好是一年)的时间里保持团队成员固定不变。这在一个全新的公司里可能不太现实,可一旦工程师团队规模达到40人,这会非常重要,因为这样做可以让团队里的人互相增强实力。

建立人员构成相对稳定的团队,同时也是在保护员工。Fry曾见到不少创业公司过度使用他们的员工。 “你的数据中心的利用率可以达到100%吗?不,不行,甚至80%或90%都不大可能。但有些公司却希望可以让他们员工的利用率达到100%。这只能是一条损耗员工、拖垮公司的不归路。“

在Fry看来,你应该关注的是员工有多少生产能力,并依此将他们分组,利用协同工作来使团队的生产效率最大化。如果只有一个人在做所有的工作,而其他人只是坐在那里看的话,这样的团队永远不能发挥出最佳的实力。同样地,一个勤奋的团队也可能被一个不合作的成员给拖了后腿。“确保每个人都有相当的并且合理的工作量,才是好的工作方式。”

“问题来了,你可以放心地把它交出去,因为你的团队可以自行想出解决方案,并执行下去。如果你能做到这一点,这就是作为领导者的最高境界。”

除了上述几点,Fry建立强大团队的策略还包括:

·让团队成员坐下在一起讨论:当工作不顺利时,这是一个最有效的办法。对一个大的全球性的团队来说,这显然很困难,可如果你的公司还在成长阶段,这可是一个关键。

·指定好负责人:需要选定一个合适的,擅于沟通的人来负责带领团队。他能够鼓励团队其他人,并随时通报进展。

·明确任务:每个团队的成员都应该明确他们团队的目标以及自身的重要性。这是让员工有工作动力的唯一途径。

·明确自己的角色:如果团队表现良好,你作为一位高管,须要确保他们能得到所需的全部资源。

很多创业者都很难放手,但要充分利用你的团队,你需要给他们足够的尊重,让他们有机会学会自我纠正。衡量一个团队是否稳定的标准是:当它遇到困难时,会重新组织,召开紧急检讨会议,并及时做出调整。

“常常有这样的情况:一个人跳槽了,相关的数据和信息也跟着走了。我们的目标应该是多一个或者少一个人都不会对工作有任何影响。而这只有人们共享知识,紧密合作了足够长的时间才能实现。”

(三)模式化的运作

“模式化是扩展规模的关键。”

“当强的小团队(而非个人)成为公司的基本单元,你就可以更快地建立并分配权限,然后去做那比较有创造性的事情了。”

第一定律:好的团队模式应该是多功能型的。

在 Twitter 早期发展无线战略时,整个无线团队都是工程师,他们简单的分成两个部门,一些人负责 Android 的开发,另一些人负责 iOS。可每当公司想增加个什么功能,项目到了这两个部门手里,进展就会变得像老牛拉破车一样缓慢。 这个问题在于,要增加一个新的功能,除了有工程师可以干活,还需要大量外部支持的配合:良好的设计,完善的产品管理以及合适的营销战略。

要解决这个问题,Fry 首先要做的就是把自己的工程师训练成跨平台的无线专家。为此 Twitter 甚至收购了一家公司,用于这项培训。这样一来,再也没有所谓的专家,人人都可以解决多方面的问题。

公司有了这些技术熟练的工程师之后,再把他们分配到各个小组去,每个小组有一个产品专家、一个设计师以及实现具体功能的工程师。每个工程师有对自己工作的控制权,同时他们也是该小组的核心支柱。因此,Twitter 能够不断推出一批批高品质的无线新功能。而且由于工程师相互认识,他们之间的互相通信能保证团队的开放性,使公司更有凝聚力。

这种模式化结构消除了对特定人的依赖。工程师有了更广泛的技能,也不必再依赖于其他人的知识。而设计师和产品经理有了自己接口的工程师可以咨询,也不必一直追着整个工程师团队来询问项目的进展了。这样,团队的每个人都明白自己项目的进展,这增强了团队成员对工作的控制感。

(四)加快出货周期

Fry 刚加入Salesforce 时,该公司的注意力都放在产品测试而非产品推出上。为了改变这一现状,Fry 的第一个动作就是重组工程部门,把当时 40 多个工程师分为若干灵活的小团队。他使用里程碑式的进度规划方法,缩短了测试周期。最终,该公司每四个月就可以推出一款新的数据驱动程序,同时工程师也增加到了 600 名。

两年前 Fry 来到 Twitter,肩负着让 Twitter 快速增长的使命。 他乐在其中。“当你处在公司快速增长的阶段时,每天都会遇到新的问题和麻烦。我得考虑我要保留什么、改变什么。”

“你总是想要尽快缩短产品的开发周期。”

很快,他沿用在Salesforce 时的理念,合理缩短了产品出货周期。

“很多创业团队都可以做到这一点,他们每周推出新版本,这公司发展初期尤其普遍。但是,随着公司的成长,这变得越来越难,”Fry说:“很难保持开发者在时间上的一致性,凝聚力也没有了,于是速度变慢了。”

这时,以小团队为基本单元的组织架构的优势就显现出来了,这使得产品出货周期可以得到保证。每个人都可以井井有条地工作着,deadline 也可以被保证了,团队的开发进程不会因为个人原因而耽搁。

“Twitter 无线产品出货周期的缩短,要归功于优秀小团队的建立,”Fry 总结道。

除非注明,本站文章均为原创或编译,转载请注明: 文章来自 36氪

36氪官方iOS应用正式上线,支持『一键下载36氪报道的移动App』和『离线阅读』 立即下载!

Advertisements
 
Leave a comment

Posted by on February 11, 2014 in Uncategorized

 

Tags:

Cocoapods xcode 5 problem.

Archive Error for cocoapods project created from xcode 4 – Library not found for lpods.

Solution:

Step 1. Set ‘Build Active Architecture Only’ to ‘No’ for both build and release.

Step 2. Remove ‘arm64’ from ‘Valid Architectures’.

 
Leave a comment

Posted by on December 24, 2013 in iPhone

 

Tags: ,

Tap for Tap:小型创业企业广告互推网络

Tap for Tap:小型创业企业广告互推网络: “

对于那些刚刚起步的互联网创业企业而言,由于没有广大的用户基础,因此他们开发的应用程序也无法吸引足够的商家投放广告。面对着空旷的广告位,有什么办法能帮助应用开发商将这些广告存货变为实际的效益呢?

Tap for Tap 本身也是一家创业企业,在他们眼中,应用开发商面对着的困境是一个绝好的商机。与传统的广告销售相异,Tap for Tap 为应用开发商带来了一种全新的模式:广告互推网络。也就是说,应用开发商可以在自己空闲的广告位上发布其余应用程序的广告,作为交易,自己的广告会出现在别人的应用程序中。可交易的广告形式包括横幅广告、插页广告和应用墙,而交易的对等性会由 Tap for Tap 通过产生的流量来判定。

Tap for Tap 认为,此项服务可以帮助那些初登征途的创业者拓展自己的事业,而广告互推网络为他们带来的流量和知名度往往比现实的广告收入更有价值。应用开发商不仅能收获更多用户,还能通过别人的平台把自己的品牌和产品带到不同国界、不同领域的用户手中。Tap for Tap 把他们的小型创业企业互推广告网络形容为‘大海中由小鱼组成的鱼群’。

我们之前介绍了 TinyCo 选择的一种推广模式,他们与推广自己游戏的同行进行利润分成,以在产业内形成较大的推广覆盖面。而 Tap for Tap 明显在谋划一个更大的工程,他们想组建一个可将广告位自由流通的网络,而这种流通带来的收益也超出了货币的衡量范围。当然,没有人愿意拒绝钱,Tap for Tap 表示商家可以直接向他们的广告网络投放广告,他们会根据点击量和用户质量等指标计算费用。

由于苹果的 App Store 已经正式发布条款拒绝应用推荐类应用,外界因此也向 Tap for Tap 提出了一些质疑的声音。对于该情况,Tap for Tap 的创立者 Dyck 和 Gerhardt 表示大可不必担忧。他们的广告网络虽然带有应用推荐的成分,但主体核心是应用而不是广告,何况网络中许多成员的应用并不在 App Store 之中。

如今 Tap for Tap 的广告互推网络已经吸引 5000 余名应用开发者加入,交易的广告量达到 500 万至 1000 万条,每日通过该网络产生的点击量业已达到 10 万次。在这个由小鱼组成的鱼群内,会不会长出一条大鱼来呢?

 

Via:TC

除非注明,本站文章均为原创或编译,转载请注明: 文章来自36氪

编程马拉松

(Via 36氪.)

 
Leave a comment

Posted by on October 17, 2012 in Tools

 

一年,你是真的在成事,还是仅仅在填补时间?

一年,你是真的在成事,还是仅仅在填补时间?: “

从朝九晚五的上班族到白手起家的创业者,这种转变能让很多人望而生畏。其实我并不真的认为这里面风险重重——毕竟,你还很有可能另谋高就(尤其是当你是一名软件开发人员的时候)——不过,这怎么说也是一个人职业生涯中的一大步。而且,大家经常是攒足了够花一年的钱之后再出来创业,因为这样的话,他们即便没有正常收入也能生存下来。我觉得提前攒钱这事挺好,避免破产和赤贫这事也不赖。

但问题是:当你给自己定了一个期限,比如说一年时间来‘成事’时,很多情况下,你最终就仅仅是填补了这段时间而已。我这么说的意思是,你给了自己一年的时间尝试着把公司做起来,但在此期间你却不会真的快速适应,或者作出那些你需要做的艰难决定…因为,毕竟你还有一年时间!

我看到太多人采取这种方法了,不知怎的,这一年时间确被神奇地填满了,不过,该有的进展却并未从天而降。结果,他们花光了自己所有的积蓄,然后又不得不去上班,还让原本陷入困境的公司自生自灭。

事实上,我们总是倾向于把自己既有的时间填充掉,不管是否出于真的需求。

当你让某个人在下周五完成一件事的时候,通常情况下他只会在下周五才开始做这件事,而不是在之前就开始动手。大伙儿好像就是不够珍惜时间,尽管我们知道它非常有限、也不可再生。我们总能找到理由把时间消磨掉,而大部分理由都站不住脚。

所以,不要再填补时间、白白浪费你一年的积蓄了。你应该想着自己没有时间了,而且,从头到尾都应该保持这样的态度。这不是说你要至始至终乱作一团,而是说,你不能假定自己会在几个月之后就能把事情理清,你不能因为自觉机会之门还在继续向你敞开就一直心安理得。因为,这个机会之门随时都可能关闭,而且关闭地比你想象得还要快。所以,不要再给自己一段时间去成功,然后又没有坚持成功所需的最高标准的诚实和‎纪律,而仅仅是把这段时间填补掉,不要再陷入这样的状态了。

当一个人跟我说,‘我有一年的时间把这事弄明白’时,我总会这样想(而且应该这样说):‘所以实际上你只有几周,或者一到两个月的时间,最多不超过三个月的时间去尝试。假如到那时你还没有验证想法,那么你就不会再有时间去构建一个产品,获得用户注意力,并拿到一笔融资或依靠自己的收入自力更生。’

别去填补时间。

加快速度执行。你要认识到,其实你没有很多时间,你要对自己保持绝对的诚实,并在付出努力时保持足够的自律。转型吧,该转型的时候就转型,别为了花时间而花时间、也别为了让自己自讨苦吃而花掉时间。不然,一年以后,你的积蓄花光了,身心俱疲还不说,又什么都没做出来。没错,这不是什么世界末日,但是,你原本可以更快地转向更好的想法,你原本可以省下一部分钱,你原本可以试试新方案,你原本可以有一份新工作,并开始为另一天奋斗。

via Instigatorblog

除非注明,本站文章均为原创或编译,转载请注明: 文章来自36氪

编程马拉松

(Via 36氪.)

 
Leave a comment

Posted by on October 9, 2012 in Uncategorized

 

苹果、Google、微软、Facebook等共推维基式开放网站Web Platform,打造网络标准和技术内容的统一、权威发布平台

苹果、Google、微软、Facebook等共推维基式开放网站Web Platform,打造网络标准和技术内容的统一、权威发布平台: “


为了给所有开放网络技术提供‘权威资源’(‘definitive resource’ ),解决优质资源离散和技术开发分化等问题,苹果、Google、Facebook、Adobe、惠普、微软、Mozilla、诺基亚和Opera等多家大公司共同加入了万维网联盟W3C,共同推出了一个多人协作式的内容发布网站Web Platform

这个新型的网站将为所有开发者提供关于HTML5, CSS3以及其他跟Web标准相关的最新优质内容,并提供关于这类技术开发、实践的技巧和建议。另外,根据W3C的消息,网站还会显示某个特定技术的标准化过程,以及跨浏览器实现状态,试图将自己打造成互联网上该类内容的单一、权威来源。

目前,整个项目已经获得资金支持,并有一个专门的团队来维护网站。因为网站本身是一个维基式的多人协作内容发布平台,所以每个成员公司的员工都可以添加内容更新,而且所有发布到网站上的文档将基于知识共享模式。对用户来说,他们可以在网站论坛上互动,或者在网站的IRC通道讨论他们的项目,交流编程技巧。尽管网站的初始内容来源于成员公司,不过,网站也鼓励其他访问者分享相关信息。

最后,正如Adobe公司说的那样:‘现在,帮助创建并维护web技术最为全面、最为权威的参考信息,将是整个网络社区的事。所以,去看看然后开始贡献,开始记录web吧!’感兴趣的开发人员、极客们可以点此开始行动。

via TNW

除非注明,本站文章均为原创或编译,转载请注明: 文章来自36氪

编程马拉松

(Via 36氪.)

 
Leave a comment

Posted by on October 9, 2012 in Uncategorized

 

How To Synchronize Core Data with a Web Service – Part 1

How To Synchronize Core Data with a Web Service – Part 1: “

Learn how to synchronize Core Data with a web service!

Learn how to synchronize Core Data with a web service!

This is a post by iOS Tutorial Team Member Chris Wagner, an enthusiast in software engineering always trying to stay ahead of the curve.

A lot of apps that store their data in a remote database only work when an Internet connection is available. Think about Twitter or Facebook – without an Internet connection, they don’t do much!

But it’s a much nicer (and faster) user experience if your app can work even without an Internet connection. And there’s good news – you can do this with caching!

The idea is you create a local cache of your data so you can access it whether the user is online or offline. And then when the user is online, you synchronize the cache and the remote database.

In this tutorial, you’ll learn how to do exactly that. You will create an app that stores its information locally with Core Data, and you will synchronize the records to a remote database when online. You can use the techniques you’ll learn in this tutorial with any web service, but to keep things simple you will use a popular web service platform called Parse to create and host your web service.

This tutorial assumes you have basic familiarity with Core Data and web services. If you are new to these topics, check out some of the other tutorials on this site.

So let’s get synchronized… 3, 2, 1, let’s go!

Getting Started

In this tutorial, you are going to create an app to help users solve a very common problem – remembering important dates! If you have ever forgotten a good friend’s birthday or your anniversary, raise your hand. Yep, thought so!

To keep the focus on the synchronization aspect, I’ve created a starter project for you. This starter project will get you to the point of running the application at a local level, where users can add and remove birthdays and holidays which will be stored locally in Core Data.

Your first order of business is to download run the starter project and get familiar with how it works.

Take a look through the app and make sure you understand how it’s currently working, how the Core Data database is set up, and how the view controllers flow together. Again, if you’re confused by anything here you might want to check out some of the other tutorials on this site first.

Once you feel comfortable with how the app is set up, keep reading to learn how to synchronize this local app to a remote web service!

Launch Screen of Starter Project Add Holiday Enter Holiday Information View Holiday List

Setup a free Parse account

Parse is a business that exists to provide the back end for your apps – i.e. the data tier of the servers and storage. Before such services existed, in order to get started with a connected application you would need to configure your own web server, database and probably write a whole lot of code. Not any more!

Parse is free for a rather substantial amount of usage. The free tier provides 1,000,000 API requests and 1,000,000 pushes per month, and provides you with 1GB of file storage. All that and other included services are more than enough to get your app established to the point where it will likely generate more than enough income to pay for the service.

This tutorial uses Parse so you will need to sign up. Parse is actively expanding their service and consistently improving their site. The registration process should be fairly straightforward for new users. Go do that step now before you continue with the rest of the tutorial. Already an existing Parse user? Then go ahead and log in!

Once you’ve created your account and logged in, create a new App for this tutorial from your Dashboard. Note that your App name in Parse can be whatever you want, but it will be referenced as CoreDataSyncTutorial, so you may prefer to use that name.

Once you are logged in and have your Parse App created, you will probably find that the browser is sitting at the Parse dashboard screen with the Overview tab selected. Select the Data Browser tab. The Data Browser will show all of your classes (analogous to the objects or entities that you see when editing your Core Data schema) – but so far there is nothing to see! Time to get busy and get your money’s worth out of this free service!

Press the ‘+’ button to add a new class.

Enter ‘Holiday’ as the name. Parse supports some predefined class types such as ‘User’, and offers support for common functions like logging in with name and password for those classes, but for now Custom is the default and is the right choice for your app.

Now add six new columns with the ‘+ Col’ button; name, observedBy, image, wikipediaLink, details and date.

Press the +Col button to add a new column

Enter ‘name’ as the name and set the type as String.

Continue adding the other columns using the +Col button. Use the list below to assist in creation of the columns.

  • name: String
  • observedBy: Array
  • image: File
  • wikipediaLink: String
  • details: String
  • date: Date

Just to save some heartbreak later, go back and verify that all of the columns (and their corresponding types) have been added later.

Repeat the above process for another class named ‘Birthday’. Again, use + to create the new class as ‘Custom’ type and name it ‘Birthday’. Add the following columns to the new Birthday class:

  • name: String
  • date: Date
  • facebook: String
  • giftIdeas: String
  • image: File

Add Some Data

Time to get those all important holidays entered! Select the ‘Holiday’ class, and click the +Row button to add a new record.

Let’s see; there’s Pi Day, Programmer’s Day, Towel Day, Talk Like A Pirate Day, Star Wars Day, and countless other incredibly important holidays — but to keep things simple for the sake of this tutorial, let’s stick to the examples below ;]

Fill out the fields with the following values. If the data browser is not letting you hit Return to confirm data entry in a field, it’s probably because you have some invalid data there. Make sure you enter the data exactly as shown, especially the more tricky formats like arrays.

Any field that isn’t mentioned above you can leave blank, or enter your own value, if you feel daring! :]

Select the Birthday class, and click the +Row button to add a new record.

If you’re anything like the rest of the world, you’re probably always forgetting the birthdays of the cast of Jersey Shore. Hey, it happens to everyone.

Fill out the fields with the following values:

  • name: Nichole (Snooki) Polizzi
  • date: 2012-11-01T00:00:00.000Z
  • facebook: NicoleSn00kiPolizzi
  • giftIdeas: A brain
  • image: Download and upload this file

It’s highly encouraged to add more records, but what you’ve entered already is enough for a sample set of data for the tutorial.

Okay! You’ve now created some content for your app – but now you need to get that data INTO your app! The next section shows you just how to do that!

Write an AFNetworking client to talk to the Parse REST API

AFNetworking is a class developed by Matt Thompson and Scott Raymond and is described as ‘a delightful networking library for iOS and Mac OS X.’ It makes common tasks like asynchronous http requests a lot easier. As of this writing, AFNetworking does not use ARC, so if you manually add it to a project that is using ARC, the -fno-objc-arc compiler flag is necessary on all of its files. This has already been done for you in the tutorial project.

Note: This tutorial assumes some AFNetworking experience. If you have not used this library before, you should definintely have a read over Getting Started with AFNetworking before you go any further in this tutorial.

The first step to using AFNetworking in your app is to create a client that uses the Singleton pattern. Go to File\New\File…, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter AFHTTPClient for Subclass of, name the new class SDAFParseAPIClient, click Next and Create.

Open your interface file, SFAFParseAPIClient.h and add a new class method:

#import 'AFHTTPClient.h'
 
@interface SDAFParseAPIClient : AFHTTPClient
 
+ (SDAFParseAPIClient *)sharedClient;
 
@end

And then complete the implementation in SFAFParseAPIClient.m:

#import 'SDAFParseAPIClient.h'
 
static NSString * const kSDFParseAPIBaseURLString = @'https://api.parse.com/1/';
 
static NSString * const kSDFParseAPIApplicationId = @'YOUR_APPLICATION_ID';
static NSString * const kSDFParseAPIKey = @'YOUR_REST_API_KEY';
 
@implementation SDAFParseAPIClient
 
+ (SDAFParseAPIClient *)sharedClient {
    static SDAFParseAPIClient *sharedClient = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedClient = [[SDAFParseAPIClient alloc] initWithBaseURL:[NSURL URLWithString:kSDFParseAPIBaseURLString]];
    });
 
    return sharedClient;
}
 
@end

Note that in the code above, you need to insert the personal information generated by Parse: YOUR_APPLICATION_ID and YOUR_REST_API_KEY are unique to your app. Replace YOUR_APPLICATION_ID and YOUR_REST_API_KEY with the values from the Overview tab of the Parse project window.

It also creates three static NSString variables for the Parse API URL, your Parse API Application Id, and your Parse API Key, and implements the +sharedClient method which uses GCD to create a new instance of the class and store its reference in a static variable, thus becoming a Singleton.

Next import AFJSONRequestOperation.h in SDAFParseAPIClient.m:

#import 'AFJSONRequestOperation.h'

Then override -initWithBaseURL: in SDAFParseAPIClient.m to set the parameter encoding to JSON and initialize the default headers to include your Parse Application ID and Parse API Key:

- (id)initWithBaseURL:(NSURL *)url {
    self = [super initWithBaseURL:url];
    if (self) {
        [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
        [self setParameterEncoding:AFJSONParameterEncoding];
        [self setDefaultHeader:@'X-Parse-Application-Id' value:kSDFParseAPIApplicationId];
        [self setDefaultHeader:@'X-Parse-REST-API-Key' value:kSDFParseAPIKey];
    }
 
    return self;
 
}

Add two methods to your interface for SDAFParseAPIClient in SDAFParseAPIClient.h:

- (NSMutableURLRequest *)GETRequestForClass:(NSString *)className parameters:(NSDictionary *)parameters;
- (NSMutableURLRequest *)GETRequestForAllRecordsOfClass:(NSString *)className updatedAfterDate:(NSDate *)updatedDate;

Beneath -initWithBaseURL in SDAFParseAPIClient.m, implement the two methods -GETRequestForClass:parameters: and -GETRequestForAllRecordsOfClass:updatedAfterDate:.

- (NSMutableURLRequest *)GETRequestForClass:(NSString *)className parameters:(NSDictionary *)parameters {
    NSMutableURLRequest *request = nil;
    request = [self requestWithMethod:@'GET' path:[NSString stringWithFormat:@'classes/%@', className] parameters:parameters];
    return request;
}
 
- (NSMutableURLRequest *)GETRequestForAllRecordsOfClass:(NSString *)className updatedAfterDate:(NSDate *)updatedDate {
    NSMutableURLRequest *request = nil;
    NSDictionary *parameters = nil;
    if (updatedDate) {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@'yyyy-MM-dd'T'HH:mm:ss.'999Z''];
        [dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@'GMT']];
 
        NSString *jsonString = [NSString
                                stringWithFormat:@'{\'updatedAt\':{\'$gte\':{\'__type\':\'Date\',\'iso\':\'%@\'}}}',
                                [dateFormatter stringFromDate:updatedDate]];
 
        parameters = [NSDictionary dictionaryWithObject:jsonString forKey:@'where'];
    }
 
    request = [self GETRequestForClass:className parameters:parameters];
    return request;
}

-GETRequestForClass:parameters: will return an NSMutableURLRequest used to GET records from the Parse API for a Parse Object with the class name ‘className’ and submit an NSDictionary of parameters. Acceptable parameters can be seen in the Parse REST API Documentation.

-GETRequestForAllRecordsOfClass:updatedAfterDate: will return an NSMutableURLRequest used to GET records from the Parse API that were updated after a specified NSDate. Notice that this method creates the parameters dictionary and calls your other method. This is merely a convenience method so that the parameters dictionary does not have to be generated each time a request is made using a date.

Okay! So you now have an AFNetworking client ready to go. But it’s not much good until you get the data synchronized! The section below will get you there.

Create a ‘Sync Engine’ Singleton class to handle synchronization

Add another new Singleton class to manage all of the synchronization routines between Core Data and your remote service (Parse). Go to File\New\File…, choose iOS\Cocoa Touch\Objective-C class, and click Next. Enter NSObject for Subclass of, name the new class SDSyncEngine, click Next and Create.

#import <Foundation/Foundation.h>
 
@interface SDSyncEngine : NSObject
 
+ (SDSyncEngine *)sharedEngine;
 
@end

Add a static method +sharedEngine to access the Singleton’s instance.

#import 'SDSyncEngine.h'
 
@implementation SDSyncEngine
 
+ (SDSyncEngine *)sharedEngine {
    static SDSyncEngine *sharedEngine = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedEngine = [[SDSyncEngine alloc] init];
    });
 
    return sharedEngine;
}
 
@end

In order to synchronize data between Core Data (your local records) and Parse (the server-side records), you will use a strategy where NSManagedObject sub-classes are registered with SDSyncEngine. The sync engine will then handle the necessary process to take data from Parse, and…uh…parse it (for lack of a better term!), and save it to Core Data.

Declare a new method in SDSyncEngine.h to register classes with the sync engine:

- (void)registerNSManagedObjectClassToSync:(Class)aClass;

Add a ‘private’ category in SDSyncEngine.m with a property to store all of the registered classes and synthesize:

@interface SDSyncEngine ()
 
@property (nonatomic, strong) NSMutableArray *registeredClassesToSync;
 
@end
 
@implementation SDSyncEngine
 
@synthesize registeredClassesToSync = _registeredClassesToSync;
 
...

Beneath +sharedEngine, add the implementation:

- (void)registerNSManagedObjectClassToSync:(Class)aClass {
    if (!self.registeredClassesToSync) {
        self.registeredClassesToSync = [NSMutableArray array];
    }
 
    if ([aClass isSubclassOfClass:[NSManagedObject class]]) {
        if (![self.registeredClassesToSync containsObject:NSStringFromClass(aClass)]) {
            [self.registeredClassesToSync addObject:NSStringFromClass(aClass)];
        } else {
            NSLog(@'Unable to register %@ as it is already registered', NSStringFromClass(aClass));
        }
    } else {
        NSLog(@'Unable to register %@ as it is not a subclass of NSManagedObject', NSStringFromClass(aClass));
    }
 
}

This method takes in a Class, initializes the registeredClassesToSync property (if it is not already), verifies that the object is a subclass of NSManagedObject, and, if so, adds it to the registeredClassesToSync array.

Note: It’s always preferable to write efficient code, but when it comes to synchronizing data with an online service, you want to get the most ‘bang for the buck’ with each synchronization call. The Parse service may have a free tier, but that doesn’t mean it’s unlimited – and you want to make use of every call in the most efficient way possible! Plus keep in mind that every piece of data pulled over the mobile network counts against the user’s data plan. No one will want to use your app if it’s going to rack up excess data charges! :]

One main concern with keeping the data synchronized is doing it efficiently, so it doesn’t make sense to download and process every record each time the sync process is executed. One solution is to use a process generally known as performing a ‘delta sync’, meaning ‘only give me the new stuff, I don’t care about what I already know’.

Your delta sync process will be accomplished by looking at the ‘updatedAt’ attribute on your Entities and determining which one is the most recent. This date will then be used to ask the remote service to only return records who were modified after this date.

Import SDCoreDataController.h in SDSyncEngine.m:

#import 'SDCoreDataController.h'

Then add this new method:

- (NSDate *)mostRecentUpdatedAtDateForEntityWithName:(NSString *)entityName {
    __block NSDate *date = nil;
    // 
    // Create a new fetch request for the specified entity
    //
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:entityName];
    //
    // Set the sort descriptors on the request to sort by updatedAt in descending order
    //
    [request setSortDescriptors:[NSArray arrayWithObject:
                                 [NSSortDescriptor sortDescriptorWithKey:@'updatedAt' ascending:NO]]];
    //
    // You are only interested in 1 result so limit the request to 1
    //
    [request setFetchLimit:1];
    [[[SDCoreDataController sharedInstance] backgroundManagedObjectContext] performBlockAndWait:^{
        NSError *error = nil;
        NSArray *results = [[[SDCoreDataController sharedInstance] backgroundManagedObjectContext] executeFetchRequest:request error:&error];
        if ([results lastObject])   {
            //
            // Set date to the fetched result
            //
            date = [[results lastObject] valueForKey:@'updatedAt'];
        }
    }];
 
    return date;
}

This returns the ‘most recent last modified date’ for a specific entity.

Next add another new method downloadDataForRegisteredObjects: beneath mostRecentUpdatedAtDateForEntityWithName:

#import 'SDAFParseAPIClient.h'
#import 'AFHTTPRequestOperation.h'

In SDSyncEngine.h import SDAFParseAPIClient.h and AFHTTPRequestOperation.h.

- (void)downloadDataForRegisteredObjects:(BOOL)useUpdatedAtDate {
    NSMutableArray *operations = [NSMutableArray array];
 
    for (NSString *className in self.registeredClassesToSync) {
        NSDate *mostRecentUpdatedDate = nil;
        if (useUpdatedAtDate) {
            mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className];
        }
        NSMutableURLRequest *request = [[SDAFParseAPIClient sharedClient]
                                        GETRequestForAllRecordsOfClass:className
                                        updatedAfterDate:mostRecentUpdatedDate];
        AFHTTPRequestOperation *operation = [[SDAFParseAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
            if ([responseObject isKindOfClass:[NSDictionary class]]) {
                NSLog(@'Response for %@: %@', className, responseObject);
                // 1
                // Need to write JSON files to disk
 
            }
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            NSLog(@'Request for class %@ failed with error: %@', className, error);
        }];
 
        [operations addObject:operation];
    }
 
    [[SDAFParseAPIClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
 
    } completionBlock:^(NSArray *operations) {
        NSLog(@'All operations completed');
        // 2
        // Need to process JSON records into Core Data
    }];
}

This method iterates over every registered class, creates NSMutableURLRequests for each, uses those requests to create AFHTTPRequestOperations, and finally at long last passes those operations off to the -enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock method of SDAFParseAPIClient.

Notice that this method is not complete! Take a look at Comment 1 inside the success block for the AFHTTPRequestOperation. You will later add a method in this block that takes the response received from the remote service and saves it to disk. Now check out Comment 2; this block will be called when all operations have completed. You’ll later add a method here that takes the responses saved to disk and processes them into Core Data.

Awesome! You’ve written a ton of code already! You’re almost to the point where you’ll start seeing some progress in the application. However, at this point you’re lacking a way to start the whole sync process — which is the whole point of this app! :] But tread carefully – the manner in which the sync process is started is crucial, and it’s important to keep track of the status, as you do not want to start the sync process more than once.

Add a readonly property to SDSyncEngine.h to track the sync status:

@property (atomic, readonly) BOOL syncInProgress;

Synthesize the syncInProgress property:

@synthesize syncInProgress = _syncInProgress;

Declare a -startSync method in SDSyncEngine.h:

- (void)startSync;

And add it’s implementation in SDSyncEngine.m:

- (void)startSync {
    if (!self.syncInProgress) {
        [self willChangeValueForKey:@'syncInProgress'];
        _syncInProgress = YES;
        [self didChangeValueForKey:@'syncInProgress'];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
            [self downloadDataForRegisteredObjects:YES];
        });
    }
}

You’ve implemented the -startSync method which first checks if the sync is already in progress, and if not, sets the syncInProgress property. It then uses GCD to kick off an asynchronous block that calls your downloadDataForRegisteredObjects: method.

Moving right along, you need to register your NSManagedObject classes and start the sync!Import the appropriate classes in SDAppDelegate.m:

#import 'SDSyncEngine.h'
#import 'Holiday.h'
#import 'Birthday.h'

And then add this to application:didFinishLaunchingWithOptions:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[SDSyncEngine sharedEngine] registerNSManagedObjectClassToSync:[Holiday class]];
    [[SDSyncEngine sharedEngine] registerNSManagedObjectClassToSync:[Birthday class]];
 
    return YES;
}

This registers the Holiday and Birthday classes with the sync engine in -application:didFinishLaunchingWithOptions:; this pattern allows you to easily add other objects to the sync engine in the future, should you wish to extend the application! Scalability is always good! :]

Then call startSync in applicationDidBecomeActive:

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [[SDSyncEngine sharedEngine] startSync];
}

You made it! You’ve been quite a trooper getting to this point — and yes, you can build and run the App! In your Xcode or device console you’ll see something very close to the following:

2012-07-09 00:39:15.764 SignificantDates[70812:fb03] Response for Holiday: {
    results =     (
                {
            createdAt = '2012-07-09T07:13:24.593Z';
            date =             {
                '__type' = Date;
                iso = '2012-12-25T00:00:00.000Z';
            };
            details = 'Give gifts';
            image =             {
                '__type' = File;
                name = '9d2d8a0d-36fb-4abe-9908-bebd7fb39056-christmas.gif';
                url = 'http://files.parse.com/bcee5dd3-46dc-40a8-abe6-37da2732e809/9d2d8a0d-36fb-4abe-9908-bebd7fb39056-christmas.gif';
            };
            name = Christmas;
            objectId = FVkYM9QROH;
            observedBy =             (
                US,
                UK
            );
            updatedAt = '2012-07-09T07:36:28.097Z';
        }
    );
}
2012-07-09 00:39:15.765 SignificantDates[70812:fb03] Response for Birthday: {
    results =     (
                {
            createdAt = '2012-07-09T07:34:39.745Z';
            date =             {
                '__type' = Date;
                iso = '2012-11-01T00:00:00.000Z';
            };
            facebook = NicoleSn00kiPolizzi;
            giftIdeas = 'A brain';
            image =             {
                '__type' = File;
                name = '5dcc3de5-3add-466a-bb46-31a7b7115903-nicole-polizzi.jpg';
                url = 'http://files.parse.com/bcee5dd3-46dc-40a8-abe6-37da2732e809/5dcc3de5-3add-466a-bb46-31a7b7115903-nicole-polizzi.jpg';
            };
            name = 'Nichole (Snooki) Polizzi';
            objectId = 23S04NSPOR;
            updatedAt = '2012-07-09T07:36:11.792Z';
        }
    );
}
2012-07-09 00:39:15.767 SignificantDates[70812:fb03] All operations completed

Exciting stuff! Now it’s time to do something with this data; it’s just floating around, and you need to persist this data to local storage. The key concept in data transactions is to perform as many network operations as possible in a single batch, in order to reduce network traffic. A really easy way to accomplish this is to queue all requests and save the responses off to disk before processing them.

Add these three three methods to SDSyncEngine.m (beneath downloadDataForRegisteredObjects:) to handle file management:

#pragma mark - File Management
 
- (NSURL *)applicationCacheDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject];
}
 
- (NSURL *)JSONDataRecordsDirectory{
 
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *url = [NSURL URLWithString:@'JSONRecords/' relativeToURL:[self applicationCacheDirectory]];
    NSError *error = nil;
    if (![fileManager fileExistsAtPath:[url path]]) {
        [fileManager createDirectoryAtPath:[url path] withIntermediateDirectories:YES attributes:nil error:&error];
    }
 
    return url;
}
 
- (void)writeJSONResponse:(id)response toDiskForClassWithName:(NSString *)className {
    NSURL *fileURL = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]];
    if (![(NSDictionary *)response writeToFile:[fileURL path] atomically:YES]) {
        NSLog(@'Error saving response to disk, will attempt to remove NSNull values and try again.');
        // remove NSNulls and try again...
        NSArray *records = [response objectForKey:@'results'];
        NSMutableArray *nullFreeRecords = [NSMutableArray array];
        for (NSDictionary *record in records) {
            NSMutableDictionary *nullFreeRecord = [NSMutableDictionary dictionaryWithDictionary:record];
            [record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                if ([obj isKindOfClass:[NSNull class]]) {
                    [nullFreeRecord setValue:nil forKey:key];
                }
            }];
            [nullFreeRecords addObject:nullFreeRecord];
        }
 
        NSDictionary *nullFreeDictionary = [NSDictionary dictionaryWithObject:nullFreeRecords forKey:@'results'];
 
        if (![nullFreeDictionary writeToFile:[fileURL path] atomically:YES]) {
            NSLog(@'Failed all attempts to save response to disk: %@', response);
        }
    }
}

The first two methods return an NSURL to a location on disk where the files will reside. The third is more specific to the application and the remote service; each response is saved to disk as its respective class name.

One interesting situation with the Parse API is that it will returnvalues, which translate to NSNull objects when converted to an NSDictionary. Unfortunately, it is not possible to serialize an NSNull object to disk — you can’t save what is, essentially, nothing! Therefore, you must remove all of the NSNull objects before persisting your data.

You’ll take an optimistic approach here when persisting your data. You’ll attempt to same the response first, without scanning for NSNull objects. If that attempt fails, then scan for NSNull objects, remove any that are found, and try again. If THAT attempt fails, then all you can do is to fall back to standard error handling techniques, where you alert the user or report the issue. this tutorial won’t cover those error handling operations, but you can easily add your own if you so desire.

Next it’s time to modify downloadDataForRegisteredObjects – replace the placeholder comment 1 as follows:

...
 
- (void)downloadDataForRegisteredObjects:(BOOL)useUpdatedAtDate {
    NSMutableArray *operations = [NSMutableArray array];
 
    for (NSString *className in self.registeredClassesToSync) {
        NSDate *mostRecentUpdatedDate = nil;
        if (useUpdatedAtDate) {
            mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className];
        }
        NSMutableURLRequest *request = [[SDAFParseAPIClient sharedClient]
                                        GETRequestForAllRecordsOfClass:className
                                        updatedAfterDate:mostRecentUpdatedDate];
        AFHTTPRequestOperation *operation = [[SDAFParseAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
            if ([responseObject isKindOfClass:[NSDictionary class]]) {
                [self writeJSONResponse:responseObject toDiskForClassWithName:className];
            }
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            NSLog(@'Request for class %@ failed with error: %@', className, error);
        }];
 
        [operations addObject:operation];
    }
 
    [[SDAFParseAPIClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
 
    } completionBlock:^(NSArray *operations) {
        NSLog(@'All operations completed');
        // 2
        // Need to process JSON records into Core Data
    }];
}

You’ve replaced the comment with a much more useful action — writing the data to file! The NSLog goes away, and the -writeJSONResponse:toDiskForClassWithName: method is called.

Build and run your App! You’ll see that the files are saved to disk.

If you want to see the results of all your hard work, you can view the actual files by opening Finder, open the Go menu, choose ‘Go to Folder…’ and enter ‘~/Library/Application Support/iPhone Simulator/’. (If you prefer to click through all the folders, hold down ‘option’ when selecting Go in Finder and Library will reappear in the list of possible destinations.) From here, you will need to do some detective work!

First, determine which version of the simulator you are running. Open the appropriate folder for that simulator, then open Applications. Next you will likely see a number of folders with random names. Eek! Stay cool. An easy way to find the correct folder is to sort the folders by Date Modified and open the most recently modified folder. You will know you found the correct folder once you see ‘SignificantDates.app’ in the folder.

Once you are in the correct App folder, open Library > Caches > JSONRecords. Here you will see the Holiday and Birthday files. You can open the files with Xcode to view them as they are simple Property List files. Hooray! If you see your data in the files, then you know your app is working! :]

At this point the sync process is finished! Even though it doesn’t do a whole lot right now, you still need to be aware when syncing is in progress, and when it is not. You already have a BOOL to track this, but you need to set it to NO at this point in order to stop sync. You’ll also want to know when the App is syncing for the first time, otherwise known as the initial sync. To track this information add the following @interface SDSyncEngine() in SDSyncEngine.m:

NSString * const kSDSyncEngineInitialCompleteKey = @'SDSyncEngineInitialSyncCompleted';
NSString * const kSDSyncEngineSyncCompletedNotificationName = @'SDSyncEngineSyncCompleted';

Then add these methods above -mostRecentUpdatedAtDateForEntityWithName:

- (BOOL)initialSyncComplete {
    return [[[NSUserDefaults standardUserDefaults] valueForKey:kSDSyncEngineInitialCompleteKey] boolValue];
}
 
- (void)setInitialSyncCompleted {
    [[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:YES] forKey:kSDSyncEngineInitialCompleteKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
 
- (void)executeSyncCompletedOperations {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self setInitialSyncCompleted];
        [[NSNotificationCenter defaultCenter]
         postNotificationName:kSDSyncEngineSyncCompletedNotificationName
         object:nil];
        [self willChangeValueForKey:@'syncInProgress'];
        _syncInProgress = NO;
        [self didChangeValueForKey:@'syncInProgress'];
    });
}

After -startSync add the method above which will be called when the sync process finishes.

Process remote service data into Core Data

Well, you have our data persisting to disk in a Property List format. But what you really want to do is to process it into Core Data. This is where you will be doing some heavy lifting and getting into the nitty gritty details. This is the part where the real magic happens!

To start off you will first need a way to retrieve the files from disk. Add -JSONDictionaryForClassWithName: in SDSyncEngine.m:

- (NSDictionary *)JSONDictionaryForClassWithName:(NSString *)className {
    NSURL *fileURL = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]];
    return [NSDictionary dictionaryWithContentsOfURL:fileURL];
}

One caveat to the NSDictionary that -JSONDictionaryForClassWithName: returns is that the information you are interested in is will be in an NSArray with the key ‘results’. So to make things easier for processing purposes, add another method to access the data in the NSArray and spice it up a little to allow for sorting of the records by a specified key.

Add this beneath -JSONDictionaryForClassWithName: in SDSyncEngine.m:

- (NSArray *)JSONDataRecordsForClass:(NSString *)className sortedByKey:(NSString *)key {
    NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
    NSArray *records = [JSONDictionary objectForKey:@'results'];
    return [records sortedArrayUsingDescriptors:[NSArray arrayWithObject:
                                                 [NSSortDescriptor sortDescriptorWithKey:key ascending:YES]]];
}

This method calls the previous method you implemented, and returns an NSArray of all the records in the response, sorted by the specified key.

You won’t really need the JSON responses that were saved to disk much past this point, so add another method to delete them when you’re finished with them. Add the following method above -JSONDictionaryForClassWithName:

- (void)deleteJSONDataRecordsForClassWithName:(NSString *)className {
    NSURL *url = [NSURL URLWithString:className relativeToURL:[self JSONDataRecordsDirectory]];
    NSError *error = nil;
    BOOL deleted = [[NSFileManager defaultManager] removeItemAtURL:url error:&error];
    if (!deleted) {
        NSLog(@'Unable to delete JSON Records at %@, reason: %@', url, error);
    }
}

In order to translate records from JSON to NSManagedObjects, you will need a few methods. First, you will need to translate the JSON values to Objective-C properties; the method you use will vary based on the remote service you are working with. In this case, you are using Parse which has a few ‘special’ data types. The data you’ll be concerned with here are Files and Dates. Files are returned as URLs to the file’s location, and Dates are returned in the following format:

{
  '__type': 'Date',
  'iso': '2011-08-21T18:02:52.249Z'
}

Since the date is in the format of a string, you will want some methods to convert from a Parse formatted date string to an NSDate and back to an NSString. NSDateFormatter can help with this, but they are very expensive to allocate — so first add a new NSDateFormatter property that you can re-use.

Add the dateFormatter property in your private category:

@interface SDSyncEngine ()
 
@property (nonatomic, strong) NSMutableArray *registeredClassesToSync;
@property (nonatomic, strong) NSDateFormatter *dateFormatter;
 
@end

Don’t forget to synthesize it as well in your @implementation:

@synthesize dateFormatter = _dateFormatter;

And add these three methods above the #pragma mark -File Management.

- (void)initializeDateFormatter {
    if (!self.dateFormatter) {
        self.dateFormatter = [[NSDateFormatter alloc] init];
        [self.dateFormatter setDateFormat:@'yyyy-MM-dd'T'HH:mm:ss'Z''];
        [self.dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@'GMT']];
    }
}
 
- (NSDate *)dateUsingStringFromAPI:(NSString *)dateString {
    [self initializeDateFormatter];
    // NSDateFormatter does not like ISO 8601 so strip the milliseconds and timezone
    dateString = [dateString substringWithRange:NSMakeRange(0, [dateString length]-5)];
 
    return [self.dateFormatter dateFromString:dateString];
}
 
- (NSString *)dateStringForAPIUsingDate:(NSDate *)date {
    [self initializeDateFormatter];
    NSString *dateString = [self.dateFormatter stringFromDate:date];
    // remove Z
    dateString = [dateString substringWithRange:NSMakeRange(0, [dateString length]-1)];
    // add milliseconds and put Z back on
    dateString = [dateString stringByAppendingFormat:@'.000Z'];
 
    return dateString;
}

The first method -initializeDateFormatter will initialize your dateFormatter property. The second method -dateUsingStringFromAPI: receives an NSString and returns an NSDate object. The third method -dateStringForAPIUsingDate: receives an NSDate and returns an NSString.

Take a little closer look, there, detective — the second and third methods do something a little strange. Parse uses timestamps in the ISO 8601 format which do not translate to NSDate objects very well, so you need to do some stripping and appending of the milliseconds and Z flag (used to denote the timezone). (Oh standards…there are so many wonderful ones to choose from!) :]

Next add this method below mostRecentUpdatedAtDateForEntityWithName:

- (void)setValue:(id)value forKey:(NSString *)key forManagedObject:(NSManagedObject *)managedObject {
    if ([key isEqualToString:@'createdAt'] || [key isEqualToString:@'updatedAt']) {
        NSDate *date = [self dateUsingStringFromAPI:value];
        [managedObject setValue:date forKey:key];
    } else if ([value isKindOfClass:[NSDictionary class]]) {
        if ([value objectForKey:@'__type']) {
            NSString *dataType = [value objectForKey:@'__type'];
            if ([dataType isEqualToString:@'Date']) {
                NSString *dateString = [value objectForKey:@'iso'];
                NSDate *date = [self dateUsingStringFromAPI:dateString];
                [managedObject setValue:date forKey:key];
            } else if ([dataType isEqualToString:@'File']) {
                NSString *urlString = [value objectForKey:@'url'];
                NSURL *url = [NSURL URLWithString:urlString];
                NSURLRequest *request = [NSURLRequest requestWithURL:url];
                NSURLResponse *response = nil;
                NSError *error = nil;
                NSData *dataResponse = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
                [managedObject setValue:dataResponse forKey:key];
            } else {
                NSLog(@'Unknown Data Type Received');
                [managedObject setValue:nil forKey:key];
            }
        }
    } else {
        [managedObject setValue:value forKey:key];
    }
}

This method accepts a value, key, and managedObject. If the key is equal to createdDate or updatedAt, you will be converting them to NSDates. If the key is an NSDictionary you will check the __type key to determine the data type Parse returned. If it is a Date, you will convert the value from an NSString to an NSDate. If it is a File, you will do a little more work since you are interested in getting the image itself!

To get the image, send off a request to download the image file. It is important to note that downloading the image data can take a considerable amount of time, so this may only work efficiently with smaller data sets. Another solution would be to fetch the image data when the record is accessed (lazy loading), but it would only be available if the user has an Internet connection at the time of lazy loading.

If the data type is anything other than a File or Date there is no way to know what to do with it so set the value to nil. In any other case you will simply pass the value and key through untouched and set them on the managedObject.

Next, add methods that create an NSManagedObject or update an NSManagedObject based on a record from the JSON response to SDSyncEngine.h:

typedef enum {
    SDObjectSynced = 0,
    SDObjectCreated,
    SDObjectDeleted,
} SDObjectSyncStatus;

Then add these two methods right above setValue:forKey:forManagedObject:

- (void)newManagedObjectWithClassName:(NSString *)className forRecord:(NSDictionary *)record {
    NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:className inManagedObjectContext:[[SDCoreDataController sharedInstance] backgroundManagedObjectContext]];
    [record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [self setValue:obj forKey:key forManagedObject:newManagedObject];
    }];
    [record setValue:[NSNumber numberWithInt:SDObjectSynced] forKey:@'syncStatus'];
}
 
- (void)updateManagedObject:(NSManagedObject *)managedObject withRecord:(NSDictionary *)record {
    [record enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [self setValue:obj forKey:key forManagedObject:managedObject];
    }];
}
  • newManagedObjectWithClassName:forRecord: accepts a className and a record, using this information it will create a new NSManagedObject in the backgroundManagedObjectContext
  • -updateManagedObject:withRecord: accepts an NSManagedObject and a record, using this information it will update the passed NSManagedObject with the record information in the backgroundManagedObjectContext

You’re heading into the home stretch! Just two more methods before you tie it all together in a method that actually processes the JSON data into Core Data. Add these methods right after -setValue:forKey:forManagedObject:

- (NSArray *)managedObjectsForClass:(NSString *)className withSyncStatus:(SDObjectSyncStatus)syncStatus {
    __block NSArray *results = nil;
    NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@'syncStatus = %d', syncStatus];
    [fetchRequest setPredicate:predicate];
    [managedObjectContext performBlockAndWait:^{
        NSError *error = nil;
        results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    }];
 
    return results;
}
 
- (NSArray *)managedObjectsForClass:(NSString *)className sortedByKey:(NSString *)key usingArrayOfIds:(NSArray *)idArray inArrayOfIds:(BOOL)inIds {
    __block NSArray *results = nil;
    NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:className];
    NSPredicate *predicate;
    if (inIds) {
        predicate = [NSPredicate predicateWithFormat:@'objectId IN %@', idArray];
    } else {
        predicate = [NSPredicate predicateWithFormat:@'NOT (objectId IN %@)', idArray];
    }
 
    [fetchRequest setPredicate:predicate];
    [fetchRequest setSortDescriptors:[NSArray arrayWithObject:
                                      [NSSortDescriptor sortDescriptorWithKey:@'objectId' ascending:YES]]];
    [managedObjectContext performBlockAndWait:^{
        NSError *error = nil;
        results = [managedObjectContext executeFetchRequest:fetchRequest error:&error];
    }];
 
    return results;
}
  • -managedObjectsForClass:withSyncStatus: returns an NSArray of NSManagedObjects for the specified className where their syncStatus is set to the specified status,
  • -managedObjectsForClass:sortedByKey:usingArrayOfIds:inArrayOfIds: returns an NSArray of NSManagedObjects for the specified className, sorted by key, using an array of objectIds, and you can tell the method to return NSManagedObjects whose objectIds match those in the passed array or those who do not match those in the array. You’ll read more about this method later on.

Now to put all of these methods to use!

- (void)processJSONDataRecordsIntoCoreData {
    NSManagedObjectContext *managedObjectContext = [[SDCoreDataController sharedInstance] backgroundManagedObjectContext];
    //
    // Iterate over all registered classes to sync
    //
    for (NSString *className in self.registeredClassesToSync) {
        if (![self initialSyncComplete]) { // import all downloaded data to Core Data for initial sync
            //
            // If this is the initial sync then the logic is pretty simple, you will fetch the JSON data from disk 
            // for the class of the current iteration and create new NSManagedObjects for each record
            //
            NSDictionary *JSONDictionary = [self JSONDictionaryForClassWithName:className];
            NSArray *records = [JSONDictionary objectForKey:@'results'];
            for (NSDictionary *record in records) {
                [self newManagedObjectWithClassName:className forRecord:record];
            }
        } else {
            //
            // Otherwise you need to do some more logic to determine if the record is new or has been updated. 
            // First get the downloaded records from the JSON response, verify there is at least one object in 
            // the data, and then fetch all records stored in Core Data whose objectId matches those from the JSON response.
            //
            NSArray *downloadedRecords = [self JSONDataRecordsForClass:className sortedByKey:@'objectId'];
            if ([downloadedRecords lastObject]) {
                //
                // Now you have a set of objects from the remote service and all of the matching objects 
                // (based on objectId) from your Core Data store. Iterate over all of the downloaded records 
                // from the remote service.
                //
                NSArray *storedRecords = [self managedObjectsForClass:className sortedByKey:@'objectId' usingArrayOfIds:[downloadedRecords valueForKey:@'objectId'] inArrayOfIds:YES];
                int currentIndex = 0;
                // 
                // If the number of records in your Core Data store is less than the currentIndex, you know that 
                // you have a potential match between the downloaded records and stored records because you sorted 
                // both lists by objectId, this means that an update has come in from the remote service
                //
                for (NSDictionary *record in downloadedRecords) {
                    NSManagedObject *storedManagedObject = nil;
                    if ([storedRecords count] > currentIndex) {
                        //
                        // Do a quick spot check to validate the objectIds in fact do match, if they do update the stored 
                        // object with the values received from the remote service
                        //
                        storedManagedObject = [storedRecords objectAtIndex:currentIndex];
                    }
 
                    if ([[storedManagedObject valueForKey:@'objectId'] isEqualToString:[record valueForKey:@'objectId']]) {
                        //
                        // Otherwise you have a new object coming in from your remote service so create a new 
                        // NSManagedObject to represent this remote object locally
                        //
                        [self updateManagedObject:[storedRecords objectAtIndex:currentIndex] withRecord:record];
                    } else {
                        [self newManagedObjectWithClassName:className forRecord:record];
                    }
                    currentIndex++;
                }
            }
        }
        //
        // Once all NSManagedObjects are created in your context you can save the context to persist the objects 
        // to your persistent store. In this case though you used an NSManagedObjectContext who has a parent context 
        // so all changes will be pushed to the parent context
        //
        [managedObjectContext performBlockAndWait:^{
            NSError *error = nil;
            if (![managedObjectContext save:&error]) {
                NSLog(@'Unable to save context for class %@', className);
            }
        }];
 
        //
        // You are now done with the downloaded JSON responses so you can delete them to clean up after yourself, 
        // then call your -executeSyncCompletedOperations to save off your master context and set the 
        // syncInProgress flag to NO
        //
        [self deleteJSONDataRecordsForClassWithName:className];
        [self executeSyncCompletedOperations];
    }
}

This method must be called where Comment 2 sits as a placeholder in the downloadDataForRegisteredObjects method. When the HTTP request operations are completed, returned data is written to Core Data, and your app can then access them as required.

This is it, kids! This is the final downloadDataForRegisteredObjects method!

- (void)downloadDataForRegisteredObjects:(BOOL)useUpdatedAtDate {
    NSMutableArray *operations = [NSMutableArray array];
 
    for (NSString *className in self.registeredClassesToSync) {
        NSDate *mostRecentUpdatedDate = nil;
        if (useUpdatedAtDate) {
            mostRecentUpdatedDate = [self mostRecentUpdatedAtDateForEntityWithName:className];
        }
        NSMutableURLRequest *request = [[SDAFParseAPIClient sharedClient]
                                        GETRequestForAllRecordsOfClass:className
                                        updatedAfterDate:mostRecentUpdatedDate];
        AFHTTPRequestOperation *operation = [[SDAFParseAPIClient sharedClient] HTTPRequestOperationWithRequest:request success:^(AFHTTPRequestOperation *operation, id responseObject) {
            if ([responseObject isKindOfClass:[NSDictionary class]]) {
                [self writeJSONResponse:responseObject toDiskForClassWithName:className];
            }
        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            NSLog(@'Request for class %@ failed with error: %@', className, error);
        }];
 
        [operations addObject:operation];
    }
 
    [[SDAFParseAPIClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
 
    } completionBlock:^(NSArray *operations) {
 
            [self processJSONDataRecordsIntoCoreData];
    }];
}

Last thing: to see the effect, go to SDDateTableViewController.m and update -viewDidAppear: and -viewDidDisappear: to register for the sync complete notification and reload the table:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
 
    [[NSNotificationCenter defaultCenter] addObserverForName:@'SDSyncEngineSyncCompleted' object:nil queue:nil usingBlock:^(NSNotification *note) {
        [self loadRecordsFromCoreData];
        [self.tableView reloadData];
    }];
}
 
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@'SDSyncEngineSyncCompleted' object:nil];
}

Run the project and see your records created in Parse come in to your App!

Manually trigger sync with remote service

Right now, your App will automatically sync when the user opens the App. This is generally an acceptable practice; however some users may want to refresh the dataset without leaving the App. You’ll next implement a refresh button on both the Holiday table view and Birthday table view in order to accomplish this.

Start by opening Storyboard.storyboard, locating the Holidays Date Table View Controller and dragging a Bar Button Item onto the top left side of the navigation bar. Once you have dragged the bar button item onto the nav bar, change its Identifier attribute to ‘Refresh’ as seen below:

Now open the Assistant editor (the bow tie button in Xcode) and make sure SDDateTableViewController.h opens in the editor. Holding down the CTRL key, click and drag from the Refresh button to your interface to add a new IBAction named ‘refreshButtonTouched’:

Do this same process again, but this time instead of creating an Action, create an Outlet for the button and name it ‘refreshButton’. The outlet should be a strong reference, not a weak one — otherwise when the button is replaced by the activity indicator, it will be nil when the time comes to replace the button on the navigation bar.

Now select the Refresh button in your Storyboard, and while holding the Option/Alt key, click and drag it to your Birthdays Date Table View Controller to copy it, by copying you will also copy the outlets you already set up. Once you copy the button, with the Assistant editor still open you can hover the circles in the gutter next to your IBAction and IBOutlet to visually see the referenced outlets in the Storyboard, both buttons should highlight as shown below.

Open SDDateTableViewController.m and add an import for SDSyncEngine.h:

#import 'SDSyncEngine.h'

Next complete the -refreshButtonTouched: method that was created for you during the Storyboard editing you just did, and add the following methods at the bottom of your class above @end:

- (IBAction)refreshButtonTouched:(id)sender {
    [[SDSyncEngine sharedEngine] startSync];
}
 
- (void)checkSyncStatus {
    if ([[SDSyncEngine sharedEngine] syncInProgress]) {
        [self replaceRefreshButtonWithActivityIndicator];
    } else {
        [self removeActivityIndicatorFromRefreshButton];
    }
}
 
- (void)replaceRefreshButtonWithActivityIndicator {
    UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(0, 0, 25, 25)];
    [activityIndicator setAutoresizingMask:(UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin)];
    [activityIndicator startAnimating];
    UIBarButtonItem *activityItem = [[UIBarButtonItem alloc] initWithCustomView:activityIndicator];
    self.navigationItem.leftBarButtonItem = activityItem;
}
 
- (void)removeActivityIndicatorFromRefreshButton {
    self.navigationItem.leftBarButtonItem = self.refreshButton;
}
 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@'syncInProgress']) {
        [self checkSyncStatus];
    }
}

-checkSyncStatus will ask the SDSyncEngine singleton if the sync is in progress. If so, it will call -replaceRefreshButtonWithActivityIndicator which does what it says,which is replace the refresh button with a UIActivityIndicatorView. Otherwise, the method will remove the UIActivityIndicatorView by replacing it with the refreshButton.

You will also implement -observeValueForKeyPath:ofObject:change:context: in order to observe changes in SDSyncEngine. To do so you will need to register for those notifications:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
 
    [self checkSyncStatus];
 
    [[NSNotificationCenter defaultCenter] addObserverForName:@'SDSyncEngineSyncCompleted' object:nil queue:nil usingBlock:^(NSNotification *note) {
        [self loadRecordsFromCoreData];
        [self.tableView reloadData];
    }];
    [[SDSyncEngine sharedEngine] addObserver:self forKeyPath:@'syncInProgress' options:NSKeyValueObservingOptionNew context:nil];
}
 
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@'SDSyncEngineSyncCompleted' object:nil];
    [[SDSyncEngine sharedEngine] removeObserver:self forKeyPath:@'syncInProgress'];
}

Update -viewDidAppear: and -viewDidDisappear: to look like the above, also notice when the view appears we check the sync status immediately.

Now when you build and run the App, you will see an activity indicator while the sync is in progress and a refresh button when the sync is inactive. Touching the refresh button fires off the sync engine.

RBScreen1 RBScreen2 RBScreen3

That’s it, folks! This concludes Part 1 of the tutorial.

Where To Go From Here!

You can download the project at this stage here.

Stay tuned for part 2 of this tutorial, where you’ll finish the app by adding the following features:

  1. Delete local objects when deleted on server
  2. Push locally created records to remote service
  3. Delete records on server when deleted locally

If you have any questions or comments, please join the forum discussion below!


This is a post by iOS Tutorial Team Member Chris Wagner, an enthusiast in software engineering always trying to stay ahead of the curve.

How To Synchronize Core Data with a Web Service – Part 1 is a post from: Ray Wenderlich

(Via Ray Wenderlich.)

 
Leave a comment

Posted by on September 4, 2012 in iPhone

 

从37signals CEO Jason Fried身上我们看到了什么?——创业圈里稀缺的人本主义精神

从37signals CEO Jason Fried身上我们看到了什么?——创业圈里稀缺的人本主义精神: “

前阵子,国外媒体快公司采访37signals CEO的一篇文章在圈子里引起了不小波澜。在这次采访中,Fried毫不避讳地发表了自己对美国创业圈生态的看法:

Fried称,现在有太多的创业公司不惜牺牲公司员工长期的士气,以换取短期的爆发式增长。这些公司通常会选择透支员工,每周让他们工作60,70甚至80小时,然后换另外一批人代替他们。因为这些公司知道,不管是公司还是员工,要么是都‘死’,要么是被收购,要么…不管是哪一种情况,他们都不在乎,他们只是一味地消耗掉自己的资源,这就像在开采石油的时候要采尽每一滴油一样…

而且,很多公司都抱着买彩票的心态,他们融一笔钱,雇一批人,然后一边把这些人往死里耗,一边梦想着自己的彩票哪天能中…整个行业呈现出一种病态,Fried甚至援引了《Maverick》一书作者的一个比喻,将很多公司梦想着可以一飞冲天,‘为了变大而变大’的心态对比‘恶性肿瘤’。

Fried的这番言论可谓火药味十足,看完以后你也可能会以为自己又回到了工业时代初期大资本家剥削工人阶级的年代。然后,已经透支的员工就像坏死的零件一样,可以随意被替换。但与那个时代不同的是,那个时代是资本家在单向剥削工人阶级,但是现在,这些公司的老板们跟员工一样忙得焦头烂额,就好像人人都被洗脑——公司自上而下,大家一荣俱荣,一损俱损。而且,所有人对这种做法都深信不疑,这也就是Fried所谓的行业病态。Fried的这番话虽然激进,也不排除他在采访中因为情绪激动有些口不择言,但从这番话中,我们却看到了创业圈里极其稀缺的一种东西——人本主义精神。首先,作为一家公司的CEO,顶着被另外一些同行‘围攻’的压力,借助一个公共平台说出这样一番话,这是需要勇气的。其次,我们可以这么说,这里有一家公司关心自己,警惕员工的工作状态和心理状态,而不是将大家统统视作拼命工作自我燃尽的行尸走肉。而Fried这么说,也确有他自己的底气。在我们之前刊登的另外一篇文章《变化》中,我们便提及了Fried的一些新尝试:

在37signals,Fried将五月份到十月份的工作制改成了一周工作四天,每天8小时,同时,公司还会为员工免费提供来自社区的新鲜果蔬。不仅如此,他们在今年夏天还做了一个新尝试——让大家在六月份这一个月干自己想干的任何事。这不是普通意义上的假期,而是说大家可以从六月份原本的工作计划中抽离出来,搁下不必要的工作,然后把时间用在探索自己的想法上面。

Fried称:

这个‘6月份我做主’的实验结果是,我看到了34名员工迸发出的前所未有的创造力。这个实验很有趣,也大大增长了我们的士气。而且,它非常地富有成效,以至于,我们决定在一年中重复这样的项目几次。

从上面的这些策略中,我们可以看到Fried的两种理念:以人为本和无为而治。Fried做的是这样一件事:尊重并信任每一位员工,不过度干预他们的工作,但同时又让他们发挥出最大的潜能。事实上,要做到这一点非常困难,它对员工和整个团队的能力,控制力,自我管理等综合素质要求也非常高。但是从一定程度上来说,37signals做到了——这家公司已经成立了有13年,但目前为止,37signals却依然坚持着一个35人的精英团队——Fried称,

假如我们愿意,我们可以雇佣成百上千的员工,我们的收入和利润可以支撑这一点,但我认为这么做糟透了。

而他们的产品同样处处渗透着以人为本的设计理念。针对他们其中的一项产品Basecamp,来自他们的竞争对手Salesforce公司的Farhad Manjoo就称赞说:‘Basecamp代表了Web软件的未来。’而Quora上的一名作者则这样说道

关怀,爱,以及实用性,这些都深深地渗透在他们搭建产品的方法以及最终的产品中。他们让客户可以轻松地完成自己的工作,这样客户就有时间放松、放假。人们愿意为获取这种内心的平和,花上无数的钱。

我被37signals,以及他们做好公司的那种方式鼓舞了。他们不仅能做事,而且能用一种正确的方式把它达成。没有炒作,所有的成绩都可以量化。

再让我们回到刚才的那个话题,诚如我们所想的,要把以人为本做到极致,让团队可以认可公司并发挥最大效益,让公司以外的人可以认可公司并购买他们的产品,实际非常困难,它对团队的每一个成员的能力,自觉度,实践力要求都非常高。但个人认为,这种企业文化的形成实际是渐进的——37signals之所以可以将在行业中如此小众的想法变成现实并发扬光大,首先要得益于领队的Fried和David自己对这种理念的深信不疑和至始至终地执行。他们会依据这一点去招人,用这种信念感染团队的其他人,进而让所有人都对这一点深信不疑并至始至终地执行。这就是所谓的,团队的每一个人都在塑造企业文化。Fried和David这么做了,而一种良性循环也逐渐形成了。

不过,需要指出的是,37signals是一家做网络软件的公司,公司生产的边际成本不高,而且拼的就是设计,所以跟其他的一些行业相比,他们没有那么依赖人力,因而员工也可以拥有更高的自由度。还有一点是,有一句话叫做‘林子大了什么鸟都有’,上面的这种管理模式或许很难在一个大的团队中推行,团队文化也容易在‘人山人海’中被稀释。而这或许也解释了,为什么这个运作了13年,效益良好的公司到目前为止,都还只有35人。不得不说,Fried很聪明,也很坚持。

除非注明,本站文章均为原创或编译,转载请注明: 文章来自36氪

来微信加36氪为好友吧,打开微信‘添加朋友’->按号码查找,然后输入‘36氪’添加好友。[二维码]

(Via 36氪.)

 
Leave a comment

Posted by on September 4, 2012 in Uncategorized