这篇文章将会在保证主体内容的完整上加入参加我本科时代最后一个比赛 Apple WWDC 2019 Scholarship 项目的完整讲解。

前言

在上周五(15号)时,我刷着微博,突然间看到了梁杰大大转发 AplloZhu 的一条关于 Apple 今年 WWDC Scholarship 的活动介绍。我注意看了下时间,15 号早上 8 点到 25 号早上 8 点,总共就 10 天的时间,在这个十天的时间使用 Playground 做出一个 demo。当然这里说的 demo 肯定不是我们平常做技术验证那般无所谓,要求是能够完整表达自己的创意,并结合 Apple 的相关 API 完成。

大概是早上 10 点左右看到的消息,从上周五直到昨天,我整个人都处在一种十分的焦虑过程中,这种焦虑伴随着激动和不安,几乎每天晚上都没有睡好,早上到公司时也没有任何的状态,一心想着我要怎么做好这个事情。

得知这个消息后,立马开始在脑海里搜寻创意,我非常明白 Apple 对创意十分看重,然后又想到这两年 Apple 对人工智能的关注几乎到了 all in 地步,于是结合 CoreML 和 AR 能做些什么呢……

思考了二十分钟后,我彻底放弃了通过技术来搜寻创意的想法。突然灵光一闪,想到 Apple 近年来对教育和环境领域是相当重视!如果我做一个跟环境保护有关的教育项目呢?在思考的同时我又浏览了一遍 WWDC 2019 Scolarship 的 repo list,把几乎所有项目介绍中的视频都看了一遍,总结出了以下几点:

  • 不需要做太精美的 demo,但精美有加分;
  • 音乐如果运用不当还是算了吧;
  • 重点在于你要表达什么,而不是你做了什么;
  • Playground 也可以没有“交互体验”,直接看运行;

这时,我悬着的心终于可以放了下来,原来并不一定要做成向 Swift Playground 中那么精美的 demo 啊!但是值得注意的是,在去年 accepted 的 repo 中,有一部分是视觉上赢了。我在想,如果我直接拼 CoreML 和 AR 等的东西实在是没发现有什么好的点子,为何不来一个弯道超车?我也来做一个视觉上的冲击?

我继续在大脑里搜索,突然!我发现了这么“黎锦”这个东西一直给了我很大的震撼!它不但具备对称美、粗旷的线条,甚至还有夸张的图案!在吃午饭的时候我仔仔细细的全盘推演了一遍交互,如果我能够运用得好“对称美”这个关键点,一定很赞!经过了一番修正后,决定就是这个题目了!

黎锦

准备工作

脑暴

午饭回来后,我开始清空大脑,放下所有其它事情,准备全力以赴。我首先确定了自己要做的是一个具备“对称美”的 demo,必须要围绕“对称”这个事情来展开;而且还要突出黎锦最核心的地方——粗旷的线条和夸张的图案,头脑风暴开始了……

健身完后,脑子连同身体一块轻松了,开始构思具体的交互和细节。黎锦,它的本质是纺织品,其次是黎族人对自然的崇拜的表达,最后才是工艺品,所以我最终目的也出来了:

  • 利用纺织品的底纹;
  • 突出黎族人的对自然崇拜的元素;
  • 尽可能做的精美;
  • 利用拼图的特性。

又经过了一个多小时的时间,把一些细节的地方都完善好,并确定自己也被自己陶醉了后,开始画原型图,下面是脑暴时的部分手稿:

手稿

原型图

临近下午六点时,终于把一些确定的元素都完成了。不得不说这些“突出黎族人对自然崇拜的元素”实在难以搞定,单是用 Sketch 画这几个小图,一两个小时就这么过去了,下面是原型图的展示:

原型图 1

原型图 2

