เจาะลึกเบื้องหลัง Firebase Code Battle ฝั่ง iOS กับการพัฒนาแอปแชทภายใน 45 นาที

Kittisak Phetrungnapha
iTopStory
Published in
8 min readMar 4, 2017

--

ก็เป็นอันผ่านไปเรียบร้อยแล้วนะครับ สำหรับช่วง Firebase Code Battle ที่เป็น highlight สุดท้ายในงาน Firebase Dev Day เมื่อวันที่ 25 กุมภาพันธ์ 2560 ที่ผ่านมา บทความนี้ก็จะเป็นการอธิบายถึงสิ่งที่ต้องทำ และวิธีที่ทำให้มันเกิดขึ้นมาได้ภายใน 45 นาทีครับ เอาล่ะ มาลุยกันเลย (ช่างกล้ามาก ที่เอาหน้าตัวเองขึ้นโชว์ก่อนเลย 555)

สิ่งที่ต้องทำ

  1. Prerequisite
  2. Sign in to Firebase with Google account.
  3. Chat UI
  4. Receive text and image.
  5. Send text
  6. Send image
  7. Offline apps
  8. Sign out

Prerequisite

ก่อนอื่นก็ต้องเตรียมโปรเจคของเราให้พร้อมก่อน เอาแบบเร็วๆ เลยละกันเนอะ

  1. สร้างโปรเจคใหม่จาก Xcode 8.x (Swift 3) ขึ้นมา แล้วติดตั้ง CocoaPods ให้เรียบร้อย
  2. ผูกโปรเจคที่เพิ่งสร้างขึ้นมาเข้ากับ Firebase Console ซะ ลง Firebase กับ Google Sign-In SDK ด้วย CocoaPods ให้เรียบร้อย
  3. ก้อป GoogleService-Info.plist ไปไว้ในโปรเจคของเรา
  4. จาก Firebase Console ไปที่เมนู Authentication -> Sign-In Method แล้วก็จัดการ enable Google Sign-In provider ให้เรียบร้อย
  5. กลับมาที่โปรเจคของเรา ให้เปิด GoogleService-Info.plist ขึ้นมา แล้วก้อปค่า REVERSED_CLIENT_ID เก็บไว้
  6. เปิด Info.plist แบบ Source Code แล้วแปะโค้ดด้านล่างลงไป โดยให้ใช้ REVERSED_CLIENT_ID ที่ได้จากขั้นตอนก่อนหน้านะครับ
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>YOUR_REVERSED_CLIENT_ID</string>
</array>
</dict>
</array>

เพิ่มเติม: ควรติดตั้ง libraries เหล่านี้ไว้ก่อนเลย เดี๋ยวต้องใช้ครับ

pod 'Firebase/Core'
pod 'Firebase/Storage'
pod 'Firebase/Auth'
pod 'Firebase/Database'
pod 'JSQMessagesViewController'
pod 'GoogleSignIn'
pod 'Kingfisher'

Sign in to Firebase with Google account

สาเหตุที่เลือกใช้ Google Sign-In SDK ไม่มีอะไรมากครับ มัน “ง่าย” ที่สุดแล้ว สำหรับทั้ง iOS, Android, Web บวกกับ Firebase เป็นของ Google ด้วย (อันนี้ไม่เกี่ยววว ถถถ)

ไปที่ AppDelegate.swift แล้วจัดการ import Firebase กับ GoogleSignIn เข้ามา

import Firebase
import GoogleSignIn

ต่อมาก็คอนฟิกค่าเริ่มต้นให้กับ Firebase และ Google ใน didFinishLaunchingWithOptions

FIRApp.configure()
FIRDatabase.database().persistenceEnabled = true
GIDSignIn.sharedInstance().clientID = FIRApp.defaultApp()?.options.clientID
GIDSignIn.sharedInstance().delegate = self

