Contents
Chào bạn đến với Fx Studio. Chúng ta tiếp tục hành trình SwiftUI đầy vui vẻ và màu mè. Bài viết này sẽ quay lại một chủ đề không mới trong UIKit. Và với SwiftUI, việc Custom View lại hoàn toàn mới mẻ và đem lại trải nghiệm thú vị cho bạn. Chủ đề bài viết này là Reusable View.
Mình đã có viết một bài về chủ đề Custom View, nhưng nó cơ bản của cơ bản. Bạn có thể xem lại tại đây.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Chúng ta sẽ bắt đầu cho bài này bằng việc tạo mới một project với SwiftUI. Giao diện thì vẫn hết sức đơn giản. Còn với demo cho bài viết này, bạn cần chuẩn bị như sau:
-
- Xcode 12
- Swift 5.4
- SwiftUI 2.0
Bạn có thể checkout code tại đây:
-
- Toàn bộ repo: https://github.com/fx-studio/swiftui-notes
Với Reusable View, cái tên nghe khá là kêu nhưng thực chất đây chính là cách bạn Custom View mà thôi. Với UIKit, chúng ta đã quen với việc Custom và xử lý nhiều vấn đề kéo theo như: Binding, Callback … Thì sang SwiftUI, chúng ta cũng phải giải quyết chúng nó.
1. Create Reusable View
1.1. Create View
Bắt đầu, bạn có thể sử dụng tiếp project SwiftUI cũ hoặc có thể tạo mới. Tuy nhiên, bạn không cần quá cầu kì trong việc chuẩn bị. Đối với SwiftUI bạn có thể tạo ở mọi nơi và bất cứ lúc nào. Còn với demo cho bài viết thì mình sẽ tạo mới một file
- UserView dùng làm View custom
- ContentView để làm nơi chứa các UserView
Bắt đầu bằng việc tạo mới một file SwiftUI. Bạn chú ý là chọn đúng template SwiftUI nha.
Sau đó, mình đặt tên (mình đặt là UserView) và bấm enter. Mặc định Xcode sẽ tạo giúp cho bạn nhiều thứ. Xem qua code mặc định của UserView như sau:
import SwiftUI struct UserView: View { var body: some View { Text("Hello, World!") } } struct UserView_Previews: PreviewProvider { static var previews: some View { UserView() } }
Ta tiến hành tạo sơ giao diện của UserView như sau:
struct UserView: View { var body: some View { VStack { Image(systemName: "person.crop.square") .resizable() .foregroundColor(.blue) Text("Noname") .fontWeight(.bold) .multilineTextAlignment(.center) Button(action: { // action here }) { Text("Tap me!") } } } }
Với bố cục bao gồm:
- Một Image hiển thị ảnh của người dùng
- Text hiển thị tên người dùng
- Button để tương tác với view
Tuy nhiên, ấn tượng đầu tiên khi bạn nhìn sang màn hình Canvas thì khá là xấu xí.
Bạn không cần quá lo lắng. Với SwiftUI, tất cả đều xem là View. Cái bạn thấy chỉ là Preview của View đó trên một thiết bị giả định. Trong tình huống này thường suy nghĩ như sau:
- Bạn sẽ bị hiểu lầm View với kích thước là toàn màn hình
- Bạn sẽ tìm cách edit lại hay tuỳ chỉnh kích thước của các Control con trong View
1.2. Edit Size
Với trường hợp này bạn nên bình tĩnh và có việc cần làm sẽ là thay đổi lại kích thước Preview để có được hiệu quả mong muốn. Xem đoạn code sau:
struct UserView_Previews: PreviewProvider { static var previews: some View { UserView() .previewLayout(.fixed(width: 100.0, height: 150.0)) } }
Kết quả bây giờ rất là nhỏ xinh rồi.
Cách trên vẫn chỉ là cách đánh lừa mắt mình mà thôi. Giờ bạn cần phải cập nhật lại kích thước của View một cách cụ thể. Xem code tham khảo:
struct UserView: View { var body: some View { VStack { Image(systemName: "person.crop.square") .resizable() .foregroundColor(.blue) .frame(width: 100.0, height: 100.0) Text("Noname") .fontWeight(.bold) .multilineTextAlignment(.center) .frame(width: 100.0, height: 25.0) Button(action: { // action here }) { Text("Tap me!") } .frame(width: 100.0, height: 25.0) } .frame(width: 100.0, height: 150.0) } }
Giờ thì khác là ổn rồi. Qua các chương sau mình sẽ trình bày về cách bố cục hay auto layout sau nha. Tiếp tục, bạn chuyển sang ContentView và tiến hành thêm UserView vào. Ví dụ như sau:
struct ContentView: View { var body: some View { HStack { UserView() UserView() } } }
Mình thêm 2 UserView cho bạn dễ hình dung. Xem kết quả như sau:
2. Properties
Thuộc tính (Properties) là một thành phần không thể thiếu được trong class/struct. Với tất cả các đối tượng View, nhiệm vụ chính vẫn là hiển thị dữ liệu. Tuy nhiên chúng ta không thể nào hiển thị một dữ liệu duy nhất và không thể thay đổi được. Bên cạnh đó View còn phải cập nhật lại giao diện khi dữ liệu có sự thay đổi.
Và ta có 2 trường hợp cụ thể cho thuộc tính của một Reusable View trong SwiftUI như sau:
2.1. Read only
Bạn mở file UserView và ta thêm thuộc tính cho tên của user như sau:
var name: String
Thuộc tính này chỉ hiển thị một lần và đơn giản. Hầu như bạn không cần phải thay đổi gì. Do đó, bạn khai báo bình thường như trước đây với UIKit.
Sau đó, bạn cập nhật lại nội dùng của Text như sau, để nó hiển thị giá trị name
của các đối tượng UserView.
Text(name) .fontWeight(.bold) .multilineTextAlignment(.center) .frame(width: 100.0, height: 25.0)
Và bạn sẽ nhận một loạt lỗi từ Xcode. Bạn bình tĩnh vì đây là:
- Bạn thêm mới thuộc tính mà chưa cấp phát giá trị khởi tạo
- Các Preview và các View đang sử dụng UserView sẽ yêu cầu bạn cung cấp thêm giá trị khởi tạo cho đối tượng UserView với tham số
name
Bạn từ từ cập nhật lại nha. Ví dụ của mình ở ContentView như sau:
HStack { UserView(name: "User 1") UserView(name: "User 2") }
Kết quả cũng khá là đẹp.
2.2. Update Data
Ăn cỗ phải có thịt gà. Đi học Phải có đàn bà mới vui!
Đúng là nếu đơn thuần View chỉ hiển thị dữ liệu và không có gì khác thì buồn lắm. App của chúng ta sẽ giống như những trang web tĩnh. Do đó, việc cập nhật lại dữ liệu cũng có ý nghĩa cực kì quan trọng. Và đó cũng là nhiệm vụ thứ 2 của một View.
Tiếp theo, chúng ta sang dạng thuộc tính có sự thay đổi giá trị. Và sự thay đổi giá trị này ảnh hưởng tới hiển thị của View. Bạn thêm thuộc tính sau vào UserView.
@State var isSelected = false
Mục đích là để người dùng tương tác vào. Nếu:
isSelected == true
thì sẽ hiện ô checkisSelected == false
thì sẽ hiện ra ô un-check
Ta có code như sau:
struct UserView: View { var name: String @State var isSelected = false var body: some View { VStack { Image(systemName: "person.crop.square") .resizable() .foregroundColor(.blue) .frame(width: 100.0, height: 100.0) Text(name) .fontWeight(.bold) .multilineTextAlignment(.center) .frame(width: 100.0, height: 25.0) Button(action: { isSelected.toggle() }) { Image(systemName: isSelected ? "checkmark.square" : "square") .foregroundColor(.green) } .frame(width: 100.0, height: 25.0) } .frame(width: 100.0, height: 150.0) } }
Điểm quan trong là khai báo @State
, với khai báo này thì Control UI của chúng ta có sự liên kết về mặt dữ liệu với thuộc tính được khai báo với @State kia. Từ đó, mỗi khi giá trị thuộc tính được thay đổi thì SwiftUI tự động cập nhật lại giao diện phụ thuộc kia.
Muốn xem được kết quả thì bạn phải chuyển sang chế độ Live Preview. Kết quả sẽ như thế này.
3. Binding
Tiếp tục, chúng ta tới phần cũng khá là hay. Khi SwiftUI kết hợp với Combine để giúp cho bạn có thể Binding dữ liệu giữa 2 view một cách nhanh chóng.
Ta sẽ tiếp tục demo phần này. Bắt đầu tại file ContentView với việc khai báo thêm 1 thuộc tính @State
. Thuộc tính này đảm nhận vai trò lưu trữ và thay đổi trạng thái cho UI mà nó được ràng buộc.
@State var count = 0
Cũng tại ContentView, bạn cập nhật thêm UI để hiển thị số lượng User được selected
. Code mới như sau:
struct ContentView: View { @State var count = 0 var body: some View { VStack { Text("Selected \(count) users.") .font(.title) HStack { UserView(name: "User 1", count: $count) UserView(name: "User 2", count: $count) } } } }
Bạn sẽ thấy, ta chỉnh sửa lại khai báo UserView với tham số $count
. Nó là gì? Ta tạm thời sang file UserView để tiếp tục thêm thuộc tính mới.
@Binding var count: Int
Điểm mới ở đây là từ khoá @Binding
, với nó thì:
- Liên kết với biến/thuộc tính từ bên ngoài
- Khi nó thay đổi giá trị thì ngoài kia cũng theo đổi theo
- 1 đối tượng có thể sử dụng cho nhiều đối tượng con khác
Ta chỉnh sửa tiếp phần action
trong UserView như sau:
Button(action: { isSelected.toggle() if isSelected { count += 1 } else { count -= 1 } }) { Image(systemName: isSelected ? "checkmark.square" : "square") .foregroundColor(.green) }
Không quá khó hiểu khi chỉ có mỗi việc tăng hoặc giảm giá trị của thuộc tính mà thôi. Chúng ta lại về ContentView và chuyển sang chế độ Live Preview. Kết quả như sau:
Tóm tắt một chút:
@State
để khai báo một thuộc tính sẽ gây ảnh hưởng tới một View thông qua việc ràng buộc dữ liệu của nó. Mỗi khi thay đổi giá trị thì View sẽ tự động cập nhật lại.@Binding
sẽ liên kế với thuộc tính@State
. Từ đó chia sẽ giá trị của thuộc tính với nhau. Khi thay đổi ở một nơi thì nơi kia cũng thay đổi theo.- Từ khoá
$
sẽ được thêm vào trước tên thuộc tính @State làm đối số cho @Binding - Về mặt bộ nhớ thì sẽ không bị duplicate hay copy vùng nhớ.
4. Callback
Phần tiếp theo cần quan tâm đó là Call back. Nhưng trong mô hình phát triển Declarative App thì hầu như không cần thiết mấy. Nhưng cũng đôi lúc, chúng ta cần sự phản hồi trở lại từ Custom View khi kết thúc một sự kiện nào đó.
Công việc này tương tự như viết Delegate cho Custom View ở UIKit vậy.
Bắt đầu, tại file UserView. Bạn tiếp tục thêm thuộc tính mới. Nó là một closure
đơn giản kết hợp optional
. Để đảm bảo không cần phải gán giá trị lúc khởi tạo đối tượng.
var action: (() -> Void)?
Ta thêm một function mới tên là onChanged
như sau:
func onChanged(action: @escaping () -> Void ) -> Self { var copy = self copy.action = action return copy }
Trong đó:
- Nhân bản đối tượng hiện tại lên
- Gán
action
bằng với action được truyền vào từ view bên ngoài - Return lại chính giá trị vừa mới được nhân bản kia.
Trong UserView, khi bạn muốn nơi nào call back
thì sẽ gọi ở đó. Ta có ví dụ sau để gọi call back tại action của Button.
Button(action: { isSelected.toggle() if isSelected { count += 1 } else { count -= 1 } action!() }) { Image(systemName: isSelected ? "checkmark.square" : "square") .foregroundColor(.green) } .frame(width: 100.0, height: 25.0)
Sang file ContentView, ta sử dụng nó như một Modifier của UserView.
VStack { Text("Selected \(count) users.") .font(.title) HStack { UserView(name: "User 1", count: $count) .onChanged { count += 999 } UserView(name: "User 2", count: $count) } }
Khi user 1 kích vào Button thì onChanged
sẽ được gọi. Ta sẽ cộng thêm một số lượng lớn vào cho dễ nhận biết. Bạn bấm Live Preview và xem lại kết quả nào.
5. The Order modifiers
Cuối cùng, chúng ta phải chú ý tới thứ tự của các lần gọi Modifiers của một View. Vì nó sẽ quyết định hiển thị View hay Reusable View của bạn trông như thế nào. Ta xem qua 2 ví dụ sau:
Ví dụ 1:
Text("Selected \(count) users.") .padding() .font(.title) .foregroundColor(Color.white) .background(Color(.red))
Ví dụ 2:
Text("Selected \(count) users.") .font(.title) .foregroundColor(Color.white) .background(Color(.red)) .padding()
Chỉ cần thay đổi thứ tự gọi .padding()
thì View của chúng ta sẽ biến đổi khác nhau. Nguyên nhân là gì?
- Sau mỗi lần thay đổi thuộc tính, nó được gọi là modifier.
- Với modifier thì một phiên bản View mới được tạo ra, sau đó sẽ gởi trả về cho View cha hiển thị
- Nên thứ tự các modifier rất quan trọng trong việc quyết định hiển thị của View
Nó khác với UIKit và khác với Imperative app, khi việc thay đổi thuộc tính thì chỉ thay đổi giá trị. Đối tượng đó vẫn còn và không bị thay đổi. Nên khi hiển thị thì sẽ giống nhau.
Bạn hết sức chú ý thứ tự các Modifiers trong việc phát triển SwiftUI App của mình.
Tạm kết
- Cách tạo một View và tái sử dụng chúng. Hay còn gọi là Custom View
- Các loại thuộc tính và cách ràng buộc dữ liệu vào UI
- @State và @Binding trong việc liên kết & chia sẽ giá trị của các thuộc tính
- Cách thực hiện Call back trong SwiftUI
- Việc ảnh hưởng của Thứ tự các Modifiers tới việc hiển thị của giao diện
Okay! Tới đây, mình xin kết thúc bài viết này. 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
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- 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
You may also like:
Archives
- December 2024 (3)
- 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)