มาทำ iOS ViewState protocol สำหรับ Network กันเถอะ

Kittisak Phetrungnapha
iTopStory
Published in
3 min readAug 15, 2018

--

สวัสดีชาว iOS ทุกท่าน พอดีผมได้ไปดูวิดีโอ Writing Your UI Swiftly มาแล้วมีหัวข้อ ViewState ที่น่าสนใจ เลยเอามาเขียนแบ่งปันให้ทุกคน เผื่อว่าจะมีประโยชน์ไม่มากก็น้อย เริ่มเลยละกัน ไม่อยากพิมพ์เยอะ เจ็บนิ้ว

ปกติในแอป iOS ที่เราเขียนกันทุกวันเนี่ย 90% ต้องมีการเชื่อมต่อ network กับ API เพื่อเรียกข้อมูลมาแสดงใช่มะ ทีนี้ state การทำงานหลักๆ ของมันก็จะมีประมาณสามแบบ คือ

  1. กำลังโหลดข้อมูลอยู่
  2. โหลดข้อมูลสำเร็จแล้ว
  3. โหลดข้อมูลแล้ว error (จะด้วยอะไรก็ตาม แต่ไม่เอาแอป crash นะ)

ซึ่งเราก็จะแสดง UI ให้ผู้ใช้ดูแตกต่างกันไปตามแต่ละ state ด้านบนใช่มะ เช่น ถ้ากำลังโหลดข้อมูลอยู่ ก็เอา loading view มาโชว์ ถ้าโหลดข้อมูลสำเร็จแล้ว ก็เอาข้อมูลที่ได้มาแสดง หรือถ้าโหลดข้อมูลแล้ว error ก็จะโชว์ error view หรืออะไรก็ว่าไปให้ผู้ใช้งานทราบว่ามันพังเพราะอะไร ตัวอย่างโค้ดสุด basic เลยก็จะประมาณนี้

enum State<T> {
case loading
case success(data: T)
case error(message: String)
}
var state = ...switch state {
case .loading:
// Show loading view
case .success(let data):
// Show data e.g. tableView.reloadData()
// Hide error view
case .error(let message):
// Show error view
// Hide tableView
}

ซึ่งในแอปเราส่วนใหญ่ก็จะมีการติดต่อกับ network หลายๆ ที่ใช่มะ ทีนี้เราก็อาจจะต้องมาเขียน switch case หรือ if-else เพื่อจัดการกับ UI หลายๆ ที่ ตาม response ที่ได้กลับมาจาก API บางทีมันก็น่าเบื่อเนอะ ทีนี้เราพอจะเขียนอะไรที่เป็นตัวกลางง่ายๆ ที่ใช้จัดการกับ state ของ view ในการติดต่อกับ network หลายๆ หน้าได้บ้างไหม ก็มาดูกันต่อเนอะ

เอา Protocol มาช่วย

แน่นอน ใน Swift เรามีพระเอกอย่าง protocol และ extension อยู่ ก็เอามันมาใช้เลยสิ

protocol DataLoading {
associatedtype Content
var state: ViewState<Content> { get set }
var loadingView: LoadingView { get }
var errorView: ErrorView { get }

func update()
}
enum ViewState<T> {
case loading
case loaded(data: T)
case error(message: String)
}

เราก็ทำการสร้าง Protocol ขึ้นมาสักตัวหนึ่ง ในที่นี้ขอใช้เหมือนกับในวิดีโอเลยละกัน (ขี้เกียจพิมพ์ เจ็บนิ้ว) ชื่อว่า DataLoading แล้วก็มีตัวแปรสามตัว กับฟังก์ชันหนึ่งอัน

  1. state ที่เป็น generic enum ซึ่งเอาไว้ใส่อะไรก็ได้ แต่หลักๆ ก็เอาไว้ใส่ data ที่จะเอาไว้แสดงผลหน้า UI นั่นแหละ
  2. loadingView เป็น LoadingView ที่เราสร้างขึ้นมาเอง เพราะปกติ loading view ในแอปเดียวกัน เพื่อความ consistency ส่วนใหญ่มันจะเหมือนกันเกือบทุกหน้า หรือถ้าใครอยากทำให้มัน generic มากขึ้น ก็สร้างเป็น protocl LoadingView แล้วสร้าง UIView มา conform อีกทีเอาก็ได้
  3. errorView อันนี้ก็เหมือนกับข้อสองข้างบน เพียงแต่เอาไว้แสดงหน้า error แทน
  4. update() เอาไว้ทำการเปลี่ยนค่า state ของตัวแปร state ซึ่งจะเปลี่ยนเป็นอะไร ก็ขึ้นอยู่ response ที่ส่งกลับมาจาก API จะ success, failure ก็ว่ากันไป

Protolcol Extension

extension DataLoading where Self: UIView {
func update() {
switch state {
case .loading:
loadingView.isHidden = false
errorView.isHidden = true
case .error(let error):
loadingView.isHidden = true
errorView.isHidden = false
case .loaded:
loadingView.isHidden = true
errorView.isHidden = true
}
}
}

เราทำ default implementation ให้กับฟังก์ชัน update() ของเรา ซึ่ง class ที่จะได้ความสามารถใน extension นี้ไปใช้ ก็ต้องเป็น UIView ที่ conform DataLoading เท่านั้น ซึ่งภายในก็เป็นแค่การซ่อนหรือโชว์ UI ตาม state ที่มันควรจะเป็นเท่านั้นเอง

เอาล่ะ หลังจากเราได้ protocol DataLoading แล้วก็เอามันไปใช้สิ วิธีใช้ก็ง่ายๆ แค่ให้ view ของเรา confirm กับ DataLoading แล้วก็ implement สิ่งที่ protocol ต้องการให้ครบเท่านั้นเอง ตัวอย่างดังนี้

final class MainView: UIView, DataLoading {
let loadingView = LoadingView()
let errorView = ErrorView()

var state: ViewState<MainViewViewModel> {
didSet {
update()
// Add your custom code here
tableView.reloadData()
}
}
}
final class SecondView: UIView, DataLoading {
let loadingView = LoadingView()
let errorView = ErrorView()

var state: ViewState<SecondViewViewModel> {
didSet {
update()
// Add your custom code here
}
}
}

แล้วเราก็เอา custom view ที่เราสร้างไปแปะใน ViewController อีกที เวลาจะเปลี่ยนหน้าการแสดงผล UI ก็แค่เปลี่ยนค่า state เท่านั้นเอง เช่น

final class ViewController {
@IBOutlet weak var mainView: MainView!
private let viewModel = ViewControllerViewModel()
override viewDidLoad() {
super.viewDidLoad()
mainView.state = .loading
viewControllerViewModel.loadData { [weak self] in
// E.g. error case
self?.mainView.state = .error(message: "Oop!, something went wrong.")
}
}
}

ก็น่าจะประมาณนี้แหละครับ สำหรับการทำ ViewState เพื่อจัดการ UI ที่เกี่ยวข้องกับ response ที่ได้จากการ call API จริงๆ เราสามารถเอาไปประยุกต์ใช้กับอย่างอื่นก็ได้ครับ ไม่จำเป็นต้องเกี่ยวกับเรื่อง Network อย่างเดียวก็ได้ ยังไงวันนี้ก็ลากันไปก่อน พบกันใหม่บทความหน้า สวัสดีครับ, Happy Coding!

ปล. ใครสนใจดูวิดีโอต้นฉบับ ดูได้จาก

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

--

--

Kittisak Phetrungnapha
iTopStory

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