เพิ่ม callback url ให้กับ AppDelegate เพื่อบอกว่า เมื่อ authen ที่ Google เสร็จแล้ว ให้วิ่งกลับเข้ามาที่แอปเรานะ

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
return GIDSignIn.sharedInstance().handle(url,
sourceApplication:options[UIApplicationOpenURLOptionsKey.sourceApplication] as? String,
annotation: [:])
}

ต่อมาให้เราสร้าง extension เพิ่มให้กับ AppDelegate ของเรา เพื่อไว้รอรับ idToken กับ accessToken จาก Google

โดยเมื่อเราได้ idToken และ accessToken จาก Google มาแล้ว ก็จัดการสร้าง FIRGoogleAuthProvider credential instance ขึ้นมา หลังจากนั้นก็เรียก FIRAuth.auth()?.signIn(…) แล้วส่ง credential ที่เพิ่งสร้างเข้าไปที่ Firebase

ต่อมาให้ทำการสร้างหน้าเปล่าๆ ขึ้นมา ใช้ปุ่ม Sign-In ที่ Google เตรียมไว้ให้ใน SDK เลยก็ได้ โดยลากปุ่มไว้ใน ViewController ใน Storyboard จากนั้นให้ Button ของเรา subclass ไปที่ GIDSignInButton ดังภาพด้านล่าง

หลังจากนั้นไปที่ LoginViewController.swift ของเรา แล้วก้อปโค้ดด้านล่างนี้

อธิบายสั้นๆ ก็คือ สร้าง authListener เอาไว้สำหรับเช็คสถานะของ Firebase User ว่าได้ทำการล้อกอินเข้ามาแล้วหรือยัง ถ้าล้อกอินแล้วก็กระโดดไปหน้าต่อไปได้เลย (หน้า chat) โดยเราจะ register มันไว้ใน viewWillAppear และอย่าลืม unregister มันออกใน viewWillDisAppear ด้วยนะครับ ทีนี้มาลองทดสอบกันเลย รันโปรเจคแล้วกดปุ่ม Sign In แล้วก็ authen ด้วย Gmail ให้เรียบร้อย

หลังจากนั้นกลับมาดูใน Xcode console ของเรา ซึ่งควรจะเห็น object user ถูกปริ้นออกมาแล้ว นั่นแปลว่าเราล้อกอินเข้า Firebase ด้วย Google เรียบร้อย ซึ่งสามารถเข้าไปเช็คใน Firebase Console เมนู Authentication แถบ Users ได้ จะมี email ของเราโผล่เข้ามาแล้ว

Chat UI

สำหรับการทำหน้า Chat นั้น ต้องขอบอกว่าเป็นอะไรที่โหดร้ายทารุณมากสุดๆ เพราะมันมีเรื่องที่ต้องให้คิดโน่นนี่เต็มไปหมด ต้องคำนวณความสูงของแต่ละ cell ตามข้อความที่จะส่งออกไปและที่จะได้รับมาจาก Database ไหนจะเรื่อง avatar profile, chat bubble, send text, send image, send video บลาๆๆๆ กว่าจะทำให้สมบูรณ์คงเสียเวลาหลายอาทิตย์เลยทีเดียว

แต่ถือว่าเป็นความโชคดี (ที่สุด) เลยก็ว่าได้ เพราะมีคนทำ library ระดับเทพที่ support เรื่องที่ว่าทั้งหมดไว้แล้ว มันก็คือ JSQMessagesViewController นั่นเอง ซึ่งถ้านำเจ้านี่มาบวกกับ Realtime Database ที่จะกล่าวในหัวข้อถัดๆ ไป ก็เหมือนเสกแอปแชทขึ้นมาได้ภายในระยะเวลาจำกัดเลยทีเดียว กราบคนทำจริงๆ เก่งมากๆ ครับ

ก่อนอื่นก็ให้เราสร้าง ChatViewController.swift ขึ้นมา แล้ว subclass มันด้วย JSQMessagesViewController ก่อนเลย จากนั้นก็ไปแก้โค้ดที่หน้า Login ให้เปลี่ยน root view มาที่หน้าแชทแทนในกรณีที่เคยล้อกอินเข้ามาแล้ว ดังนี้