插一句题外话:在此我要强烈鄙视曾经的我。以往参加各种比赛时,我都对原型图嗤之以鼻,认为这部分工作毫无意义,存粹是浪费时间。但经过 BonfirePLook 这两个项目后,给我打了一个狠狠的脸!真诚的希望大家在日后进行项目的开发工作时一定要先做好原型图。

开发

第二天

整个 demo 的核心难点在今天基本上就完成了。主要是解决“对称”问题。当时我给自己下的一个死要求——不要考虑性能,原因很简单,第一是没有时间,第二是我的运行平台为 Mac,不需要纠结这方面的问题。

镜面对称

当用户在视图虚线的左半部分拖动色块时,与之对应在屏幕右半部分的色块也会随着一起滑动至同等位置,且为镜面对称。这部分功能的实现比较粗暴,但实际上就是这么一回事,但在内存占用上是有相当大可以进行优化的空间,因为用户永远只能操纵左半部分的色块,而对于右半部分的色块是无法操纵的,所以位于右半部分的色块完全可以抛去 UIResponder 等协议的遵循,仅仅只需要 CALayer 即可。

底部栏

这部分比较简单,一个 UICollectionView 即可完事,但考虑到后期组件重用问题,还是给 UICollectionView 包了一层 UIView。到现在反过头去看,实际上是没有任何必要的。

完工图

第一天完工图

第三天

今天是周日,时间比较多,开始做一些交互上的东西。

从底部栏拖拽元素至画布上

首先是从“底部栏”拖拽元素至画布上。这部分其实也还 OK,需要维护好两套坐标系的转换,用户触摸点从底部栏到画布上这一过程色块需要进行转换的坐标不能使用底部栏的坐标进行,因为色块在底部栏上的 x 和 y 都是 10 以内的数,如果还以底部栏作为坐标转换的依据,那么当用户拖拽色块时,色块会直接跑到屏幕的最上方。

因此我们需要以底部栏的 superview 来作为坐标转换的依据,也就是 UIViewControllerview。经过一番调整后,使用了长按手势来激发拖拽功能,并增加了底部栏元素被拖拽后数据源的删除。

自动吸附

这部分 bug 比较多,直到昨天都还在维护相关逻辑。我想要达到的效果是,把画布分为 smallnormalbig 三个尺寸的大小,其中 normal 是默认大小,3 * 9 的方格充满画布。当用户把元素拖拽到画布上 touchEnded 时,如果此时元素不满足完全嵌入离它最近的方格时,系统将自动把该元素“吸附”到距离该元素最近的方格中,下面这个动图可以比较好的进行展示:

吸附功能

当时之所以想到这个吸附功能,主要是我在玩的过程中没法判赢~如果只是让用户去自己根据完成图的提示来进行拼图,那实在是无趣了点,要稍微的营造出一点慢慢的看出端倪,最后拼图完成后发出一声“哦!”的感叹就满足了~

占位还原

这个功能是依赖“自动吸附”的。如果用户此时拖拽了一个元素覆盖到了一个旧元素上,系统要自动把拖拽的元素还原到原来的位置上。这部分也 OK~

第四天

今天是周一,距离提交截止还有七天。

判赢

完成了之前把画布切割成不同 size 的方块功能后,此时再来思考如果才能算赢就很简单了,因为画布本身就是一个二维列表,嗯,就是这么简单了。

在 Playground 中跑起来

之前有几次尝试使用 Playground 进行开发的糟糕体验后,这次彻底放弃了使用在 Playground 开发的想法,先用写一个 app 的架构模式去完成然后再迁移到 Playground 中。

第一次迁移真是把我搞得不行~整了好久根本没跑起来。幸亏有了去年的 WWDC Scholarship repo list,看了好几个 repo 后才明白是怎么个事情,第一次使用 Playground 运行项目可以参考我的这一篇文章

第四天完工图

第五天

到了今天核心功能基本上都已经完成,接下来要做的就是提升交互,尽可能的做得更加精美,完善除了主流程之外的其它 case 下产生的问题。

