iPlayground 2018

LINE 期許工程師們互相交流工作遇到的挑戰與技術架構,內部時常有夥伴跟大家分享新的所學所見。
也很鼓勵工程師們參與外部社群與研討會,之前有補助同仁去參加 Selenium ConferenceAgile + DevOps EASTGoogle I/OWWDC 等技術研討會。
LINE 也歡迎開發者社群來公司內進行技術交流,之前也有在公司內主辦過 Test CornerLINE Developer Meetup 等等社群 Meetup。

這次正好有機會參與到台灣發起的第一屆 iOS 研討會 iPlayground, 學習到不少新知,也與很多開發者進行技術交流。
接下來跟大家分享參加活動的收穫。  

iPlayground Introduction

iPlayground 是在台北舉辦的 Apple 軟體開發相關的研討會,名字來自於 Xcode 內建的開發工具 Playground,議程涵蓋 iOS APP 開發、Mac APP 開發與軟體測試。
主辦方鼓勵除了開發者以外,設計師、QA、PM 都能來一起交換想法,分享所學。此活動緣起於台灣有一群⼯程師去東京參加 iOSDC 2017,看到⽇本當地開發社群的蓬勃活力,兼具深度、廣度的諸多講題及趣味的舉辦⽅式,其中有許多台灣社群可以學習的地方。
相形之下,在台灣舉辦的專屬 iOS 開發的研討會仍不多,上述原因促使他們決定在台北辦⼀場 iOS 開發專⾨的研討會。

iPlayground 2018

iPlayground 2018 在台大管理學院系館舉辦,舉辦的日期為10月20日(六)~10月21日(日)為期一天半的時間。

活動相關資訊: iPlayground 2018

iPlayground 會場立牌
第一天議程
第二天議程

活動議程分享

那些年被蘋果 Ban 掉的 API

第一天的 Keynote 請到業界知名資深大神 Zonble 來做開場,主題是那些年被蘋果 Ban 掉的 API。
投影片連結:https://www.slideshare.net/zonble/ban-api

演講中 Zonble 提到 Apple 對於使用者的隱私保護非常重視。
事實上若沒有好好保護使用者隱私,洩漏了使用者身分,除了廣告行銷公司可以鎖定使用者,犯罪機關或情治機關也可以鎖定使用者。
而透過使用者有安裝使用的 App,甚至可以對這位使用者作出 Profiling。例如:

  • 有安裝航空公司或旅館的 App:表示可能常旅行。
  • 有安裝雙北公車 App:表示可能是台北人
  • 有安裝 Dcard:可能是大學生
  • 有安裝 BabyHi、萌寶日記:可能有小孩

在鎖定使用者,以及有了使用者的 Profile 後,除了廣告公司能夠精準行銷,
而若是還有了 GPS 的紀錄之後,犯罪集團也能鎖定那些可能較有錢,常出國,甚至還有小孩的使用者。

所以 Apple 在 iOS 作業系統每一代更新時,將一些有可能透露使用者身分及洩漏隱私的 API 逐步關閉。
對於使用者來說,只要不去對手機作出越獄破解,iOS 作業系統相對來說,提供使用者更完善的保護。

以下會對這些被封閉的 API 做出介紹,以及他們被封閉的原因。

UDID

  • 在同一台手機上,每個 App 都可以取得相同的裝置 ID,也可以比對不同網路服務中的不同帳號,實際上是否為同一個人
  • Deprecated in iOS 5,在 iOS 6 之後則完全不能被呼叫

而在禁止使用 UUID 後,Apple 官方的也有提供對於用戶更友善的 UUID 替代品

IDFV

  • identifierForVendor,只有屬於同一個開發商的 App 之前可以辨識用,在所以相同 prefix App 都被移除後,重新安裝 App 取得的 IDFV 會改變。
  • 若要解決這個 IDFV 可能改變的問題,講者有提供目前一個可能的作法,將 ID 寫入 iOS 系統的 Keychain 中,Keychain 資料不會因刪除 App 而改變,能做到部分的替代效果。

IDFA 

  • advertisingIdentifier ,讓廣告商可以追蹤用戶,但用戶也可以在設定中限制廣告追蹤

在禁用 UDID 之後,雖然官方提供的這兩種替代方式,但已無法百分百保證能追蹤用戶身分。

有些 App 的商業模式只能讓服務在有限數量的裝置上使用,此時沒有了 UDID 就無法有效地追蹤用戶使用的裝置數量。