โดยจะมีการใช้ uid จาก Firebase ที่ได้ตอนล้อกอินมาเซ็ทเป็น sender id สำหรับผู้ส่งข้อความ แล้วก็ใช้ sender display name เป็น “iOS” ซะเลย

ในส่วนโค้ดของ JSQMessagesViewController ผมจะไม่อธิบายลงลึกมากนะครับ เพราะสามารถดูได้จากตัวอย่าง Example project ของเขา แล้วก็ยังมีบทความอีกมากมายที่สอนใช้ library ตัวนี้ ถ้าอธิบายด้วย บทความนี้น่าจะยาวเกินไปนะครับ เอาเป็นว่าผมจะแปะโค้ดที่เป็นโครงสร้างหน้า chat ให้ตามด้านล่างเลยละกัน แล้วก็เขียน TODO ไว้ให้ แล้วเดี๋ยวค่อยๆ มาเติมทีละส่วนไปพร้อมๆ กันครับ

เอ้อ แล้วผมใช้ Kingfisher ในการโหลดรูปจาก url แล้วก็ทำ cache ด้วยนะครับ ไม่งั้นตอนเปิดแอปมาต้องโหลดรูปใหม่ทุกครั้ง เปลืองเน็ทมากครับ ผมสร้างคลาส ImageDownloadManager สำหรับให้เรียกใช้งาน Kingfisher ง่ายๆ มาด้วย ไปก้อปมาใส่โปรเจคได้เลยครับ หลังจากนั้นก็ลองรันทดสอบแอปดู มันควรจะได้หน้าตาแชทเปล่าๆ คล้ายๆ แบบนี้ออกมาาาาา

เอาล่ะ ตอนนี้เราได้ chat ui มาแล้ว จะเห็นว่ามีช่องสำหรับใส่ข้อความ แตะแล้วพิมพ์ได้ มีปุ่ม Send ไว้กดส่ง และปุ่ม attach file สำหรับใช้ส่งรูปครับ เอ้อ แล้วก็อย่าลืมใส่ permissions สองตัวนี้ใน Info.plist ด้วยล่ะ ไม่งั้นตอนจะส่งรูปด้วย iOS 10 แล้วแอปจะเด้งนะจ๊ะ

<key>NSCameraUsageDescription</key>
<string>We access your camera to attach an image to messages</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We access your photos to attach an image to messages</string>

Receive text and image

เอาล่ะ ต่อไปก็มาถึงขั้นตอนดึง text กับ image จาก Realtime Database มาแสดงแล้ว แต่ก่อนอื่นต้องขออธิบายโครงสร้าง database ซึ่งเป็น NoSQL ที่ใช้ภายในโปรเจคนี้เสียก่อน ว่าแล้วก็ไปเปิด Firebase Console แล้วเข้าเมนู Database กันได้เลย

โดยเราจะมี root อยู่อันนึง ชื่อว่า fir-devday จากนั้นก็จะมี child ที่ชื่อว่า messages (ส่วน list ไม่ต้องสนใจ สงสัย Android GDE แอบมาสร้างไว้ หึหึหึ) โดยภายใน messages นั้นก็จะเก็บ object (ต่อไปนี้จะเรียกว่า snapshot) ของแต่ละ message ไว้ ที่มี id ซึ่งถูก generated ออกมาเป็น key unique value หลังจากนั้นในแต่ละ snapshot ก็จะมี fields อีกต่างๆ ดังต่อไปนี้

  1. avatar ไว้แสดงรูปโปรไฟล์ของผู้ส่งและตัวเราในหน้า chat โดยเก็บเอา image url ที่ได้จาก Google Sign-In มาใช้ซะเลย
  2. data ข้อความใน chat หรือ image url (ในกรณีที่เป็นการส่งรูปภาพ)
  3. senderId เป็น unique id ของผู้ที่ส่ง message อันนี้มา
  4. type ประเภทของข้อความ มี “text” กับ “image”
  5. username ชื่อของผู้ส่งที่เอาไว้แสดงในหน้า chat

