Ứng dụng State Machine trong Swift
State Machine là một kỹ thuật không mới trong lập trình nói chung và rất hữu dụng để giải quyết các bài toán về trạng thái. Trước đây đã có 1 bài viết của @huydx trong series hard-core nói về State Machine khá chi tiết. Lần này mình sẽ giới thiệu một ứng dụng trong lập trình IOS (Swift) khi ứng ...
State Machine là một kỹ thuật không mới trong lập trình nói chung và rất hữu dụng để giải quyết các bài toán về trạng thái. Trước đây đã có 1 bài viết của @huydx trong series hard-core nói về State Machine khá chi tiết. Lần này mình sẽ giới thiệu một ứng dụng trong lập trình IOS (Swift) khi ứng dụng có biểu đồ di chuyển phức tạp.
State Machine là gì
State Machine là ... một cái máy tính đơn giản, bao gồm một tập nhiều trạng thái (state) và các di chuyển giữa các trạng thái đó với nhau. Mỗi khi có 1 sự kiện (event) xảy ra, cái máy của chúng ta sẽ chuyển từ trạng thái này sang trạng thái khác.
Ở hình bên trên trạng thái nguyên thuỷ là State A.Với sự kiện T1 diễn ra, trạng thái State A sẽ di chuyển đến trạng thái State B v.v...
Vậy nếu liên tưởng mỗi màn hình trong ứng dụng là 1 trạng thái, và sự kiện để chuyển màn hình là input thì chúng ta sẽ có 1 State Machine để quản lý toàn bộ quá trình di chuyển màn hình.
|--------------------| |--------------------| |--------------------| |--------------------| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | initial | --> | greeting | --> | login | --> | home | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |--------------------| |--------------------| |--------------------| |--------------------| launch start loggedIn
Điều này có lợi gì so với cách làm thông thường ?
Khi số lượng màn hình của 1 tính năng (từ đây sẽ gọi là 1 flow) tăng đến tầm 20~30 màn hình, chúng ta thường sẽ không muốn cho tất cả vào trong 1 storyboard mà chia làm các storyboard riêng biệt rồi kết nối lại với nhau bằng code. Các đoạn kết nối nói trên sẽ bị chia lẻ tẻ vào nhiều file source khác nhau, khó quản lý và điều chỉnh khi có thay đổi về tính năng.
Dùng State Machine sẽ quản lý được toàn bộ các dữ kiện lưu chuyển của flow bằng một machine duy nhất.
Implement State Machine
Trước hết là implement phiên bản đơn giản cho một State Machine. Ở đây mình dùng một từ điển 2 tầng routes để lưu tham chiếu từ một trạng thái, 1 sự kiện để dẫn đến 1 trạng thái mới.
import Foundation protocol StateType: Hashable {} protocol EventType: Hashable {} struct Path<S: StateType, E:EventType> { let from: S let to: S let by: E } class StateMachine<S: StateType, E: EventType> { var currentState: S var routes: [S: [E: S]] = [:] init(_ initState: S, _ config: (StateMachine) -> Void) { currentState = initState config(self) } func addPath(_ p: Path<S, E>) { var dict = routes[p.from] ?? [:] dict[p.by] = p.to routes[p.from] = dict } func transition(from: S, by: E) -> Bool { let next = routes[from].flatMap { $0[by] } guard let existed = next else { return false } currentState = existed return true } }
Tạo ra 1 State Machine cụ thể
Đến bây giờ mình có thể định nghĩa tất cả các trạng thái và sự kiện thành kiểu enum, mà chỉ cần thoả mãn StateType và EventType bên trên:
enum ScreenState: StateType { case initial case greeting case login case home } enum MovingEvent: EventType { case launch case start case loggedIn }
Và cùng với đó là quản lý đồ thị di chuyển tập trung tại 1 chỗ
let graph: [Path<ScreenState, MovingEvent>] = [ Path(from: .initial, to: .greeting, by: .launch), Path(from: .greeting, to: .login, by: .start), Path(from: .login, to: .home, by: .loggedIn) ] class StateMachineManager { static let machine = StateMachine<ScreenState, MovingEvent>(.initial, { machine in for p in graph { machine.addPath(p) } }) }
Nối các màn hình vào vào các trạng thái tương ứng
Để sử dụng State Machine ở trên, mình sẽ tạo 1 protocol chuyên biệt để tiện sử dụng cho các ViewController mong muốn
protocol StatefulViewController { var state: State { get } func moveNext(event: Event) -> UIViewController? } extension StatefulViewController { func moveNext(event: Event) -> UIViewController? { let machine = StateMachineManager.machine guard machine.transition(from: state, by: event) else { return nil } switch machine.currentState { case .initial: // Return correcsponding View Controller case .greeting: // Return correcsponding View Controller case .login: // Return correcsponding View Controller case .home: // Return correcsponding View Controller } } }
Xong! Đơn giản hơn mọi người vẫn nghĩ phải không nào