當然還是有些服務希望能做到用戶追蹤,所以在舊版本的 iOS 也曾存在以下幾種可能的方式。

網路卡卡號:

  • 雖然 Mac Address 也可以拿來做唯一的裝置識別,但在 iOS 7 之後也已經被 Apple 封鎖拿不到真正的 Mac Address

UIPasteBoard

  • 看似無關的剪貼簿,也可用來作為追蹤用戶的工具。在不同 App 間做複製貼上的剪貼簿,若使用 +pasteboardWithName:create: 來建立剪貼簿,就可以變成跨 App 互相溝通的 ID。
    但在 iOS 6 以後,只有同屬一個 App Group 的 App 才能共享資料。

SafariViewController

另一個可能做跨 App 溝通偵測用戶的方式為將能代表用戶的 Cookie 放入系統的 SafariViewController之中,不過這個方法也在 iOS 11 之後被限制,停止 App 之間共用 Session。

canOpenURL

查詢 user 手機中裝了什麼 App,就能做到對 user 的 profiling。
Apple 在 iOS9 之後限制 canOpenURL: 這個方法能查詢的 App 數量上限為 50。
而且開發者必須在送審 App 的時候,在固定的 Info.plist 中,LSApplicationQueriesSchemes 中,載明可能要查詢的 App Scheme 清單。 

講者另外也提到一些可能洩漏 App 使用/安裝情況的管道

  • 使用某些 Social 登入或密碼存取的 App,則這些服務的提供者也會有用戶的 App 使用情況
  • iOS 第三方的鍵盤提供的語音輸入,處理能知道是哪個 App 在呼叫之外,也可能從用戶打了什麼字而取得更多資訊。

DeviceCheck

  • https://developer.apple.com/documentation/devicecheck
  • 除了上面講者提到的部分,這裡補充一個 iOS 11 之後的合法方法,對於提供免費試用期的 App 而言,可防止使用者移除 App 再重新安裝 App 來再次取得免費試用的行為。
  • Apple 官方提供 DeviceCheck API,可讓 App 開發者有一個 2 bit 共四種狀態的儲存器可用。在移除 App 重裝之後,仍不會被刪除。
  • 能拿來辨別該裝置是否有試用過,但又不能因此來追蹤使用者。

結論

總結而言,講者用回顧歷史的方式,將蘋果對於個資保護的歷程一一闡述。

身為一個用戶,跟著升級 iOS 版本且不做越獄動作,會得到系統根本上的保護。而知道哪些管道可能會有個資洩漏的問題,就可以在使用習慣下多加提防。

而身為 App 的開發者或服務提供者,講者也提醒若不當搜集個資是違反蘋果跟用戶的期待,甚至是 GDPR 的規範的。

iOS Redux 番外篇

投影片連結:https://speakerdeck.com/ejameslin/redux-fan-wai-pian

這個 Session 是 JJ Lin 所準備的 Lighting Talk。
在議程當天,沒注意到時間的限制,後面的投影片無法全部講完。很開心在這篇文章中,能把想傳達的概念完整論述一次。

Redux Intro

