Sử dụng Custom UIView vào SwiftUI Project – SwiftUI Notes #16
SwiftUI . TutorialsContents
Chào bạn đến với Fx Studio. Chúng ta đã được tìm hiểu về tích hợp qua lại giữa SwiftUI & UIKit trong các bài viết trước. Và bài viết hôm nay sẽ trình bày việc tích hợp cho thể loại / thành phần cuối cùng. Đó là Custom UIView.
Bạn có thể xem lại các viết về tích hợp giữa SwiftUI & UIKit tại các link sau:
Và nếu như mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Bạn cần phải chuẩn bị thông số về môi trường như sau:
-
- MacOS 10.15.x
- Swift 5.3
- iOS 13.x (hoặc mới hơn)
- SwiftUI 2.0
Vì demo sử dụng cho bài viết lần này vừa dùng cả SwiftUI Project & UIKit Project. Nên bắt buộc iOS phải là 13 (hoặc mới hơn). Ngoài ra, bạn có thể checkout các demo của cả series tại đây.
1. Create a Custom UIView
Bắt đầu, bạn cần có một Custom UIView. Bạn có thể tạo View này ở UIKit Project hoặc ở SwiftUI Project đều được. Xcode của Apple vẫn bá đạo như ngày nào, nó cho phép bạn có thể sử dụng cả 2 hay nhiều nền tảng của Apple vào cùng một project.
1.1. Create View
Về giao diện của Custom UIView này thì mình làm cực kì đơn giản. Bạn hãy tạo một file Swift và một file *.xib
với giao diện như hình sau:
Đặt tên là AvatarView. Bạn xem code của nó như sau:
import UIKit class AvatarView: UIView { //MARK: Outlets @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var nameLabel: UILabel! //MARK: Properties var name: String = "Noname" var color: UIColor = .blue //MARK: ConfigView func updateView() { nameLabel.text = name avatarImageView.tintColor = color } //MARK: Actions @IBAction func tap(_ sender: Any) { // code here } }
Trong đó:
- AvatarView là một class. Khác với các SwiftUI View là các struct
- 2 thuộc tính sẽ dùng nhận dữ liệu với bên ngoài là
name
vàcolor
- View nhận sự kiện của người dùng tại function IBAction
tap
Mục đích cho Custom UIView này được dùng:
- Nhận dữ liệu truyền từ các SwiftUI View khác
- Gởi sự kiện người dùng cho các SwiftUI View
1.2. Protocol
Về việc truyền dữ liệu (Passing data) từ Custom UIView cho các đối tượng khác, thì bạn có rất nhiều cách để sử dụng. Mình sẽ liệt kê như sau:
- Protocol
- Callback closure
- Notification
- UserDefault
- …
Trong demo này mình sẽ chọn Protocol, vì nó là hiệu quả nhất trong mô hình của Declarative App với SwiftUI. Bạn tiếp tục tạo thêm một Protocol Delegate như sau:
protocol AvatarViewDelegate: class { func avatarView(avatarView: AvatarView, name: String) }
Tại AvatarView, bạn thêm thuộc tính delegate
. Cần lưu ý cho thuộc tính này:
weak
để tạo liên kết yếu, tránh chiếm giữ bộ nhớ
weak var delegate: AvatarViewDelegate?
Sử dụng delegate tại IBAction tap
.
@IBAction func tap(_ sender: Any) { if let delegate = delegate { delegate.avatarView(avatarView: self, name: name) } }
Vì là Optional nên cần phải sử dụng if let
để unwrapping nó một cách an toàn. Nhiều bạn dev iOS thường hay sử dụng dấu ?
hoặc !
, tuy nhiên nó sẽ là nguyên nhân tiềm tàng gây crash ứng dụng. Chịu khó tí vẫn hơn!
2. UIViewRepresentable with Custom UIView
Chúng ta đã có một Custom UIView, công việc tiếp theo sẽ là tạo một SwiftUI View từ Custom UIView đó. Và Custom UIView thực chất là sub-class của UIView. Do đó, bạn có thể sử dụng tới protocol UIViewRepresentable để tiến hành đưa Custom UIView vào SwiftUI Project.
2.1. Create Representation
Bắt đầu phần này bằng việc tạo một file SwiftUI View mới và implement protocol UIViewRepresentable. Mình sẽ đặt tên là MyAvatar. Code tham khảo như sau:
struct MyAvatar: UIViewRepresentable { typealias UIViewType = AvatarView func makeUIView(context: Context) -> AvatarView { let avatarView = Bundle.main.loadNibNamed("AvatarView", owner: nil, options: nil)?.first as! AvatarView return avatarView } func updateUIView(_ uiView: AvatarView, context: Context) { print("updating") } }
Vẫn là những thành phần quen thuộc cho UIViewRepresentable, trong đó:
- Typealias với AvatarView, giúp cho việc xác định cụ thể kiểu của đối tượng UIKit được sử dụng
- Tại function
makeUIView
bạn cần tạo đối tượng Custom UIView vàreturn
nó về. Ta sử dụng phương pháploadNibName
truyền thống cho Custom UIView - Function
updateUIView
dùng để cập nhất lại đối tượng UIView khi có sự thay đổi từ bên ngoài.
2.2. Preview
Preview là đặc sản của SwiftUI và chúng ta cũng nên tạo thêm nó cho đối tượng View trên một Preview. Mục đích bạn có thể xem giao diện một cách trực quan.
Bạn tạo tiếp struct cho Preview này như sau:
struct MyAvatar_Preview : PreviewProvider { static var previews: some View { MyAvatar() } }
Lúc này phần Canvas sẽ hiện ra. Bạn hãy bấm Resume để xem kết quả nào.
2.3. Update UIView
Cơ bản thì bạn đã có một Custom UIView và một SwiftUI View tạo ra từ nó rồi. Tiếp tục, bạn cần tạo thêm các thuộc tính để nhận các giá trị từ bên ngoài và cho View hiển thị.
Bạn mở MyAvatar lên và thêm các thuộc tính sau:
var name: String var color: UIColor { UIColor(red: CGFloat(redValue), green: CGFloat(greenValue), blue: CGFloat(blueValue), alpha: 1.0) } @Binding var redValue: Double @Binding var blueValue: Double @Binding var greenValue: Double
Trong đó:
name
&color
là các thuộc tính để dùng tương tác với đối tượng Custom UIView của UIKit- các thuộc tính với khai báo
@Binding
dùng để liên kết dữ liệu với các đối tượng SwiftUI View từ bên ngoài. Nó sẽ tự động cập nhật giá trị mới
Bạn tiếp tục cập nhật các function chính của SwiftUI View này.
func makeUIView(context: Context) -> AvatarView { let avatarView = Bundle.main.loadNibNamed("AvatarView", owner: nil, options: nil)?.first as! AvatarView avatarView.name = name avatarView.color = color avatarView.updateView() return avatarView } func updateUIView(_ uiView: AvatarView, context: Context) { print("updating") uiView.name = name uiView.color = color uiView.updateView() }
Về giải thích thì mình đã giải thích nhiều rồi và tóm tắt thì như sau:
- Tại
makeUIView
cần các giá trị ban đầu cho việc khởi tạo một UIView - Tại
updateUIView
thì sẽ cập nhật lại các giá trị (đã thay đổi từ bên ngoài) của đối tượng UIView thông qua tham sốuiView
Việc thêm các thuộc tính cho đối tượng SwiftUI View này dẫn tới bạn phải cập nhật lại Preview. Khởi tạo lại đối tượng MyAvatar với các đối số cần thiết.
struct MyAvatar_Preview : PreviewProvider { static var previews: some View { MyAvatar(name: "Fx Studio", redValue: .constant(1.0), blueValue: .constant(0.5), greenValue: .constant(0.5)) } }
Trong đó, khai báo .constant(_)
dùng để đưa dữ liệu cho các thuộc tính với khai báo là @Binding
. Bạn bấm lại nút Resume và tận hướng kết quả nha.
3. Coordinator with Protocol
Với function updateUIView
, ta có thể truyền dữ liệu từ ngoài cho đối tượng UIView. Còn chiều ngược lại từ đối tượng UIView để truyền ra ngoài thì chúng ta phải sử dụng tới Coordinator. Nó được xem là đối tượng trung gian để kết nối SwiftUI View (Representation) với UIView.
3.1. Create class
Bắt đầu, ta cần tạo class Coordinator ở bên trong struct SwiftUI View kia. Bạn xem code sau sẽ hiểu:
class Coordinator: NSObject, AvatarViewDelegate { var parent: MyAvatar init(_ parent: MyAvatar) { self.parent = parent } //MARK: Delegate func avatarView(avatarView: AvatarView, name: String) { parent.redValue = Double.random(in: 0...1) parent.blueValue = Double.random(in: 0...1) parent.greenValue = Double.random(in: 0...1) } }
Trong đó:
- Class sẽ implement thêm Protocol AvatarViewDelegate. Để cho class Coordinator sẽ là người đón nhận các sự kiện từ UIView trả về.
- Ta cần khai báo thêm thuộc tính
parent
để có thể sử dụng được các thuộc tính từ SwiftUI View - Cập nhật lại giá trị của SwiftUI View, vì chúng nó khai báo là
@Binding
nên bạn không cần lo lắng việc gởi dữ liệu đi hay cập nhật các nơi khác. Mọi thứ sẽ được tự động
Khi đã khai báo 1 class Coordinator bên trong một SwiftUI View thì bạn cần phải khai báo thêm function makeCoordinator
cho nó. Bạn tham khảo code sau.
func makeCoordinator() -> Coordinator { Coordinator(self) }
3.2. Implement Delegate
Như đã trình bày ở trên, bạn thấy class Coordinator: NSObject, AvatarViewDelegate { }
. Nó kế thừa lại AvatarViewDelegate rồi. Công việc cuối cùng còn lại là xác nhận .delegate
của đối tượng UIView.
Bạn về lại function makeUIView
và cập nhật thêm dòng code sau trước khi phải return UIView.
avatarView.delegate = context.coordinator
Như vậy, bạn đã setup xong mọi thứ cần thiết rồi. Tiếp tục là sang phần sử dụng đối tượng SwiftUI View mới.
4. Use Custom UIView
4.1. Layout
Bạn cập nhật lại bố cục giao diện với việc thêm một đối tượng MyAvatar được tạo ở trên vào màn hình. Mình sẽ sử dụng màn hình UserView để demo việc sử dụng MyAvatar này.
Bạn tham khảo lại cần code cho body
của UserView như sau:
var body: some View { VStack { HStack { VStack { MyAvatar(name: name, redValue: $redValue, blueValue: $blueValue, greenValue: $greenValue) } .frame(height: 300, alignment: .center) VStack { Image(systemName: "person.crop.square") .resizable() .aspectRatio(1.0, contentMode: .fit) .foregroundColor(Color(red: redValue, green: greenValue, blue: blueValue, opacity: 1.0)) Text(name) .fontWeight(.bold) .multilineTextAlignment(.center) Button(action: { print("Select: \(name)") if let action = action { action(name) } redValue = Double.random(in: 0...1) blueValue = Double.random(in: 0...1) greenValue = Double.random(in: 0...1) }) { Text("Tap me!") } } .frame(height: 300, alignment: .center) } VStack { MyColorUISlider(color: .red, value: $redValue) .frame(maxWidth: .infinity) MyColorUISlider(color: .blue, value: $blueValue) .frame(maxWidth: .infinity) MyColorUISlider(color: .systemGreen, value: $greenValue) .frame(maxWidth: .infinity) } } .padding() }
Bố cục mới như sau:
- 2 View hiển thị cho User sẽ ở trong 1 HStack
- HStack đó lại ở trong VStack
- Vẫn giữ lại các slider được tạo bằng UISlider của UIKit từ bài trước
Khởi tạo đối tượng MyAvatar thì như sau:
MyAvatar(name: name, redValue: $redValue, blueValue: $blueValue, greenValue: $greenValue)
Các đối số cho việc khởi tạo thì sử dụng trực tiếp các thuộc tính @State
từ UserView. Và bạn nhớ thêm $
để nó tự động cập nhật lại giá trị.
Cuối cùng, bạn bấm Resume để xem giao diện mới của UserView nha
4.2. Actions
Để thực hiện việc kiểm tra xem mọi đối tượng có hoạt động với nhau đúng theo thiết kế & ý muốn của mình hay không. Bạn hãy bấm Live Preview ở UserView và bắt đầu kiểm thử.
Trong đó, các sự kiện bao gồm như sau:
- 2 action
Tap me!
. Nó sẽ làm cho các thuộc tính@State
thay đổi giá trị một cách ngẫu nhiên. Các giá trị này sẽ ảnh hưởng tới nhiều View mà có sự ràng buộc dữ liệu với nó. (Slide & MyAvatar …) - Các sự kiện khi kéo slider. Các sự kiện này chỉ làm thay đổi 1 trong 3 thuộc tính
@State
. Tuy nhiên, chúng vẫn đủ sức làm cho các Userview thay đổi
Bạn có thể bấm Live Preview hoặc build cả project lên Simulator để test. Chúc bạn thành công!
Tạm kết
- Custom một UIView và thêm nó vào SwiftUI Project thông qua protocol UIViewRepresentable
- Cập nhật dữ liệu một cách tự động từ SwiftUI sang UIKit thông qua khai báo
@Binding
cho các thuộc tính - Truyền dữ liệu từ Custom UIView sang các View của SwiftUI thông qua Coordinator
- Tiến hành implement Delegate của Custom UIView cho Coordinator
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)