Contents
Chào mừng bạn đến với Fx Studio. Chúng ta tiếp tục với hành trình bất tận cùng với series SwiftUI này. Chủ đề bài viết này là Modal View & Alert, một cách điều hướng khác với NaviagtionView.
Nếu bạn chưa biết gì về NavigationView, thì có thể tìm đọc các bài viết sau:
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.)
Setup Views
Tại bài viết trước, chúng ta đã làm việc với Login Flow. Và ta lại sử dụng chúng cho demo của bài viết này. Bài toán của chúng ta sẽ là hiển thị một View cho các câu hỏi điều khoản trong chương trình. Đặt tên là FAQView.
Cách hiển thị của FAQView thì sẽ từ dưới lên trên. Hay còn gọi là present
hoặc modal
. Nhưng trước tiên, bạn có thể xem qua code của FAQView để tham khảo.
struct FAQView: View { @State var items = FAQItem.dummyData() var body: some View { List { ForEach(0..<items.count) { index in DisclosureGroup( isExpanded: $items[index].showContent, content: { Text(items[index].answer) .font(.body) .fontWeight(.light) }, label: { Text("\(index + 1). \(items[index].question)") .font(.body) .fontWeight(.bold) }) } } .listStyle(InsetListStyle()) } }
Trong đó, dữ liệu cho FAQView là 1 mãng FAQItem. Bạn có thể tham khảo qua đọc code sau:
struct FAQItem: Identifiable { var id = UUID() var question: String var answer: String var showContent = true } extension FAQItem { static func dummyData() -> [FAQItem] { return [FAQItem(question: "Fx Studio có phải là blog tốt nhất không?", answer: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book."), FAQItem(question: "Các cách để bạn donate cho Fx Studio?", answer: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."), FAQItem(question: "Admin của Fx Studio là người như thế nào?", answer: "t is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. "), FAQItem(question: "Có bao nhiều channel của Fx Studio?", answer: "Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. "), FAQItem(question: "Bạn có biết fanpage của Fx Studio?", answer: "There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable."), FAQItem(question: "Đâu là nơi bạn có thể học Combine & SwiftUI tốt nhất?", answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."), FAQItem(question: "Bạn có thể gởi câu hỏi thông qua các hình thức nào?", answer: "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. "), FAQItem(question: "Làm thế nào subscribe website?", answer: "On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. "), FAQItem(question: "RxSwift có thực sự tốt không?", answer: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), FAQItem(question: "Tương lại của lập trình iOS sẽ như thế nào?", answer: "Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. ")] } }
Kết quả ban đầu trông như sau:
Modality
Modality là kỹ thuật thiết kế trình bày nội dung ở chế độ tạm thời tách biệt với ngữ cảnh hiện tại trước đó của người dùng và yêu cầu hành động rõ ràng để thoát. Trình bày nội dung theo phương thức có thể:
- Giúp mọi người tập trung vào một nhiệm vụ khép kín hoặc tập hợp các tùy chọn có liên quan chặt chẽ
- Đảm bảo rằng mọi người nhận được và nếu cần thiết, hành động dựa trên thông tin quan trọng
Các loại Modality trong iOS cung cấp như là: Alert, Modal View, Activity View (hay Share sheets) & Action Sheets. Chúng ta sẽ tập trung vào 2 loại chính thôi.
- Alert
- Modal View
Bạn sẽ tìm hiểu & học cách sử dụng chúng qua các phần dưới đây.
Modal View
Đây là một hiển thị cũng khá là quen thuộc trong iOS. Khi View sẽ được hiển thị từ dưới lên trên. Nó sẽ phủ toàn bộ màn hình (full screen) hoặc chừa lại một phần màn hình. Nhằm giúp cho người dùng có thể chạm và kéo nó xuống để ẩn đi (dismiss).
Sheet
Việc hiển thị này giống như 1 thẻ (card) được xếp chồng lên Content View hiện tại của bạn. Hay còn gọi là sheet presentation. Và bạn sẽ dùng modifier .sheet
để đưa nội dung cần hiển thị trên.
.sheet(isPresented: $showModal) { DetailView() }
Tham số isPresented
sẽ được ràng buộc với một State. Khi giá trị của thuộc tính State là true
thì DetaiView sẽ được xuất hiện.
Present View
Bạn đã có View làm demo & modifier giúp hiển thị rồi. Bắt tay vào việc thôi. Trước tiên ta sẽ sử dụng lại LoginView (ở bài viết trước), thêm một thuộc tính State để quản lý việc hiện thị Modal View của nó.
@State var isPresent = false
Tiếp theo, bạn lại cần cái cớ để hiển thị Modal View nhé. Ta sẽ thêm một Button đơn giản như sau:
Button { isPresent = true } label: { Text("FAQ") } .buttonStyle(DefaultButtonStyle())
Trong đó:
- Tại
action
của Button, ta sẽ xét lại giá trị thuộc tínhisPresent = true
- Còn lại tuỳ ý bạn sử dụng nó với giao diện như thế nào và đặt ở đâu
Cuối cùng, bạn sẽ cài đặt modifier .sheet
cho phù hợp. Bạn tham khảo nhoé.
NavigationView { VStack { // .... } .sheet(isPresented: $isPresent) { // dismis } content: { // content FAQView() } }
Trong đó:
.sheet
sẽ sử dụng trong NaviagtionView và cho View con của nó.- Trong trường hợp này thì chính là VStack của bạn
- Bạn sẽ truyền
$isPresent
cho tham sốisPresented
dismiss
thì sử dụng khi bạn muốn làm gì sau khi Modal View bị ẩn đicontent
chính là nội dung ta cần hiển thị.
Bấm Live Preview và test lại nhoé.
Present with Optional Binding
Bạn còn 1 cách nữa để hiển thị một Modal View với một giá trị Optional Binding. Đó là sử dụng modifier .sheet
với tham số item
thay cho isPresented
.
Ví dụ như:
.sheet(item: self.$selectedArticle) { article in ArticleDetailView(article: article) }
Với cách này, bạn cần khai báo thêm 1 biến selectedArticle
là một kiểu Optional. Khi nó được gán giá trị thì với Binding ràng buộc với sheet
, sẽ tự động kích hoạt content view hiển thị.
Cách dùng này sẽ được sử dụng nhiều với List. Khi bạn muốn dữ liệu của Modal View của bạn là dữ liệu động.
(Mình không demo cách này & bạn tự xử nhoé!)
Dismiss View
Hiển thị được rồi, chúng ta sẽ tìm cách ẩn chúng đi nhoé. Và ta cũng có 2 cách luôn à. Bạn sẽ tìm hiểu lần lượt như sau.
Environment
Tại View content của Modal View là FAQView, chúng ta sẽ thêm một thuộc tính với kiểu là @Environment
nhoé.
@Environment(\.presentationMode) var presentationMode
presentationMode
sẽ quản lý việc present của Modal View. Cách này bạn dùng với iOS 14.0 trở xuống.
Và với iOS 15 thì chúng ta có một
keypath
đơn giản hơn là\.dismiss
.
Vì khi bạn muốn dismiss
với presentationMode
thì sẽ như thế này.
presentationMode.wrappedValue.dismiss()
Bạn phải dùng tới wrappedValue
của nó. Nhìn hơi xấu nhĩ. Còn tiếp theo, bạn sẽ làm thêm 1 Button nhằm để làm sự kiện cho việc dismiss
nhoé. Tham khảo code sau:
List { Section { VStack(alignment: .center) { Button { presentationMode.wrappedValue.dismiss() } label: { Text("Dismiss") } .buttonStyle(BlueButton()) } .frame(maxWidth: .infinity) } // .... }
Không biết nhét nó ở đâu, nên mình sẽ tạo 1 Button và đưa vào Section cho List. Quan trọng tại action
của Button là nơi bạn gọi presentationMode.wrappedValue.dismiss()
để ẩn đi Modal View.
Bấn Live Preview và test nhoé!
Binding
Cách thứ 2, bạn sẽ tác động tới thuộc tính State từ View khác. Chính xác đó là bạn sử dụng thuộc tình Binding tại Modal View nhằm thay đổi giá trị của State. Và theo nguyên tắc The single source of truth thì sẽ lôi được Modal View xuống thôi. Khá là EZ!
Đầu tiên, bạn cần khai báo thêm một thuộc tính tại Modal View như sau:
@Binding var isPresent: Bool
Cập nhật lại Button nữa.
Button { //presentationMode.wrappedValue.dismiss() isPresent = false } label: { Text("Dismiss") } .buttonStyle(BlueButton())
Thay thế cách dùng presentationMode
bằng việc xét lại giá trị cho isPresent = false
là xong.
Cuối cùng, bạn cập nhật lại ở LoginView với khởi tạo FAQView với tham số isPresent
nhoé.
.sheet(isPresented: $isPresent) { // dismis } content: { // content FAQView(isPresent: $isPresent) }
Bấm Live Preview và tiếp tục test nha.
Float Button for Dismiss
Ta sẽ cải thiện tiếp giao diện cho Modal View, vì với FAQView kìa thì quá xấu rồi. Bên cạnh đó ta sẽ tạo ra một Button lơ lững (Float Button) trông sẽ đẹp hơn nhiều.
- Tạo lại
title
cho Section
Section { VStack(alignment: .center) { Text("FAQs") .font(.title) .fontWeight(.bold) .italic() .frame(maxWidth: .infinity) } }
- Tạo Float Button
List { // ... } .listStyle(InsetListStyle()) .overlay( HStack { Spacer() VStack { Button(action: { self.presentationMode.wrappedValue.dismiss() }, label: { Image(systemName: "chevron.down.circle.fill") .font(.largeTitle) .foregroundColor(.accentColor) }) .padding(.trailing, 20) .padding(.top, 10) Spacer() } } )
Trong đó:
- Float Button sẽ ở tại 1 vị trí và không bị ảnh hưởng khi scroll. Do đó, ta lợi dụng
overlay
của View - Để xác định vị trí, bạn sử dụng các thuộc tính
.padding
Ta sẽ dùng lại các cách dismiss
như ở trên, tại action
của Button. Trong ví dụ trên mình sử dụng self.presentationMode.wrappedValue.dismiss()
.
Bạn bấm Live Prevew và test lại nhoé!
Đẹp rồi nhóe!
Alert
Chúng ta tiếp tục với View đại diện thứ 2, đó là Alert. Về ngữ nghĩa, nó cũng tương tự như Modal View. Nhưng nó sẽ block giao diện xung quanh nó. Nội dung sẽ được hiển thị trong một hộp thoại (dialog). Và mục đích của Alert là sẽ cảnh báo hay truyền một thông điệp cho người dùng biết.
Create
Cú pháp của một Alert thì khá đơn giản. Ví dụ như sau:
Alert(title: Text("Warning"), message: Text("Are you sure you want to leave?"), primaryButton: .default(Text("Confirm")), secondaryButton: .cancel() )
Trong đó:
- Các tham số
title
&message
là kiểu String và thông điệp bạn muốn truyền vào - Chúng ta sẽ có các button đi kèm
primaryButton
haysecondaryButton
- Với
.cancel()
sẽ làm Alert mất đi
Khi bạn muốn có thêm action
cho các Button thì có thể thêm tham số của các button đó. Ví dụ:
.default(Text("YES"), action: { })
Hoặc bạn có thể custom lại nội dung của các Button, cho dù là mặc định. Ví dụ cho .cancel()
.cancel(Text("NO")
Show Alert
Tới công việc chính nào. Alert thì ta đã tạo ra sẵn rồi. Còn để hiển thị nó thì bạn cần gọi tới modifier .alert
. Ta sẽ ráp nó vào trong LoginView nhoé.
.alert(isPresented: $isAlert) { Alert(title: Text("Login"), message: Text("Are you sure?"), primaryButton: .default(Text("YES"), action: { isAlert = false }), secondaryButton: .cancel(Text("NO")) ) }
Trong đó:
- Bạn sử dụng modifier vào View gốc của LoginView nhoé
- Chúng sẽ cần ràng buộc với một thuộc tính @State là
isAlert
- Với
action
của Button, ta xét lạiisAlert = false
Do đó, bạn cần khai báo thêm một thuộc tính nữa cho View của bạn, chính là isAlert
là kiểu State.
@State var isAlert = false
Cuối cùng, bạn sẽ phải tìm một cái cớ để hiển thị Alert của mình lên. Ví dụ mình chọn nút Login trên giao diện. Và cách hiển thị lên cũng đơn giản, xét lại giá trị isAlert = true
thôi.
Button { isAlert = true } label: { Text("Login") } .buttonStyle(BlueButton())
Bấm Live Preview và cảm nhận kết quả nhoé!
Full Screen Modal View
Cuối cùng, là cách bạn sẽ hiển thị Modal View full màn hình. Với trạng thái full màn hình, thì bạn không thể dùng tay để kéo Modal View mất đi được.
Nó là cách mà
modal
ở các iOS trước iOS 14 hiển thị.
Bạn sẽ không cần phải tuỳ chính nhiều thứ. Bạn sẽ thay modifier .sheet
thành .fullScreenCover
là được.
.fullScreenCover(isPresented: $isPresent) { // dismis } content: { // content FAQView(isPresent: $isPresent) }
Bấm Live Preview và test lại nhoé!
Mọi thứ vẫn hoạt động bình thường và Modal View đã full màn hình. Và là cách để cho Float Button phát huy hiệu quả cao nhất. Nhìn đẹp hơn rồi!
ActionSheet
Ta sẽ tới một SwiftUI View tiếp theo, tuy không phải một dạng Modal View. Nhưng về bản chất nó lại tương tự như Alert và bạn sẽ dùng nó nhiều trong ứng dụng iOS. Đó là ActionSheet. ActionSheet cung cấp cho bạn nhiều lựa chọn hơn kiểu Alert.
Mặt hiển thị, nó sẽ xuất hiện từ dưới lên trên một hộp thoại. Người dùng có thể lựa chọn nhiều lựa chọn tại đó.
Mặt bản chất, nó khá tương đồng với Alert & Alert View. Nên mình sẽ đi nhanh phần này thôi, ta tiến vào demo luôn nhóe.
Demo
Đầu tiên, bạn cần một thuộc tính State để quản lý việc hiển thị của ActionSheet.
@State private var showingActionSheet = false
Tiếp theo, bạn cần tìm một sự kiện làm cái cớ cho việc hiển thị ActionSheet nhóe.
Button { // goto forgot view showingActionSheet = true } label: { Text("Forgot password") .frame(maxWidth: .infinity) } .buttonStyle(GrayButton())
Mình lợi dụng Button Forgot password
trong màn hình LoginView. Bạn chỉ cần xét lại giá trị của thuộc tính showingActionSheet = true
là xong.
Cuối cùng, bạn sẽ sử dụng tới modifier .actionSheet
để hiển thị một ActionSheet nha. Thao khảo đoạn code sau:
.actionSheet(isPresented: $showingActionSheet) { ActionSheet(title: Text("Forgot password"), message: Text("We will be sending a reset password link to ..."), buttons: [ .default(Text("Email")) { // code }, .default(Text("SMS")) { // code }, .default(Text("Phone")) { // code }, .cancel() ]) }
Như đã nói ở trên, bản chất của nó giống với Alert nên bạn cũng nhanh chóng nắm bắt được cách tạo và các button & action cho từng lựa chọn.
Bấm Live Preview và cảm nhận kết quả nhóe!
Tạm kết
- Tìm hiểu về 2 loại Modality là Modal View & Alert
- Các cách hiển thị và ẩn một Modal View
- Cách hiển thị full màn hình với Modal View
- Xử lý Alert và hiển thị Alert lên giao diện
- Tìm hiểu và sử dụng ActionSheet
Okay! Tới đây, mình xin kết thúc bài viết về Modal View, Alert & ActionSheet 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
- SMART – Hướng dẫn dành tạo Prompt cho người mới bắt đầu
- Nhìn lại năm 2024
- CO-STAR – Công thức vàng để viết Prompt hiệu quả cho LLM
- 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
You may also like:
Archives
- January 2025 (2)
- December 2024 (4)
- 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)