Contents
Chào mừng bạn đến với Fx Studio. Chúng ta tiếp tục hành trình bất tận trong vũ trụ SwiftUI. Chủ đề bài viết này là Section in Grid, cách bạn sẽ trình bày bố cục giao diện theo Grid Layout và có phân chia nội dung. Về bản chất, Section trong Grid Layout cũng tương tự như List hay UICollectionView. Nắm được cấu tạo & cách hoạt động của Section trong Grid sẽ giúp bạn chủ động hơn trong việc bố cục giao diện phực tạp.
Nếu bạn chưa biết gì về Grid Layout trong SwiftUI, thì có thể đọc lại các bài viết trước.
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 3.0
- Xcode 13
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.)
Section
Đây cũng là vấn đề muôn thuở. Bạn có nhiều danh sách cần được hiển thị hoặc chia thành nhiều bố cục khác nhau. Với List & TableView thì chúng ta sẽ chia chúng thành nhiều Section khác nhau. Và vấn đề này cũng lại xuất hiên trên Grid, nhưng …
Tại sao phải cần Section trong Grid làm gì?
Vấn đề
Để trả lời câu hỏi đó, thì chúng ta sẽ xem qua ví dụ code sau:
struct SectionInGridDemoView: View { private var configGridItem = [ GridItem(.fixed(100), spacing: 10), GridItem(.fixed(100), spacing: 10), GridItem(.fixed(100), spacing: 10) ] var body: some View { ScrollView(.vertical) { LazyVGrid( columns: configGridItem, spacing: 10 ) { ForEach(1...10, id: \.self) { index in Text("1.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } .padding(.horizontal) } .frame(maxHeight: .infinity) } }
Trong đó:
- Một LazyVGrid bình thường
- Config với 3 cột GridItem dạng
.fixed
- ForEach lặp từ
1
đến10
Kết quả sẽ trông như thế này.
Mọi thứ vẫn đẹp. Chúng ta sẽ thêm tiếp 1 đoạn ForEach tiếp tục nữa vào LazyVGrid để xem Grid biến đổi như thế nào.
ForEach(11...20, id: \.self) { index in Text("2.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) }
À há, vẫn không báo lỗi và không bị crash. Tuy nhiên … kết quả sẽ có một chút bất thường.
Vấn đề chính là đây. Bạn sẽ thấy các phần tử của ForEach thứ 2 tiếp tục nối vào ForEach thứ 1. Chúng liên kết với nhau tạo thành một thể thống nhất. Và không có sự tách biệt ở đây.
Ta sẽ thử dùng với Section xem như thế nào.
Add Section
Bạn sẽ cập nhật lại nội dung của LazyVGrid thêm các đối tượng Section cho các ForEach ở trên nhóe. Tham khảo code như sau:
Section { ForEach(1...9, id: \.self) { index in Text("1.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } Section { ForEach(10...19, id: \.self) { index in Text("2.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } }
Trong đó:
- Các đối tượng Section bọc lại các ForEach
- Các Section chứa trong LazyVGrid
Với các Section, giúp cho Grid của bạn tách biệt các phần nội dung hay các danh sách riêng lẻ với nhau. Bạn bấm Resume và xem kết quả nhóe!
Lúc này, mọi thứ nhìn sạch đẹp rồi đó. Hi vọng qua vài ví dụ nhỏ cũng giúp bạn hiểu được ý nghĩa vì sao cần Section trong Grid. Cũng tương tự như là với List. Mỗi đối tượng sinh ra đều có ý nghĩa riêng hết. Ahihi!
Header
Chúng ta tiếp tục sang công việc tiếp theo cần xử lý Section. Bạn cần phải hiển thị thêm nội dung cho Section. Như bạn cũng đã biết thì ta có 2 view phụ cho nó:
- Header
- Footer
Chúng ta sẽ demo với Header thôi nhóe, còn lại bạn tự xử.
Title
Bạn cần thêm các đối số cho header
của hàm khởi tạo của Section. Code thì khá đơn giản, vì chúng ta đã quá quen thuộc với List rồi. Bạn tham khảo thêm nhóe!
Section(header: Text("Section 1").font(.title)) { ForEach(1...10, id: \.self) { index in Text("1.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } Section(header: Text("Section 2").font(.title)) { ForEach(11...20, id: \.self) { index in Text("2.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } }
Sử dụng Text làm header
& tùy chỉnh thêm một ít với modifier .font
. Bấm Resume và xem kết quả nhóe!
Chỉ vài thao tác nhỏ thì bạn đã có được Title cho Section rồi nhóe.
Custom
Tất nhiên, chúng ta luôn sẽ phải custom view. Với header
thì cũng không ngoại lệ được. Điều này giúp bạn tiết kiệm code và tránh viết code quá nhiều trong khởi tạo một đối tượng.
Bạn sẽ thêm một function như sau:
private func customVHeader(with header: String) -> some View { Text(header) .font(.title2) .bold() .foregroundColor(.white) .padding() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .background(RoundedRectangle(cornerRadius: 10) .fill(Color.headerBackground)) }
Một function đơn giản và trả về với some View
. Hiển thị nội dung title được truyền cho tham số của function. Bạn thay thế vào tham số header
của Section.
var body: some View { ScrollView(.vertical) { LazyVGrid( columns: configGridItem, spacing: 10 ) { Section(header: customVHeader(with: "Section 1")) { ForEach(1...10, id: \.self) { index in Text("1.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } Section(header: customVHeader(with: "Section 2")) { ForEach(11...20, id: \.self) { index in Text("2.\(index)") .frame(maxWidth: .infinity, minHeight: 50) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } } .padding(.horizontal) } .frame(maxHeight: .infinity) }
Cũng khá đơn giản, ta cần truyền các String khác nhau để làm title cho mỗi Section. Cuối cùng, bấm Resume để xem tiếp kết quả nha!
Cũng ra gì và này nọ chứ. Bạn có thể tùy biến chúng đa dạng hơn nhiều so với ví dụ đơn giản của mình. Nhưng bạn sẽ chủ động hơn về mặt thiết kế giao diện cho ứng dụng của mình.
LazyHGrid
Chúng ta sẽ tiếp tục demo các Section trên cho đối tượng Grid thứ 2 là LazyHGrid. Mục đích để xem hiển thị như thế nào với các Section được scroll ngang. À, không phải đơn giản chỉ là thay cái tên từ LazyVGrid sang thành LazyHGrid nha.
Công việc cũng khá phức tạp.
Đầu tiên, ta cần tùy chỉnh lại lại array configGridItem
trước đã. Cho chúng có khả năng linh hoạt một tí với .adaptive
.
private var configGridItem = [ GridItem(.flexible(minimum: 100), spacing: 10), GridItem(.flexible(minimum: 100), spacing: 10), GridItem(.flexible(minimum: 100), spacing: 10) ]
Tiếp theo, ta sẽ phải thay đổi lại lại function Custom Header cho Section. Tham khảo đoạn code sau nhóe!
private func customHHeader(with header: String) -> some View { Text(header) .bold() .frame(minWidth: 70) .rotationEffect(Angle(degrees: -90)) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(RoundedRectangle(cornerRadius: 10) .fill(Color.headerBackground)) }
Bạn cần chú ý tới:
- Góc quay
.rotationEffect
frame
vớimaxWidth
&maxHeight
(đã hoán vị so với LazyVGrid)
Cuối cùng, là sử dụng LazyHGrid vào trong ScrollView nhóe. Tiếp tục tham khảo đoạn code sau:
var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHGrid( rows: configGridItem, spacing: 10 ) { Section(header: customHHeader(with: "Section 1")) { ForEach(1...10, id: \.self) { index in Text("1.\(index)") .frame(minWidth: 50, maxHeight: .infinity) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } Section(header: customHHeader(with: "Section 2")) { ForEach(11...20, id: \.self) { index in Text("2.\(index)") .frame(minWidth: 50, maxHeight: .infinity) .background(Color.random) .clipShape( RoundedRectangle(cornerRadius: 10.0) ) } } } .padding(.horizontal) } .frame(maxHeight: .infinity) }
Trong đó:
- ScrollView sẽ được kích hoạt scroll ngang với
.horizontal
- LazyHGrid sẽ cần cung cấp giá trị cho tham số
rows
- Custom Header của Section sẽ gọi tới function
customHHeader
- Mỗi item trong Grid cần điều chỉnh lại
frame
cho phù hợp
Mọi thứ còn lại thì không thay đổi gì nhiều. Bạn bấm Live Preview và xem kết quả nhóe.
Để thiết bị nằm ngang cho dễ quan sát. Bạn thử scroll qua lại xem như thế nào. Ahihi!
PinnedScrollableViews
PinnedScrollableViews là các View mà bạn có thể pin
(hay luôn hiển thị) trên Grid Layout. Đây là một tính năng mới được thêm cho các LazyVGrid & LazyHGrid.
Đem lại một trải nghiệm tương tự như với UIKit. Và bạn sẽ không cần triệu hồi các UIKit để thực hiện nữa. Tất cả, đều có sẵn và chỉ cần sử dụng mà thôi.
Còn với Grid Layout cho cả 2 đối tượng LazyVGrid & LazyHGrid, thì đều có tham số pinnedView
cho hàm khởi tạo của chúng. Ví dụ như sau:
LazyVGrid( columns: configGridItem, spacing: 10 pinnedViews: [.sectionHeaders, .sectionFooters] ) { // .... }
Trong đó:
.sectionHeaders
chính là các Header của Section.sectionFooters
chính là các Footer của Section
Khi các View được khởi tạo và bạn scroll chúng. Phần header
hoặc footer
sẽ ở nguyên vị trí của chúng, chỉ khi nào toàn bộ Section trôi qua thì chúng sẽ bị trôi theo.
Bạn hãy áp dụng vào 2 demo ở trên để xem kết quả nhóe, nhớ là thêm vài Section nữa (thay vì 2 cái như ví dụ).
- LazyVGrid
- LazyHGrid
Trông cũng khá ổn rồi đó, chúc bạn thành công nhóe. Ahihi!
Tạm kết
- Thêm các Section cho Grid Layout
- Custom các Header & Footer
- Tùy biến Section riêng với LazyHGrid & LazyVGrid
- Sử dụng các PinnedScrollableViews để dính các header hoặc footer trên Grid Layout
Okay! Tới đây, mình xin kết thúc bài viết về Section in Grid 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!
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
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)