Contents
Chào mừng bạn đến với Fx Studio. Chúng ta đã bắt đầu với việc hiển thị một danh sách với dữ liệu tĩnh tại bài viết trước rồi. Bài viết này, sẽ tiếp tục hướng dẫn bạn hiển thị một danh sách động cùng với List. Chủ đề hôm nay là Dynamic List, bài viết thuộc series SwiftUI Notes.
Nếu bạn chưa biết về cách hiển thị một danh sách đơn giản, thì có thể tham khảo link dưới đây:
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
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. Mình sẽ thực hiện các View riêng biệt với nhau, nên bạn không cần lo lắng gì nhiều về tính liên kết của các View trong một Project. Về mặt giao diện thì khá là đơn giản à.
(Hoặc bạn có thể checkout project demo tại đây.)
Dynamic List
Chúng ta đã có bài viết hướng dẫn về cách hiển thị một danh sách với List & ForEach rồi. Tuy nhiên, chúng chỉ là cách hiển thị với các dữ liệu tĩnh hay cố định. Hoặc các dữ liệu tăng tuyến tính mà thôi.
Trong dự án thực tế, chúng ta không thể nào hiển thị dữ liệu như vậy được. Với ứng dụng mobile, sẽ luôn nhận được một danh sách với dữ liệu lấy về từ API. Chúng ta không biết trước được việc hiển thị chúng sẽ là bao nhiêu phần tử, hay cụ thể các phần tử sẽ như thế nào …
Do đó, chúng ta sẽ phải làm được việc là hiển thị một Array đối tượng lên List hay ForEach. Đây sẽ là tiền đề cho bạn trong các bài viết sau, với dữ liệu từ API hay từ DataBase.
Hiển thị array String
Chúng ta sẽ bắt đầu từ cái đơn giản trước. Đó là bạn sẽ hiển thị một Array String lên giao diện. Array String này được xem là phần dữ liệu cho View.
Ví dụ ta có 1 array như sau:
let cities = ["Hà Nội", "Hải Phòng", "Vinh", "Huế", "Đà Nẵng", "Nha Trang", "Hồ Chí Minh", "Vũng Tàu"]
Giải pháp đầu tiên sẽ là sử dụng ForEach, vì nó có index
. Từ index
bạn sẽ lấy được dữ liệu từng phần tử trong Array. Bạn xem tiếp ví dụ nha:
var body: some View { ScrollView { ForEach(0..<cities.count) { index in Text(cities[index]) } } }
Đó là ưu điểm của ForEach đem lại cho bạn. Khi chúng ta có được index
để duyệt các phần tử trong một mãng. Bạn bấm Resume và xem kết quả nhoé.
Kết quả nhìn không được đẹp cho lắm nhĩ. Chúng ta sẽ cải thiện nó tiếp thôi.
Sử dụng List
Phiên bản trên nhìn hơi xấu, nó không đem lại cho chúng ta cảm giác là một danh sách hay UITableView như với UIKit. Để cải thiện thì bạn sẽ sử dụng List, mặc dù chúng ta sẽ không có được index
như ForEach.
Nhưng với control này, SwiftUI hỗ trợ thêm nhiều tham số có ý nghĩa cao hơn. Chúng ta sẽ chỉnh sửa lại code trên với List như sau:
var body: some View { List(cities, id: \.self) { city in Text(city) } }
Trong đó:
cities
là phần dữ liệu cho Listid
làm tham số được thêm vào để xác định mỗi phần tử trongcities
là duy nhất.- Vì phần tử của
cities
là một String, mà không phải là một cấu trúc dữ liệu phức tạp. Do đó, ta sẽ lấykeypath
chính là bản thân nóself
để làmid
Để dễ hiểu hơn, thì trước đây chúng ta gọi id
là identifier
cho các định danh của UITableViewCell của UIKit. Bấm Resume và xem kết quả như thế nào nhoé.
Bây giờ thì trông đẹp rồi đó, đã có trải nghiệm giống UITableView lúc xưa rồi. Ahihi!
Sử dụng kiểu dữ liệu phức tạp
Chúng ta lại tiếp tục tìm một giải pháp toàn diện hơn. Bắt đầu, chúng ta sẽ định nghĩa lại kiểu dữ liệu cần dùng cho List. Sử dụng struct để tạo ra một kiểu dữ liệu phức tạp hơn.
Ví dụ với struct Weather như sau:
struct Weather { enum WeatherStatus { case sun case rain case cloud } var city: String var country: String var temperature: Int var status: WeatherStatus func getStatusInfo() -> String { var str = "" switch status { case .sun: str = "sun.max.fill" case .rain: str = "cloud.bolt.rain" case .cloud: str = "cloud.fill" } return str } }
Chúng ta sẽ dùng nó làm dữ liệu cho các phần tiếp theo của bài viết.
Tiếp theo, bạn sẽ custom một SwiftUI View để phục vụ cho việc hiển thị được đẹp hơn. Xem ví dụ nha:
struct WeatherRow: View { var weather: Weather var body: some View { HStack { Image(systemName: weather.getStatusInfo()) .resizable() .padding(.all) .frame(width: 80.0, height: 80.0) .aspectRatio(contentMode: .fill) VStack(alignment: .leading) { Text(weather.city) .font(.title) Text(weather.country) .fontWeight(.thin) } Spacer() Text("\(weather.temperature)°C") .font(.largeTitle) .fontWeight(.bold) .multilineTextAlignment(.center) .padding(.all) } } }
Cũng không quá phức tạp, bạn xem Preview của nó như sau. Bạn hãy tạo 1 đối tượng Weather để truyền vào Preview nhoé.
Cuối cùng bạn cần tạo mới một SwiftUI View để hiển thị danh sách thời tiết cho các thành phố nhoé. Xem ví dụ sau:
struct WeatherList: View { var weathers = Weather.dummyData() var body: some View { List(weathers, id: \.city) { item in WeatherRow(weather: item) } } }
Trong đó:
weathers
là một Array với kiểu là Weather.dummyData()
để sinh ra dữ liệu cho thuộc tính, bạn tự viết phần này nhoé. Còn không thì tham khảo source code của repo- Sử dụng
id
vớikey path
là thuộc tínhcity
Kết quả sẽ trông đẹp mắt hơn nhiều so với 1 Array String truyền thống. Xem nhoé!
Identifiable Protocol
Ở phần trên, ta sử dụng tới id
hay là một Identifier để định danh các phần tử là duy nhất trong một Array. Tuy nhiên, chúng vẫn có thể trùng lặp, nếu thuộc tính bạn lựa chọn là id
có thể xảy ra trùng lặp với nhau về giá trị.
Do đó, Swift cung cấp cho chúng ta một Protocol là Identifiable. Sẽ giúp cho class/struct bạn có thêm được tính năng định danh riêng biệt. Điều này giúp các phần tử trong Array sẽ là duy nhất.
public protocol Identifiable { /// A type representing the stable identity of the entity associated with /// an instance. associatedtype ID : Hashable /// The stable identity of the entity associated with this instance. var id: Self.ID { get } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension Identifiable where Self : AnyObject { /// The stable identity of the entity associated with this instance. public var id: ObjectIdentifier { get } }
Việc kế thừa Identifiable Protocol cho các struct/class, thì cần bạn khai báo thêm thuộc tính id
. Với kiểu dữ liệu cho id
đảm bảo được là một Hashable value
.
Chúng ta sẽ nâng cấp struct Weather trên với việc sử dụng Identifiable Protocol.
struct Weather: Identifiable { enum WeatherStatus { case sun case rain case cloud } var id = UUID() var city: String var country: String var temperature: Int var status: WeatherStatus func getStatusInfo() -> String { var str = "" switch status { case .sun: str = "sun.max.fill" case .rain: str = "cloud.bolt.rain" case .cloud: str = "cloud.fill" } return str } }
Trong đó:
- Chúng ta implement thêm Identifiable Protocol cho Weather
- Với Identifiable Protocol, cần phải có thêm một thuộc tính là
id
. Và bạn sẽ phải khai báo thêm thuộc tính đó. id
sẽ phải là các giá trị mà đảm bảo chúng là duy nhất.- Sử dụng
UUID()
để có được một chuỗi String & đảm bảo là duy nhất
Cuối cùng, bạn chỉ cần edit nhẹ phần List lại như sau:
struct WeatherList: View { var weathers = Weather.dummyData() var body: some View { List(weathers) { item in WeatherRow(weather: item) } } }
Lúc này, code của chúng ta nhìn đẹp hơn nhiều rồi. Và khi đối tượng chúng ta là một Identifiable, thì không cần tới tham số id
như ở trên.
Bạn hãy bấm Resume và cảm nhận kết quả nhoé!
NavigationView
A view for presenting a stack of views representing a visible path in a navigation hierarchy.
Chúng ta đã có được một Dynamic List hiển thị một danh sách với dữ liệu động cho giao diện rồi. Để trải nghiệm được đẹp hơn nữa, ta sẽ sử dụng thêm một NavigationView cho giao diện của chúng ta. Vì mục đích của chúng ta không chỉ đơn giản là xem 1 danh sách với các phần tử của nó mà thôi.
Mục đích chính sẽ là điều hướng từ List sang các View khác.
Đây là phiên bản của UINavigationViewController từ UIKit cho SwiftUI.
Cú pháp
Chúng ta đi sơ qua các cú pháp cơ bản nhất của một NavigationView trước nhoé. Tạo một NavigationView:
NavigationView { List { Text("Hello World") } .navigationBarTitle(Text("Navigation Title")) // Default to large title style }
Trong đó:
- NavigationView đóng vài trò là
root
của View - Chúng ta sẽ đưa vào trong nó các SwiftUI View cần thiết, trong ví dụ là một List
Tiếp theo, bạn muốn thay đổi title
của NavigationView, bạn sẽ sử dụng modifier .navigationBarTitle(_:_:)
. Ví dụ như sau:
.navigationBarTitle(Text("Navigation Title"), displayMode: .inline)
Bạn có thể chọn các kiểu hiển thị của Title với tham số cho displayMode
.
Cuối cùng, bạn có thể thêm các BarButtonItem cho NavigationBar. Xem ví dụ nha:
//barbuttonitems. : traing & leading .navigationBarItems(trailing: Button(action: { // Add action }, label: { Text("Add") }) )
Xem lại cập nhật cho WeatherList của chúng ta với NavigationView nhoé!
struct WeatherList: View { var weathers = Weather.dummyData() var body: some View { NavigationView { List(weathers) { item in WeatherRow(weather: item) } .navigationBarTitle(Text("Weather List")) .navigationBarItems(trailing: Button(action: { //action }, label: { Text("Add") }) ) } } }
Bấm Resume và cảm nhận kết quả nhoé!
NavigationLink
Quan trọng nhất khi sử dụng NavigationView đó là sự điều hướng. Các để bạn đưa một View khác vào bằng NaviagtionView đó là sử dụng đối tượng NaviagtionLink.
Sẽ có nhiều điều bất ngờ tại đây khi bạn từ UIKit chuyển sang SwiftUI nhoé.
Trong đó cần:
destination
là điểm đến. Chính là View của bạn muốn đưa vào.label
chính là nội dung của bạn cần hiển thị cho NavigationLink.
Xem ví dụ với WeatherList của chúng ta nhoé.
struct WeatherList: View { var weathers = Weather.dummyData() var body: some View { NavigationView { List(weathers) { item in NavigationLink( destination: WeatherDetail(), label: { WeatherRow(weather: item) }) } .navigationBarTitle(Text("Weather List")) } } }
Trong đó, ta sử dụng NavigationLink để bọc các WeatherRow lại với nhau. Mỗi khi kích vào Row thì sẽ được di chuyển sang màn hình WeatherDetail.
Kết quả sẽ như thế này!
Sẽ có một cái dấu >
nhỏ nhỏ đó bạn à. Nhìn cũng đẹp và tương đối ổn đó.
Chúng ta sẽ gặp lại NavigationView ở các phần sau hoặc 1 chương khác. Ở đây chỉ là giới thiệu sơ bạn để sử dụng trong List mà thôi.
Master – Detail
Đây là mẫu design được sử dụng khá nhiều trong các ứng dụng mobile. Với List sẽ là một Master View. Dùng chính điều hướng của NavigationView để chuyển sang các màn hình Detail View.
Detail View
Ở ví dụ trên, bạn có điểm đích của NavigationView là một WeatherDetail. Đó chính là màn hình cho Detail trong mô hình Master – Detail của chúng ta.
Lúc này, ta sẽ design lại WeatherDetail như sau, bạn tham khảo code nhoé.
struct WeatherDetail: View { var weather: Weather var body: some View { VStack(alignment: .center) { CircleImage(name: "img1") HStack { Image(weather.getStatusInfo()) .padding(.all) .frame(width: 80.0, height: 80.0) .aspectRatio(contentMode: .fill) VStack(alignment: .leading) { Text(weather.city) .font(.title) Text(weather.country) .fontWeight(.thin) } Spacer() Text("\(weather.temperature)°C") .font(.largeTitle) .fontWeight(.bold) .multilineTextAlignment(.center) .padding(.all) } Spacer() }.navigationBarTitle(Text(weather.city), displayMode: .inline) } }
Trong đó:
- Chúng ta có dữ liệu của màn hình là một thuộc tính
weather
. Sẽ được truyền từ List vào. - Bạn cần custom view cho Image, trong ví dụ mình tạo một custom view là CircleImage. Giúp cho ảnh của bạn được bo tròn và bóng đổ.
Xem kết quả nhoé!
Mình có thay đổi lại icon cho các biểu tượng thời tiết, để nhìn cute hơn. Ahihi!
Master View
Master View trong mô hình Master – Detail là một danh sách. Tại đó:
- Điều hướng sang các Detail View khác nhau
- Lưu trữ dữ liệu tập trung
- Chuyển dữ liệu cho các Detail View
Trong ví dụ của chúng ta thì đã sử dụng List làm Master View rồi. Nhiệm vụ cuối cùng là kết nối và truyền dữ liệu sang Detail View thôi. Xem ví dụ code nhoé.
List(weathers) { item in NavigationLink( destination: WeatherDetail(weather: item), label: { WeatherRow(weather: item) }) } .navigationBarTitle(Text("Weather List"))
Bạn chỉ cần quan tâm tới dòng code có WeatherDetail(weather: item)
là xong. Tất cả chỉ có như vậy mà thôi. Bạn hãy bấm Preview và test lại các View của chúng ta nhoé. Chúc bạn thành công!
Tạm kết
- Hiển thị một danh sách với dữ liệu động từ một array
- Các cách tạo các Identifier từ các đối tượng trong array
- Custom một class/struct với Identifiable Protocol để sử dụng làm dữ liệu cho List
- Cơ bản về NavigationView & NavigationLink
- Mô hình Master – Detail trong SwiftUI
Okay! Tới đây, mình xin kết thúc bài viết về Dynamic List 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
- 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)