โอเค ตอนนี้เราเข้าใจโครงสร้างคร่าวๆ ของ database กันแล้ว ต่อไปก็มาเริ่มสร้างขั้นตอนการรับ text และ image จาก database กันเล้ยยยย เปิด ChatViewController.swift ขึ้นมา แล้วประกาศตัวแปรดังนี้

// TODO: - Declare messageRef as FIRDatabaseReference
private lazy var messageRef: FIRDatabaseReference = FIRDatabase.database().reference().child("messages")
// TODO: - Declare storageRef as FIRStorageReference
fileprivate lazy var storageRef: FIRStorageReference = FIRStorage.storage().reference(forURL: "gs://fir-devday.appspot.com")
// TODO: - Declare event listener
private var newMessageRefHandle: FIRDatabaseHandle?
private var updatedMessageRefHandle: FIRDatabaseHandle?

สำหรับ messageRef จะชี้ไปที่ messages ใน database (ที่อธิบายไปแล้วด้านบน) แล้วก็มี storageRef ที่ชี้ไปที่ Storage ที่เอาไว้เก็บรูปภาพของเราอยู่ เอาไว้สำหรับตอน upload image และสุดท้ายก็มี event listener 2 ตัว เอาไว้สำหรับ observe message และ update message ที่มาจาก database

ต่อมาให้สร้าง deinit ขึ้นมา เพื่อ unregister สำหรับตอนที่ไม่จำเป็นต้องใช้ listeners แล้ว (มี register ก็ต้องมี unregister เสมอนะแจ๊ะ อย่าลืม อย่าลืม)

// TODO: - Unregister Firebase listeners.
deinit {
if let refHandle = newMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
if let refHandle = updatedMessageRefHandle {
messageRef.removeObserver(withHandle: refHandle)
}
}

มาที่ observeMessages() แล้วสร้าง messageQuery ซึ่งทำหน้าที่บอกว่าให้ query ข้อมูล 25 messages ล่าสุดมาแสดงนะ

// TODO: - Create messageQuery with query limit (25)
let messageQuery = messageRef.queryLimited(toLast: 25)

จากนั้นใน func เดียวกัน จัดการ register listener ตัวนึงของเราที่ชื่อว่า “newMessageRefHandle” ทำการ observe .childAdded ไว้ ซึ่ง .childAdded นี้หมายความว่า เมื่อมีข้อความอันใหม่ถูกเพิ่มเข้าไปใน database ก็จะวิ่งเข้าที่ listener ตัวนี้ทันที ทำให้เราสามารถ update chat ui ได้อย่างสะดวกสบาย

หลักๆ ก็ไม่มีอะไรมากครับ ก็ไปแกะ snapshot แล้วแยก type ว่าเป็น “text” หรือ “image” แล้วก็จับยัดลง dataSource ของ JSQMessagesViewController ซะ จากนั้นสั่ง reload JSQMessagesViewController แล้วทำการ refresh chat ui ก็เรียบร้อย ลองทดสอบรันแอปได้เลยครับ เอ้อ แล้วก็อย่าลืมไปทำให้ Database ของเรามี snapshot ตามที่กล่าวไว้ด้านบนด้วยนะครับ เดี๋ยว message ไม่มา จะงงแบบเส้นผมบังภูเขาได้นะเอ้อ…

Send text

หลังจากสามารถรับ message ได้ทั้งข้อความและรูปภาพแล้ว เราจะลองมาส่งข้อความที่เป็น text ขึ้นไปบน Database กันบ้างล่ะ ให้ไปที่ didPressSend ที่ override มาจาก JSQMessagesViewController แล้วเพิ่มโค้ดตามนี้

override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
// TODO: - Sent text message to Firebase
let itemRef = messageRef.childByAutoId()
let messageItem = [
"avatar": avatarString,
"data": text,
"type": "text",
"username": senderDisplayName,
"senderId": senderId
]

itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()
}

