Contents
Chào mừng bạn đến với Fx Studio. Chúng ta sẽ tìm hiểu về thực thể đầu tiên trong phần State & Data Flow. Đó là User Interface State, nó sẽ bao gồm cả về dữ liệu và trạng thái dữ liệu. Và đây cũng là bài viết thuộc series SwiftUI Notes dài bất tận này.
Trước khi vào bài viết, bạn cần phải nắm được các khái niệm cơ bản. Nếu bạn chưa biết thì có thể truy cập link dưới đây để tham khảo.
Chuẩn bị
Về mặt tool và version, các bạn tham khảo như sau:
-
- SwiftUI 2.0
- Xcode 12
Về mặt kiến thức, bạn cần biết trước các kiến thức cơ bản với SwiftUI & SwiftUI App. Tham khảo các bài viết sau, nếu bạn chưa đọc qua SwiftUI:
(Mặc định, mình xem như bạn đã biết về cách tạo project với SwiftUI & SwiftUI App rồi.)
Về mặt demo, bạn chỉ cần thực hiện demo trên các SwiftUI View đơn giản, Vì trong phần này, chúng ta chỉ tập trung và dữ liệu & những thứ dữ liệu ảnh hưởng tới dữ liệu mà thôi.
(Hoặc bạn có thể checkout project demo tại đây.)
State
Chúng ta đã có một bài về Declaring Data trong series SwiftUI Notes. Trong đó, mình đã giới thiệu về State và cách sử dụng nó trong SwiftUI. Và nếu bạn là một theo dõi series này từ đầu, thì cũng quen thuộc với @State rồi. Còn bây giờ, ta sẽ phân tích nó kĩ hơn một chút.
Với State, được xem là một trong những điều tuyệt với nhất của thế giới SwiftUI.
Như bạn đã biết, các SwiftUI View đều là các struct và nó là non-mutable (không thể thay đổi). Bên cạnh đó, Views sẽ được tạo lại mỗi khi dữ liệu thay đổi. Nên các thuộc tính (dữ liệu) của View cũng phải được tạo lại.
Và với khai báo thuộc tính là Property Wrappers @State
, bạn nói với SwiftUI rằng:
Bạn muốn nó giữ dữ liệu này trong một phần riêng của bộ nhớ. Cho phép nó được thay đổi và bảo toàn giá trị hiện tại của biến trong quá trình làm mới chế View.
Các biến/thuộc tính @State
luôn là value type (kiểu tham trị) và thường sử dụng trong chính View. Nên khuyến cáo cho bạn khai báo private
cho chúng nó.
Bạn có thể truyền các giá trị (read-only) của @State
cho các subview của. Nhưng chỉ sử dụng cho việc hiển thị mà thôi, còn với dữ liệu động (read-write) thì các subview sẽ không nhận được giá trị đã bị thay đổi.
Ngoài ra, bạn có thể tạo các cầu nối 2 chiều (Binding) tới các thuộc tính @State
từ các View khác. SwiftUI theo dõi các thay đổi trong dữ liệu và cập nhật mọi chế độ xem bị ảnh hưởng nếu cần.
Manage Mutable Values as State
Khai báo
Chúng ta sẽ tìm hiểu tiếp cách hoạt động của State với việc quản lý các trạng thái giá trị như thế nào. Bắt đầu, nếu SwiftUI View của bạn cần một store data có thể thay đổi được giá trị. Cách tốt nhất là hãy khai báo nó với @State
Property wrapper.
Ngoài cách này, bạn có thể sử dụng trực tiếp việc khai báo đối tượng từ Struct State. Nhưng mà thôi, biết vậy là được rồi.
Ta có ví dụ như sau:
struct PlayerView: View { @State private var isPlaying: Bool = false var body: some View { // ... } }
Sử dụng
Ta khai báo thuộc tính isPlaying
cho PlayerView, để biết trạng thái của View là đang chơi nhạc hay dừng lại. Ta sẽ xem tiếp, cách sử dụng thuộc tính đó như thế nào trong View nha.
Button(action: { self.isPlaying.toggle() }) { Image(systemName: isPlaying ? "pause.circle" : "play.circle") }
Trong đó:
- Nếu bạn chỉ muốn hiển thị dữ liệu từ thuộc tính. Thì cần truy cập vào
wrappedValue
của thuộc tính State. - Dựa vào nội dung của giá trị thuộc tính mà View sẽ có hiển thị phù hợp. Trong ví dụ, là sự thay đổi giữa 2 hình “play” hay “pause“.
- Nếu bạn muốn thay đổi giá trị của thuộc tính State. Thì sử dụng như bao biến/thuộc tính bình thường trước đây.
Khi bạn thay đổi giá trị, SwiftUI cập nhật các phần bị ảnh hưởng của chế độ xem.
Cuối cùng, giới hạn phạm vi của các biến trạng thái (State) bằng cách khai báo chúng là riêng tư (private). Điều này đảm bảo rằng các biến vẫn được đóng gói trong cấu trúc phân cấp View mà chúng được khai báo.
Kết quả đạt được là chúng ta có thể biểu diễn trạng thái của View đồng bộ với trạng thái của dữ liệu. Tất cả, thông qua việc sử dụng các thuộc tính State.
Store Properties Immutable Values
Đôi khi chúng ta vẫn sử dụng các thuộc tính mà chúng không thay đổi giá trị. Tức là SwiftUI View chỉ hiển thị mà thôi và không cần tạo lại. Như vậy, bạn hãy khai báo các thuộc tính như bình thường. Đơn giản vậy thôi.
Ta sẽ làm một ví dụ đơn giản nhoé.
struct PlayerView: View { let episode: Episode // The queued episode. @State private var isPlaying: Bool = false var body: some View { VStack { // Display information about the episode. Text(episode.title) Text(episode.showTitle) Button(action: { self.isPlaying.toggle() }) { Image(systemName: isPlaying ? "pause.circle" : "play.circle") } } } }
Trong đó:
episode
chứa thông tin hiển thị
Nó sẽ không thay đổi giá trị được. Cho dù các thuộc tính State khác thay đổi bao nhiêu lần đi nữa và SwiftUI View có tạo mới lại nhiều lần đi nữa. Thì giá trị của nó là bất biến và bạn không thể làm được gì. Hoặc bạn cố gắng khai báo với var
thì hầu như cũng là bất lực mà thôi.
Share Access to State with Bindings
Ta đã đi qua 2 phần với 2 kiểu trạng thái của giao diện (thay đổi được và không thay đổi được). Tiếp theo, ta sẽ tìm hiểu một đặc tính nữa của các biến/thuộc tính State.
Khi View của bạn có các subview (view con) và bạn muốn kiểm soát các thuộc tính của subview. Hoặc muốn chia sẽ việc truy cập tới store data (chính là biến State). Thì hãy khai báo các thuộc tính của subview với Property Wrappers @Binding
.
Một Binding đại diện cho một tham chiếu đến bộ nhớ hiện có, duy trì một nguồn sự thật chân lý (The single source of truth) cho dữ liệu (với kiểu dữ liệu cơ bản, như: Int, Float …).
(Chúng ta sẽ tìm hiểu về nguồn sự thật chân lý (The single source of truth) trong bài viết sau.)
Define
Bạn thử nâng cấp ví dụ ở trên, với việc tạo ra một SwiftUI View riêng cho Button nha. Tham khảo code như sau:
struct PlayButton: View { @Binding var isPlaying: Bool var body: some View { Button(action: { self.isPlaying.toggle() }) { Image(systemName: isPlaying ? "pause.circle" : "play.circle") } } }
Tại PlayButton, ta khai báo isPlaying
với kiểu là @Binding
. Bạn sẽ nhận ra một điều rất sướng của SwiftUI.
Bạn không cần cấp giá trị ban đầu cho nó và cũng không cần thêm hàm khởi tạo nữa.
Binding
Các dữ liệu của Binding sẽ được cung cấp từ bên ngoài. Và để xem việc truyền dữ liệu từ bên ngoài vào sẽ như thế nào. Bạn xem tiếp ví dụ code sau cho PlayerView.
struct PlayerView: View { var episode: Episode @State private var isPlaying: Bool = false var body: some View { VStack { Text(episode.title) Text(episode.showTitle) PlayButton(isPlaying: $isPlaying) // Pass a binding. } } }
Trong đó, bạn chỉ cần chú ý tới dòng code truyền dữ liệu từ PlayerView cho PlayButton mà thôi.
Với khai báo kèm theo tiền tố $
, bạn tạo nên một sự ràng buộc với projectedValue
của thuộc tính State. Sự kết nối này sẽ trỏ tới vùng nhớ của thuộc tính đó. Do đó, các subview có thể truy cập và lấy được giá trị của State. Có thể truyền qua nhiều subview với nhau.
Binding to a scoped value
Khi bạn có cả một đối tượng và bạn vẫn có thể chỉ cần Binding một phần dữ liệu của đối tượng đó, thì thuộc tính @State
sẽ cân được hết.
Theo nguyên tắc trên,
@State
chỉ áp dụng các kiểu (type value) dữ liệu cơ bản mà thôi. Ví dụ: Int, Float, String …
Nên khi khai báo một thuộc tính State với kiểu dữ liệu phức tạp (Struct), ta sẽ vẫn áp dụng việc Binding được cho các thuộc tính riêng lẻ của đối tượng mà thôi. Còn toàn bộ đối tượng thì không được.
Bạn xem qua ví dụ nha:
struct Podcaster: View { @State private var episode = Episode(title: "Some Episode", showTitle: "Great Show", isFavorite: false) var body: some View { VStack { Toggle("Favorite", isOn: $episode.isFavorite) // Bind to the Boolean. PlayerView(episode: episode) } } }
Bạn có một Podcaster là một SwiftUI View lớn nhất, nó chứa PlayerView. Trong đó:
episode
là một kiểu Struct và được khai báo theo@State
- Sử dụng thuộc tính
.isFavorite
để thay đổi một giá trị thuộc tính của Stateepisode
Qua phần này, bạn đã có đầy đủ vũ khí với State và Binding rồi. Hãy sử dụng một cách khôn ngoan nha. Thành bại đều tuỳ thuộc vào cách suy nghĩ của bạn mà thôi.
Animate State Transitions
Khi bạn thay đổi giá trị của thuộc tính State, thì hầu như các View sẽ ngay lập tức thay đổi hiển thị ngay. Nến bạn muốn các hiệu ứng mượt mà hơn khi các trạng thái thay đổi giá trị. SwiftUI cung cấp cho bạn một function withAnimation(_:_:)
để làm việc đó. Nó hoạt động như một trigger, trong quá trình diễn ra hiệu ứng, thì dữ liệu của bạn sẽ được từ từ biến đổi theo.
Ta hãy lấy ví dụ cho Button của chúng ta. Bạn xem qua code ví dụ như sau tại PlayButton.
Button(action: { withAnimation(.easeInOut(duration: 1)) { self.isPlaying.toggle() } }) { Image(systemName: isPlaying ? "pause.circle" : "play.circle") .scaleEffect(isPlaying ? 1 : 1.5) }
Trong đó:
- Thay đổi trạng thái của
isPlaying
được sử dụng vớiwithAnimation
- Sự thay đổi này kèm theo hiệu ứng cho Image với
.scaleEffect(isPlaying ? 1 : 1.5)
- Bạn sẽ yên tâm là button của chúng ta sẽ phóng to và thu nhỏ liên tục với nhau
Và một điều mà bạn sẽ phải biết là Image chỉ thay đổi được kích thước dựa theo hiệu ứng. Còn nội dung ảnh của nó hoàn toàn không thể có sự biến đổi đó được.
Ta sẽ có thêm một ví dụ nữa để chứng tỏ việc thay đổi dữ liệu sẽ ảnh hưởng qua nhiều View với nhau. Tham khảo thêm code này ở PlayerView.
VStack { Text(episode.title) Text(episode.showTitle) PlayButton(isPlaying: $isPlaying) // Pass a binding. } .background(isPlaying ? Color.green : Color.red) // Transitions with animation.
Trong đó:
- Bạn sẽ thay đổi màu nền của VStack qua lại giữa 2 màu
green
&red
, thông qua việc thay đổi giá trị của thuộc tính StateisPlaying
.
Chúng ta sẽ thay đổi hiểu thị cho toàn bộ một View, với hiệu ứng có được từ việc thay đổi giá trị của một thuộc tính State. Nhưng vị trí diễn ra sự thay đổi đó lại ở một nơi khác (View khác).
Bạn hãy build lại project và cảm nhận kết quả nha!
Tạm kết
- Phân tích State trong việc ảnh hưởng tới trạng thái của giao diện
- Các kiểu thuộc tính tác động tới giao diện
- Chia sẽ tới các State bằng các Binding
- Các cách Binding cơ bản
- Hiệu ứng cho việc thay đổi giá trị của các State
Okay! Tới đây, mình xin kết thúc bài viết về User Interface State trong SwiftUI. Và nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)