Redux (https://redux.js.org/) 在 2015 年建立,由 Flux 演變而來。Redux 架構設計上,讓程式的多個狀態變化對開發者來說更容易管理。
Redux 是個有眾多人使用的 Open Source JavaScript Library,但在 iOS 開發上應用的情況仍未普遍,所以想說把這個架構的概念傳達給聽眾,並分享開發經驗。

source: https://www.youtube.com/watch?v=nYkdrAPrdcw&t=11m40s

在開發過程中,很常遇見的問題是在使用傳統 MVC 的架構中,Model 和 View 之間的互動影響複雜。
Model 的改變影響 View,View 的變化又反過來影響 Model,進而可能形成一個 Loop。
也許可以使用較複雜的架構如 MVVM / MVP / VIPER,但這些架構主要著眼於避免龐大的單一頁面 Massive View Controller,將 UI 顯示 / 資料處理 / Rourting 的角色拆開。
而在龐大的程式及複雜的狀態下,甚至是跨頁面情況下,仍沒有一套好的架構來規範資料之間應該如何同步及傳遞。

source: https://github.com/ReSwift/ReSwift

而 Redux 架構設計上,有著嚴格的單向資料流,讓程式的資料及邏輯更容易管理。

當 View 接收到使用者指令後,發出 Action 到 Store。
Action 中含有被執行的 Action 的類型,以及附帶的資料。
Store 負責儲存 State,接收 Action,dispatch Action 到 Reducers 來改變 State,以及將被改變的 State 通知 Listers。
Reducer 是 Pure function,負責對傳入的 Action 及 State,修改並傳出更新後的 State。

Redux 三大原則

  • Single source of truth
    整個程式的 State,被儲存在唯一的 Store 中。
    在不同頁面的時候,State 因為只有一份,就不會發生不同步的狀況。
    在設計這個 State 時,建議對要儲存的物件做好正規化,才不會同份物件在 State 中被複製成多份,彼此不互相同步。
  • State 是唯讀的
    唯一能改變 State 的途徑是發出 Action,並藉由 Reducer 來傳出新的 State。
    State 經過了 Reducers 的改變後,最後才被更新至 Store 中,此時才需要通知 Lister 或各頁面進行畫面重繪,避免 State 改變到一半卻需要被取用的問題。
  • Changes are made with pure functions
    Reducer 是 Functional Programming 中的 Pure function,不能產生 Side Effect 來改變 Global / instance variables。
    結合上面 State 是唯獨的特性,避免 Race condition。

在以往的開發經驗中,相比於直接對應 MVC,我們是使用 MVVM 的架構,加上 Redux 而成。
這樣做的好處是 ViewController 仍只需負責顯示畫面,並作好 ViewModel 及 View 的 Binding。
而較為複雜的邏輯或網路請求仍由 ViewModel 負責,並將取得的新資料 Dispatch 給 Store,最後由 Reducer 傳出新的 State。

Reducer & State 設計需注意

而在開發的經驗上,Reducer 及 State 的設計是個需要注意的部分。
讓我們來看看 RxSwift 所設計的 Reducer 的 Function signature

Reducer 的 input 為 Action 以及 State,而傳出一個新的 State,且必須遵守之前提到的 pure function 原則。

此時可以參考 ReSwift 的設計
var state = state ?? AppState()

因為 Swift struct value type 的特性,基本上 ReSwift 是對 state 物件做一個 copy 的動作,並去改變這個 copied state。

在 Swift 上,State 的設計可以使用 Class 或 Struct 兩個選擇設計。

Swift Struct copy 有著非常高的效能,因為 copy-on-write 的特性

Collections defined by the standard library like arrays, dictionaries, and strings use an optimization to reduce the performance cost of copying. 
Instead of making a copy immediately, these collections share the memory where the elements are stored between the original instance and any copies. 
If one of the copies of the collection is modified, the elements are copied just before the modification. The behavior you see in your code is always as if a copy took place immediately.

copy 只發生在即將被改變的物件上,而絕大多數時候,每次改變的 state 只是整個 App state 的一小部分而已。所以使用 struct 是個非常好的選擇。

而在選擇 Class 實現的情況下,無論是在 Swift 或 ObjC 上,藉由 Archive / UnArchive 或 Encode / Decode 物件來達到 True Deep Copy 是非常耗時的,在資料變多的情況下,有過 copy state 會達到數秒鐘的經驗。
此時可選擇實作 NSCopying 的 Protocol,在 immutable 的 property 做 copy 時就可以使用 Shallow Copy 來加速。

結語

在設計 State 的時候,首選自然是使用 Swift 的 Struct,才可做到快速 State copy 的功能。

在 State 逐漸擴大,遇到效能問題的時候,必須做好釋放一些用不到或過時的資料。
架構設計上,將完全無關的資料切分成 sub-state 來儲存也是可行的方向。

Swift 也能訓練 Machine Learning 模型?Create ML 實戰

這個議程是資深 iOS 程式設計師張景隆大大所帶來的,介紹 WWDC 2018 新發表的 CreateML 的實作方法。

投影片連結:https://github.com/Appletone/iPlayground-CreateML/blob/master/CreateML.pdf

時空背景

2018 WWDC 的亮點之一,就是在 Xcode 10, macOS Mojave 上可以之間用 Swift 來建置 iOS 的 machine learning model。在這之前,產生 iOS 上可用的 ML model 必須用 Python 進行 data/train/evaluate 的工作,今年卻可以完全用 Swift 來完成了,這就是 CreateML。

Image Classifier

感覺有點驚奇,只要 import CreateMLUI,利用 MLImageClassifierBuilder 便能直接在 Swift Playground 裡以互動的方式建置圖片資料來產生 image classifier。而且還能用 crop,blur,rotate,expose,noise,flip,等工具,讓有限數量的圖片擴增不少 data,提升 classifier 的準確性。

不少 machine learning 的課程會帶著你做 image classifier,但用 Swift Playground 可以如此輕鬆建造一個 model (還帶有可愛小動畫)仍然令人印象深刻。把資料夾命名完成,放進有代表性的圖片,熱騰騰的image classifier 馬上就訓練出來了!值得一提的是,image classifier 只能解選擇題,在不同的答案之間猜測一張圖比較像哪個,但不適合拿來判斷是非題,判斷一張圖是不是某樣東西。

Text Classifier

CreateML 現在也能做文章內容的分類,也支援中文文字。只要把不同的文章做分類標籤,可以用 CSV 或 JSON 各式分類,就可以訓練並使用了,也是非常簡單方便。訓練完的模型,可以直接拉進 Xcode Project,就能在 App 裡面使用了。

Tabular Data

這部分利用台北市 Open Data 的資料,台北捷運進出站的即時統計資料來做捷運的即時人數預測。利用 MLDataTable 再餵入捷運人數的 CSV 檔案,其中 80% 做 training,20% 做 validation,幾行程式碼便產生可以直接拉進 Xcode 的 CoreML model。同時分享了一些資料格式上的需求,比如 column 無法接受中文這樣的限制。

Model Accuracy

如何喔判斷一組機器學習模型預測準不準? 機器學習常用的兩個重要的指數有 Confusion Matrix 和 Classification Report。

Confusion Matrix

Confusion Matrix 又稱作 Error Matrix,裡面四個值,分別紀錄正面與反面預測的正確性。演講中用預測是否會下雨的例子,生動解說 confusion matrix 的直觀概念。

Recall, Precision, F1 Score

另外用撒網捕魚,獲得魚,蝦,螃蟹的例子說明 Recall (召回率),Precision(準確率),和 F 值的意義。如同我們希望機器學習模型預測的結果都是準確的,我們當然希望捕到的都是魚。準確率就是捕到的漁獲中魚所佔的百分比。召回率就是一池子的魚當中,我們究竟捕獲了幾條。另外,我們也得知 F1 Score 應高於 80% 才是有價值的預測。

短短三十分鐘,介紹了如何輕鬆利用蘋果提供的 API,做出實用的 machine learning 工具,也講解如何判斷訓練出來模型的品質,真是獲益良多。

Test Code、Test UI、Test EveryThing !!!

這個議程是由 iOS@Taipei 的 AKI YU 所主講,內容是分享 iOS 測試相關的事情,議程中 AKI 跟我們分享為什麼要寫測試,以及在 iOS 有哪些好用的測試工具可以幫助我們進行測試。

投影片連結: https://github.com/ios-taipei/DataBed/raw/master/iPlayground.key

User Story

一開始講者給我們一個情境,老闆希望工程團隊在專案裡實作一個登入的功能,因為實作登入功能需要釐清實作細節,所以工程師詢問老闆此登入功能「要不要single sign-on? 要不要JWT?」,但老闆可能不是技術背景的人,所以無法理解工程師的問題,於是老闆找來一位 PM 當作他與工程團隊的溝通橋樑以期專案能順利進行,但這種工程師與老闆中間多一層溝通層的開發方式很常在開發接近尾聲的時候,因為一些細節沒有釐清到,而導致開發的成品跟老闆的期待有所落差,為了彌補這種溝通的問題,講者提到可以在產品開發進行前撰寫 User Story,將可能的使用情境與其預期結果先寫下來,範例如下:

情境:

As an User, I want a login feature.
When I login, 
So That I Can see Main Page.

User Story:

Scenario: User login feature.
Given an exist User
When User login, 
Then User Can see Main Page.

以老闆想做的登入功能為例,我們可以清楚的了解到老闆希望登入之後直接將畫面導到首頁,而非其他頁面,User story 除了可以彌補溝通問題外,其描述的方式還很適合工程師將其轉換成程式碼,除此之外,User Story 也使自動化測試人員能在產品開發初期就開始定義何謂符合需求的程式、撰寫自動化測試,相對於以往須等工程師開發完才能進行測試,User Story 也加速了開發流程。

為何要寫自動化測試?

接下來講者提到,很多人可能不知道測試人員是相當忙碌的,他用一個清晰易懂的例子說明測試人員的工作量:

在專案一開始時,專案裡只有一個功能 A,測試人員只需要驗證功能 A 是否符合需求,故測試的時間成本為 1,接下來專案進行到第二期,加了功能 B,此時測試人員須測試 A, B, (A + B) 是否符合需求,故測試的時間成本為 3,以此類推專案第三期須涵蓋的測試項目為 A, B, C, (A + B), (A + C), (B + C), (A + B + C),故測試的時間成本為 7。按照這樣進行下去,當專案內的功能越來越多,在有限的人力與時間前提下,測試人員終將面臨無法涵蓋所有測試項目的困境,因此需導入自動化測試來降低測試人員的工作負擔並提高產品品質。

Unit Test

在開始講 Unit Test 之前,講者問我們一個問題「不做單元測試可以嗎?」,隨即他舉了一個例子,下方是我根據講者的例子寫了一個類似的範例程式:

上述是一個判斷某字串是否為整數的 function 在大部分的情況下是正確的,但是當輸入的整數字串有點大時…

執行到 testIsIntegerWithBigNumber() 這筆測試時,會顯示測試失敗,因為 isInteger(aStr: String) 沒有處理大數的情境。如果有撰寫測試,我們就有機會事先發現 isInteger(aStr: String) 無法處理大數,進而補強這個情境。

接下來講者又用一個例子讓我們思考寫單元測試的好處。

有一天某位工程師接到要寫一個驗證 email 是否合法的程式,他從網路上找到一段用來驗證使用者輸入的 email 是否合法的 regular expression,此時他是否要寫單元測試? 講者沒講他心中的答案,留著讓我們自己思考。

我認為是需要撰寫單元測試來驗證這段 regular expression 的,透過撰寫測試可以驗證這段網路上查找到的程式碼是否符合我們的需求,而且專案內的其他人也能透過閱讀單元測試的程式碼來了解這段 regular expression 的用途。

UI Test

除了單元測試以外,測試人員也可以撰寫 UI Test 來確保某些畫面的操作是符合預期的,例如先前提到的登入功能,測試人員就可以寫 UI Test 來模擬使用者輸入帳號密碼並按下登入按鈕,以測試登入功能是否運作正常,但要進行 UI 測試,除了直接撰寫 UI 測試的程式碼以外,還有其他方式,例如:

按下左下角的紅色圓形按鈕後可以錄製測試人員的操作,再按一次紅色按鈕即可完成錄製,之後就能使用這段錄製操作所產生的程式碼重複進行測試。

Apple Document: Recording UI Tests

最後,講者提到除了 Unit Test 跟 UI Test 以外,他也時常透過其他工具來驗證程式碼的正確性,他會透過 Network Link Conditioner 來觀察程式在不同的網路環境下是否會有問題,甚至他會直接去查看封包內容是否符合他的預期。

這個議程讓我瞭解到「測試」其實並不侷限於 Unit Test 或 UI Test 的撰寫,而它其實是一種精神,一種想要確保程式正確性的精神,為了確保程式的正確性,開發者跟測試人員會找各種方式、使用各種工具來驗證產品的行為符合預期,我也從這個議程學習到很多以前不知道的測試方法、測試工具,是個有趣又收穫滿滿的議程。

Core Animation vs. SpriteKit

這個議程是由 Luke Wu 所主講,內容是分享 iOS 裡有一個好用的 framework 叫做 SpriteKit,議程中 Luke 拿了 iOS 裡另一個常被用來做動畫的 framework,名為 Core Animation 來跟 SpriteKit 作比較,我會將他的投影片內容整理成表格,方便大家看懂兩者的差異。

投影片連結: https://www.slideshare.net/ssusera2163f/core-animation-vs-spritekit

Core Animation 跟 SpriteKit 的比較

FrameworkCore AnimationSpriteKit
作業系統支援iOS / macOS /tvOSiOS / macOS /tvOS/ watchOS
Related ClassUIView, CALayer, CAAnimationSKView, SKScene, SKNode, SKAction
暫停動畫無法,iOS 10 推出了 UIViewPropertyAnimator才開始能暫停動畫可以
動畫過程中,回應使用者點擊事件無法,iOS 10 推出了 UIViewPropertyAnimator才開始能在動畫執行時偵測點擊事件可以
座標系UIKit 往右往下為正SpriteKit 往右往上為正,跟我們習慣用的 XY 座標軸一樣
Physics Simulate Engine

從上述表格我們可以看到,在作業系統支援的部分,SpriteKit 多支援了 watchOS,SpriteKit 也支援暫停動畫與動畫過程中偵測點擊事件,Core Animation 則是從 iOS 10 之後才開始支援,座標系的部分 SpriteKit 跟數學中所學的 x 軸與 y 軸一致,皆為往右往上為正,最後一點,SpriteKit 支援 Physics Simulate Engine 方便開發者能做到一些較複雜的動畫。

SpriteKit Related Class

SKNode

  • SKSpriteNode 是繼承自 SKNode
  • SKNode 是 SpriteKit 裡最基礎的 class
  • 所有對 SKNode 的操作,都必須在 Main Thread 執行
  • 幾個常用的 SKNode Subclass
    1. SKSpriteNode – 圖片
    2. SKShapeNode – 各種形狀
    3. SKVideoNode – 影片
    4. SKAudioNode – 聲音

SKAction

  • 用來設定 SKNode 能夠執行的動畫效果
  • 會有一系列 SKAction 的 class method 可以建立動畫效果,但一但初始化 SKAction 後,即無法在調整內容,只能修改 duration 跟 speed
  • 還多了一個 sequence 的 action,讓動畫依序執行

SKView

  • 繼承自 UIView
  • 負責 Render 畫面,需要外部給一個 SKScene 物件
  • 如果你想要有轉場效果,可以透過以下 method
  • SKViewDelegate 可以控制 SKView 要不要 render 畫面

SKScene

  • 一樣是繼承自 SKNode
  • Root node for all Sprite Kit object display in a view
  • 負責計算與追蹤底下 node 的狀態與位置
  • Life Cycle

Why SpriteKit?

  • SKView 多了 Physics Simulate Engine,相對於 Core Animation 而言,SpriteKit 可以相對容易地做到物理效果,例如: 物體的碰撞與反彈效果
  • SpriteKit 的畫面是由 SKView 這個 class 所負責,而 SKView 繼承自我們所熟悉的 UIView,這讓 SpriteKit 可以很順利的加入以 UIKit 為基礎建立的 project
  • SpriteKit 是原生的 framework,可以直接用 swift 寫,不需為了使用物理引擎而導入其他 framework 或學其它語言

Physics Simulate Engine

碰撞或是反彈這種物理效果,在 Core Animation 很難實現,但在 SpriteKit 卻相對容易。

每個 SKNode 都持有一個 SKPhysicsBody 的 property,只要修改這個 property 就可以改變這個 SKNode 的物理行為。

SKPhysicsBody Property

  • 摩擦力: friction
  • 反彈係數: restitution
  • 速度上的自然阻力:linearDamping
  • 旋轉上的自然阻力:angularDamping
  • 質量:mass,用於物體碰撞時,計算碰撞後速度大小
  • 決定兩個物體要不要互相碰撞:
    1. categoryBitMask
    2. collisionBitMask
    3. 只要自身的 collisionBitMask 與對方的 categoryBitMask 做 Bitwise OR ( | ) 運算後不為 0,代表會有碰撞效果出現

結語

這次的會議內容精彩不在話下,更珍貴的是認識許多同行與 iOS 同好。不管是技術交流或職場上的經驗分享,都獲得很多。很高興能見到在台灣辦一場 iOS/macOS 專屬的 conference, 聽中文內容的演講果然特別溫暖,少了機票與飯店的費用,更使得參與 iPlayground 成為任何 iOS developer 心目中的 no-brainer (當然要參加啊!)。希望主辦單位覺得辛苦有代價,明年也請一定繼續辦!

現在的開發者 conference 其實都會把演講內容錄影並分享到網路上,錯過了還可以上網看到,iPlayground 也不例外(https://medium.com/iplayground/iplayground-2018-slides-11587ebdddc5)。但更珍貴的是與會同行和朋友們之間的討論分享與腦力震盪,不僅帶來工作上的新靈感,也互相感應出開發的熱情與動力。很感謝公司鼓勵大家參與這樣的開發會議,不管是對工作上或自己的職涯發展都有非常有幫助。

相關職缺

你也是 iOS 的開發者嗎? LINE iOS  團隊正在徵才,歡迎透過以下方式加入我們。