ไม่มีอะไรยากครับ ก่อนอื่นเราก็ใช้ messageRef.childByAutoId เพื่อสร้าง unique key ให้กับ message ของเรา จากนั้นก็สร้าง Dictionary แล้วระบุ key-value ให้ถูกต้อง โดย value text จะเป็นข้อความที่พิมพ์มาจากผู้ส่งครับ ทีนี้รันแอปแล้วมาทดสอบส่งข้อความ text อันแรกกันเล้ยยย

ทีนี้ลองกลับมาดูที่ Database กันบ้าง

โอ้วววววว เย เราแชทข้อความแรกออกไปได้แล้ว ง่ายสุดๆ

Send image

มาๆ มาต่อกันด้วยการ upload รูปภาพ และแสดงผลกัน สเตปนี้จะใช้ Storage เข้ามาช่วยแล้ว ซึ่งเอาไว้เก็บ static files ต่างๆ เช่น รูป เสียง วิดีโอ บลาๆ ได้หมดถ้าสดชื่น ให้เราไปที่ func sendPhotoMessage() แล้วเพิ่มโค้ดตามนี้ครับ

fileprivate func sendPhotoMessage() -> String? {
// TODO: - Sent photo message to Firebase.
let itemRef = messageRef.childByAutoId()

let messageItem = [
"avatar": avatarString,
"data": "NOTSET",
"type": "image",
"username": senderDisplayName,
"senderId": senderId
]

itemRef.setValue(messageItem)
JSQSystemSoundPlayer.jsq_playMessageSentSound()
finishSendingMessage()

return itemRef.key
}

ก็คล้ายๆ กับการส่ง text เลย ต่างกันตรงที่ “data” นั้นผมจะส่งคำว่า “NOTSET” ขึ้นไปก่อน เพื่อให้ในหน้า chat ui ของผมนั้นแสดงหน้ากล่อง uploading ก่อนเลย แล้วเดี๋ยวผมจะทำการ update รูปอีกที เมื่อมันถูก upload ไปที่ storage เสร็จสมบูรณ์แล้ว

ไปที่ func setImageUrl แล้วเพิ่มโค้ดตามนี้ เพื่อทำการ update “NOTSET” ให้เป็น image url จริงๆ เมื่อ file ถูก upload เสร็จสิ้นแล้ว

fileprivate func setImageURL(_ url: String, forPhotoMessageWithKey key: String) {
// TODO: - Update existing image when generate image url successfully.
let itemRef = messageRef.child(key)
itemRef.updateChildValues(["data": url])
}

จากนั้นให้เราไปทำการแก้ไขโค้ด UIImagePickerControllerDelegate, UINavigationControllerDelegate ที่เราได้ทำ extension ให้กับ ChatViewController ของเราเป็นดังนี้

ซึ่งจะ support ครบทุกกรณี ทั้ง browse รูปจาก simulator, เลือกหรือถ่ายรูปจาก iPhone ก็ได้ โดยผมไม่ได้ compress รูปให้นะครับ แต่ถ้าจะทำจริงๆ ควรจะ compress ก่อน upload ขึ้นไปที่ Storage ด้วย เพื่อไม่ให้ไฟล์ใหญ่เกินไป ซึ่งวิธีการ upload รูปก็ง่ายๆ ครับ แค่เรียกใช้

self.storageRef.child(path).put(...)

เพื่อ put file หรือ data ขึ้นไปที่ Storage โดยจะมีการระบุ metadata ของรูปด้วยนิดหน่อย ตามโค้ดยาวๆ ด้านบนเลยครับ

ต่อไปสเตป (เกือบ) สุดท้ายแล้ว ให้เราไปที่ func observeMessages() แล้วเพิ่ม observe .childChanged เพิ่ม เพื่อบอกว่าเมื่อรูปจริงถูก upload เสร็จแล้ว ให้ทำการ update chat ui จากกล่องรูปว่างๆ ที่มี spinner หมุดอยู่ เป็นรูปจริงๆ ของเราแทนนะ ก็เป็นอันเรียบร้อยแล้ว