画布大小

这部分的功能在第二天的时候已经思考过了,提供了三种大小的画布尺寸,就不展开了~

其它

今天纠结错了一个点。原本想在自定义模式下提供“长方形”、“正方形”和“圆形”三种形状的画布,但却因为最初的架构问题导致一直没法完成,看着时间越来越短,拼图模块却一点没做~内心开始有些紧张了。今天反反复复的把时间浪费了在了修改画布形状的功能上。

进过反复修改后,终于确定了“大力神”的定稿!我当时看到“大力神”的表情……😲

黎族守护神之一“大力神”

第六天

今天是周三,留校与毕设导师见面的日子,又多了完整的一天。给自己下了一个必须完成的任务,今天务必要完成拼图模块。

拼图模块

拼图大家都玩过,按照提示图把零散的部件拼好。我并没有全盘借用拼图的全套游戏模式,而是仅仅采用了它的游戏逻辑。可以确定的是,肯定不能把画十几二十个拼图元素小图,这样不但会把人画疯而且时间会浪费得更多,再加上如果考虑到屏幕适配问题那就更痛苦了呢~

于是我想到使用 Core Graphics 的方法对一张图片按照所需尺寸进行切割,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
extension UIImage {
/// 通过原图获取 rect 大小的图片
func image(with rect: CGRect) -> UIImage {
let scale = UIScreen.main.scale
let x = rect.origin.x * scale
let y = rect.origin.y * scale
let w = rect.size.width * scale
let h = rect.size.height * scale
let finalRect = CGRect(x: x, y: y, width: w, height: h)

let originImageRef = self.cgImage
let finanImageRef = originImageRef!.cropping(to: finalRect)
let finanImage = UIImage(cgImage: finanImageRef!, scale: scale, orientation: .up)

return finanImage
}
}

通过以上方法,我就达到了只需要通过一些简单的数学计算就可以根据用户当前设置 sizeType 类型来控制单个拼图元素的大小~

拼图完成

如果用户都按照拼图的实际位置放置好了,实际上就是赢了~但当我写完这部分的逻辑后,看到最终的成果图,总感觉哪里不对劲。

奇怪的拼图完成图

对比了以后发现!woc???怎么两个头?反复查看取拼图元素的逻辑代码后,发现原来是这么一回事……

我在代码中写的是从每行 x=0 处开始往后取 item=62.5 宽度的图,连续取三个,但 iPhone 7 的屏幕宽度为 375,一半就是 187.5,三个 62.5 就是 187.5 这没啥问题,但问题出在我的图是不是标准的 750 * 1334,而是 647 * 1159,所以这就导致会多往后截取的问题。不想改图稍微改动了下相关位置逻辑完事。(其实应该把图改了)

给拼图完成后加了一点彩纸动效和背景音乐,但背景音乐不知为何在 Playground 中只“滴”的一下就没有下文了,彩纸动效如下:

彩纸动效

第七天

今天一直在维护拼图逻辑,想着赶紧把拼图做完,然后立马开始做自定义部分。

第八天

今天是周五,学校下午开了个年级会,又多了完整的一天。今天一定要完成自定义模块,然后明天开始写各种文案。

自定义模块

完成了拼图部分后,自定义就很简单了,剔除判赢、自动吸附和裁切元素三个大头,加上一些好看的元素替换到底部栏中的拼图部分中的元素即可。但因为是用户自定义部分,要提供一定的个性化功能,因为今天已经周五了,好多文案也没写,想着那就完成“删除”和“旋转”两个功能好了~

  • 删除。再次使用长按手势完成,稍微啰嗦一点的地方在于各种数据源的删除和恢复的维护逻辑。
  • 旋转。使用了双击手势。考虑到用户在 Playground 中使用旋转手势实在是难以操作而做出选择。在实现旋转的过程中,也要时刻保持旋转后的镜面对称,不能只是简单的 currentItem.transform = copyItem.transform 这般简单了。需要根据不同的情况做几个取反操作再赋值。