// TODO: - Call to observeUpdateMessage
updatedMessageRefHandle = messageRef.observe(.childChanged, with: { (snapshot) in
let key = snapshot.key
let messageData = snapshot.value as! Dictionary<String, String>

if let type = messageData["type"] as String!,
type == "image",
let photoURL = messageData["data"] as String!,
photoURL.characters.count > 0 {

// The photo has been updated.
if let mediaItem = self.photoMessageMap[key] {
self.fetchImageDataAtURL(photoURL, forMediaItem: mediaItem, clearsPhotoMessageMapOnSuccessForKey: key)
}
}
})

ทีนี้รันแอปมาทดสอบกันเลย กดปุ่มคลิปหนีบกระดาษเพื่อเลือกรูป แล้ว upload ขึ้นไปโล้ดดดด

จะเห็นว่าระหว่าง upload รูปอยู่ เพื่อ UX ที่ดี เจ้า JSQMessagesViewController ก็สามารถแสดง spinner ให้ดูได้ และเมื่อ upload เสร็จแล้ว ก็ค่อยแสดงรูปจริงออกมาให้ดู ด้วยวิธีการที่บอกไว้แล้วด้านบนนะฮ้าฟ แต่วิธีนี้อาจจะกระทบกับฝั่ง Android และ Web นิดหน่อย ตรงที่ตอนแรกผมส่ง “NOTSET” ขึ้นไป นั่นแปลว่า ถ้าอีกสองฝั่งไม่ implement .childChanged ด้วย ก็จะทำให้โชว์กล่องขาวๆ ตลอดกาล เพราะไปเรียกคำว่า “NOTSET” จาก snapshot มาโหลดรูป มันก็ต้องไม่ขึ้นอยู่แว้ว ยังไงก็รบกวนด้วยนะฮะ สำหรับฝั่ง Web และ Android :)

ทีนี้มาลองดูรูปที่เราเพิ่งอัพไปใน Storage ซิ้ ว่ามันมีไหม เปิด Firebase Console ไปที่เมนู Storage เลย แล้วก็ refresh browser สักที นับหนึ่งถึงสิบ ขึ้นอยู่ความเร็วของเน็ท ถถถ

มาล่ะ รูปที่เพิ่ง upload ขึ้นไป จะสังเกตเห็นว่าผมจะใช้เป็น snapshot key เป็นชื่อไฟล์รูปเลยนะครับ เพราะมันไม่มีทางซ้ำกันอยู่แล้ว จะได้ไม่ไปทับรูปของคนอื่นเขา :) โดยเราสามารถดูรายละเอียดของรูปได้ตามที่เห็นเลยครับ เย้ เรา Send image ได้แว้ว

Offline apps

มีความสามารถที่ยอดเยี่ยมอีกอย่างของ Database ก็คือ เราสามารถเซ็ทให้มันทำงานแบบออฟไลน์ได้ด้วยโค้ดเพียงบรรทัดเดียว ที่ผมแอบใส่ไว้ใน AppDelegate ตั้งแต่ต้นแล้ว

FIRDatabase.database().persistenceEnabled = true

ทำให้ snapshot ที่ได้จาก Database จะถูก save ไว้ที่ Disk Cache ในเครื่องของเรา บวกกับที่ผมใช้ Kingfisher ทำให้สามารถ cache รูปภาพไว้ใน Disk ได้อีกเช่นกัน ทำให้ต่อให้เราไม่มีเน็ท ก็ยังสามารถเปิดแอปมา แล้วเห็นข้อความและรูปภาพได้ครบถ้วน สมบูรณ์แบบเหมือนเดิม ที่สำคัญเจ้า Database มันจะจัดการ sync ข้อมูลให้เราอย่างอัตโนมัติเมื่อเน็ทกลับมาใช้งานได้เหมือนเดิม ทำให้แอปเราได้รับการอัพเดทข้อความจากผู้ส่งคนอื่นๆ ตลอดเวลา สุดยอดดดดด real-time จริมๆ

Sign out

เอาล่ะ เรามาถึงขั้นตอนสุดท้ายกันแล้วเนอะ นั่นก็คือการทำการ sign out ออกจากหน้า chat นั่นเอง ซึ่งสิ่งที่เราต้องทำก็คือ sign out Firebase และ Google ทั้งคู่ เนื่องจากมันเป็นคนละส่วนกัน ให้ไปที่ func signOut() แล้วเพิ่มโค้ดดังนี้

// TODO: - Firebase Sign out
do {
try FIRAuth.auth()?.signOut()
} catch let signOutError as NSError {
print ("Error Firebase signing out: \(signOutError)")
}
// TODO: Google Sign out
GIDSignIn.sharedInstance().signOut()

โค้ด sign out ง่ายมากๆ รันแอป แล้วกด Sign out ที่หน้า chat ได้เลย เราควรจะเด้งกลับมาหน้า login เหมือนเดิมเนอะ ^^

เรียบร้อย ในที่สุดเราก็ได้แอปแชทภายในระยะเวลา 45 นาทีออกมาแล้ว ( แต่กว่าจะเขียนบล้อกอันนี้เสร็จ ใช้เวลาไปหลายชั่วโมง TT ) ดีงามพระรามแปดสุดๆ

ก็จบลงเรียบร้อยแล้วนะครับ สำหรับบทความ “เจาะลึกเบื้องหลัง Firebase Code Battle ฝั่ง iOS กับการพัฒนาแอปแชทภายใน 45 นาที” อันนี้ ยาวมากจริงๆ แต่คุ้มค่าครับ เพราะคิดว่าทุกคนน่าจะสามารถเอาไปประยุกต์ใช้กับงานของตนเองได้เป็นอย่างดี ส่วนสำหรับใครที่สนใจ Full Source ก็สามารถไปโหลดมาได้ที่นี่เลยครับ

โดย branch master จะเป็นตัวที่เสร็จสมบูรณ์แล้ว และก็จะมี branch code_battle แยกให้อยู่ ซึ่งเป็น template ตั้งต้นของบทความนี้ โดยมี TODO: ให้ทีละขั้นตอนครับผม สามารถโหลดมา แล้วทำตามบทความนี้ไปได้เลย (แต่ต้อง Config Firebase กับ Google กันใหม่เอาเองนะ) สำหรับแหล่งอ้างอิงข้อมูลเพิ่มเติม ตามนี้เลยครับ

หวังว่าบทความนี้จะเป็นประโยชน์กับท่านผู้อ่านทุกท่านไม่มากก็น้อยนะครับ สุดท้ายนี้อยากขอบคุณทาง Firebase Thailand และ GDG Thailand รวมถึง Ascend Group อีกครั้งนะครับ ที่ให้ผมได้มีโอกาสเป็น speaker ในงาน Firebase Dev Day ครั้งนี้ มันเป็นประสบการณ์ที่คุ้มค่ามาก และก็อีกครั้งนะครับ สำหรับใครก็ตามที่มีโอกาสที่ตัวเองคิดว่า “มันคุ้มค่า” กับชีวิตของเราผ่านเข้ามา จงอย่าลังเลที่จะไขว่คว้ามันไว้ครับ เพราะมันอาจจะไม่มาอีกเป็นครั้งที่สองแล้วก็ได้ พึงระลึกไว้เสมอครับว่า

ทำแล้วเสียใจ ดีกว่าเสียใจที่ไม่ได้ทำ

สำหรับวันนี้ ลากันไปเท่านี้ก่อน (ซะทีเนอะ ^^) แล้วพบกันใหม่ บทความหน้า สวัสดีครับ :)

ติดตามเรื่องราวต่างๆ ทั้งเทคโนโลยี มุมมองชีวิต การเรียนรู้ การใช้ชีวิต ได้ที่ https://www.facebook.com/itopstory/

https://www.facebook.com/FirebaseThailand/photos/rpp.548266378686903/678865432293663/?type=3&theater

--

--

Kittisak Phetrungnapha
iTopStory

I am a software engineer who fall in love to code, read, and write. :) itopstory.com