第九天

到了今天不知为何毫无动力,已经没有再继续开发和优化下去的欲望了,只想着快点结束。只对用户自定义部分新增了几个新资源后就结束了全部的开发工作,开始写 Playground。

Playground

我给大家一个建议:开两个工程,一个是以 app 的模式来进行开发的工程,一个是 Playground 工程。写完 app 一个功能后就立马在 Playground 中进行复现,直到复现成功且功能正常后,再继续写 app。这样你会爽得飞起。

当然,Apple 给了一个 PlaygroundBook 的模版工程,可以根据这个工程来进行修改。

Playground 遵循大部分的 Markdown 语法,如下图所示:

Xcode markdown

需要注意的是:

  1. 使用 //: [Previous: The First Part](@previous) 返回上一个 Page,使用 //: [Let's get the first part!](@next) 进入下一个 page,目前来看只能做顺序跳转,如果是纯英文命名的 page file,排序是按照字母序来进行的(差点被坑)。推荐在命名前加上 123 来进行标识。
  2. 在 Page 中进行任何一行代码修改都会触发 liveView 重新构建。如果你不想维护一个全局对象,可以尝试使用我的这种做法通过全局与用户进行交互:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import UIKit
    import PlaygroundSupport

    public var brocadeType: PJHomeViewController.BrocadeType = .normal
    public var brocadeBackgroundColor: UIColor = UIColor.bgColor()

    public func start(_ gameType: PJHomeViewController.GameType) {
    let vc = PJHomeViewController()
    vc.brocadeType = brocadeType
    vc.gameType = gameType
    vc.brocadeBackgroundColor = brocadeBackgroundColor
    PlaygroundPage.current.liveView = vc
    }
  3. 一个 Page 都会有一个独立的 liveView,多个 page 之间互不干扰,所以尽量让多个 page 之间不要产生关联。
  4. 每个 page 的 liveView 的 frame 很怪异,不推荐用 UIScreen.main.bounds 的方式获取屏幕宽高,因为这会获取到当前 Playground 所运行平台的屏幕宽高,在 Mac 上这屏幕的宽高就很酸爽了,记住这只是个 demo,屏幕适配的时机不适合现在。

Submission

一切完成后,就要开始写文案了,今年的 WWDC Scholarship 可以写以下文案:

  • 介绍你自己是怎么学习计算机的;
  • 介绍这个 Playground 中运用到哪些技术以及功能点;
  • 如果你有自己的独立开发的 App,可以介绍一下;
  • 如果你还有什么想让 Apple 知道的,可以说一下;
  • 超过十八岁可以把自己的简历附上去。

我非常的后悔自己在大学四年中没有开发出任何一款完全属于自己的 App,我昨天有点微微难受。希望接下来的毕设能够给自己一些安慰吧~

简历我没有附,虽然 Apple 明确表示简历不会影响最终的评分,但我还是不想。

结束了

当我按下 Submit 时,整个人完全放松下来了……

世间还有这么多美好的事物,我为什么要和自己这么过不去?整整一周的时间整个人的心情都是紧张的,前两三天的时候一直在担心东西做不完,做的不好,还有多少功能没有实现。但到按下 Submit 后,管它还有什么没做完,也不管最后到底是能不能被选上,只想远离这个位置。去呼吸新鲜空气,去看这蓝天白云,去看这纷繁多彩。

原本打算蒙头盖被睡一觉,但不知为何心情又开始剧烈激动起来,翻来覆去睡不着,买了张《波西米亚狂想曲》的电影票,在电影院里一个人吃着鸡米花,享受着这视听盛宴,真好!

Playground 工程:https://github.com/windstormeye/WWDC19_brocadeOfLiNationality
App 工程(你可以听到独特的黎族歌曲):https://github.com/windstormeye/brocadeOfLiNationality