Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Written by chuotfx on October 4, 2021

SideBar & Toolbar – SwiftUI Notes #50

SwiftUI

Contents

  • Chuẩn bị
  • SideBar
    • SideBar with NavigationView
    • SideBar with List
  • Router
    • Setup Model
    • Custom SideBar
    • Master View
    • Detail View
    • Combine Views
  • Toolbar
    • Create ToolbarItem
    • ToolbarItem Placement
    • ToolbarItemGroup
  • Tạm kết

Chào mừng bạn đến với Fx Studio. Chúng ta đã tìm hiểu nhiều về NavigationView rồi, tuy nhiên nó vẫn còn một tính năng khá là hay nữa. Đó là SideBar. Và nếu bạn theo dõi series SwiftUI này từ đầu, thì mình đã giới thiệu về SideBar trong MacOS. Lần này, chúng ta sẽ tìm hiểu SideBar ứng dụng ra sao trong iOS & iPadOS nhoé.

Chủ đề bài viết này đã được đề cập tại một bài viết trước rồi, bạn có thể đọc lại ở link sau đây:

    • Multiplatform App : Design Views

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:

    • Làm quen với SwiftUI
    • Cơ bản về ứng dụng SwiftUI App

(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.)

SideBar

Từ trước tới giờ, có thể bạn chỉ biết mô hình Master-Detail đi kèm với Navigation. Tuy nhiên, bạn sẽ bất ngờ khi Navigation hay NavigationView lại cho thêm một View thứ 3 nữa.

Đó là SideBar.

SideBar được xem là màn hình phụ trong cả macOS và iOS/iPadOS. Nhiều bạn sẽ lần tưởng SideBar là SideMemu. Nhưng nó gần giống với UISplitViewController trên iPadOS. Trong iOS, SideBar được đùng như 1 màn hình hiển thị kết quả search mà thôi, nghe hơn bất ngờ phải không nào.

Về điều hướng, SideBar sẽ dùng điều hướng Master View, sau đó Master View sẽ điều hướng tới Detail View. SideBar khác TabView là nó không thể điều hướng chuyển đổi qua lại giữa các luồng chính (Master View) như TabView.

Ta sẽ tiếp tục tìm hiểu SideBar trong các phần ở dưới nhoé!

SideBar with NavigationView

Bắt đầu một cách đơn giản nhất. Bạn sẽ tạo một NavigationView, sau đó thêm cho nó 3 View con nhoé. Bạn tham khảo code sau:

struct SideBarDemoView: View {
    var body: some View {
        
        NavigationView {
            Text("Sidebar")
            Text("Primary View")
            Text("Detail View")
        }
    }
}

Xem kết quả để hình dung SideBar như thế nào? và nó nằm ở đâu nhoé.

  • iOS

SideBar

  • iPadOS

SideBar

Khi bạn nhấn vào Back Button trên, nó sẽ đưa bạn qua màn hình SideBar nhoé. Đồng thời giao diện sẽ bị đẩy sang phải, che đi một phần của Detail View. Bạn nhấn vào Detail View thì sẽ ẩn đi SideBar.

Qua trên bạn khái quá được ý SideBar sẽ trông như thế nào rồi. Ta sang phần tiếp theo nhoé.

SideBar with List

Mục đích chính là dùng để điều hướng cho Master View. Do đó, ta thường sẽ sử dụng List để làm giao diện chính. SwiftUI sẽ tối ưu hoá việc điều hướng này cho bạn bằng kiểu là SideBar.

Xem ví dụ nhoé.

NavigationView {
    List {
        ForEach(1...10, id:\.self) { index in
            NavigationLink(destination: Text("Master \(index)")) {
                Text("Item \(index)")
            }
        }
    }
    .navigationTitle("SideBar")
    Text("Primary View")
    Text("Detail View")
}

Bạn thay thế Text bằng List thôi và mọi thứ không có gì đặc biệt cả. Bạn bấm Resume để xem kết quả nhoé.

SideBar

Tuy nhiên, bạn sẽ còn một cách nữa để biến List thành SideBar nhoé. Đó là sử dụng modifier .listStyle.

NavigationView {
    List {
        ForEach(1...10, id:\.self) { index in
            NavigationLink(destination: Text("Master \(index)")) {
                Text("Item \(index)")
            }
        }
    }
    .navigationTitle("SideBar")
    .listStyle(.sidebar)
    //Text("Primary View")
    //Text("Detail View")
}

Trong đó,

  • Sử dụng modifier với tham số là .sidebar cho List
  • Comment 2 Text View ban đầu để cho mọi người dễ cảm nhận hơn

SideBar

Với cách này, bạn sẽ có đc một Bar Button Item giúp ẩn hiện SideBar. Khá là tiện lợi phải không nào.

Nếu ban đầu, bạn cung cấp thêm cho NavigationView 2 View cho Master & Detail, thì Bar Button Item trên sẽ không xuất hiện nha.

Còn với iOS, kết quả hiển thị sẽ không có Bar Button Item đó và vẫn giống như List bình thường.

Nhìn phèn quá!

Router

Trong phần này, mình sẽ thực hiện demo để xem được việc điều hướng như thế nào trong SideBar nhoé. Tổng quan bạn sẽ có 3 loại View chính:

  • SideBar
  • Master View
  • Detail View

Việc điều hướng sẽ theo cấp như vậy. Mình sẽ demo một ứng dụng Mail đơn giản, trong đó:

  • Folder chứa các loại mail, như: Inbox và Sent
  • Mail Store: chứa các mail trong từng loại
  • Mail View: hiển thị nội dung của một mail

Bắt đầu thôi nào!

Setup Model

Ta sẽ cần chuẩn bị đối tượng đại diện cho Mail như sau:

struct Mail: Identifiable, Hashable {
    let id = UUID()
    let date: Date
    let subject: String
    let body: String
    var isFavorited = false
}

Trong đó:

  • id sẽ là định danh cho từng đối tượng trong Array với việc kế thừa Identifiable Protocol.
  • Các thuộc tính còn lại thì bình thường thôi

Tiếp theo, mình sẽ chuẩn bị dummy data cho chương trình. Tham khảo đoạn code sau:

final class MailStore: ObservableObject {
    @Published var allMails: [String: [Mail]] = [
        "Inbox": [ .init(date: Date(), subject: "Subject1.1", body: "Very long body..."),
                   .init(date: Date(), subject: "Subject1.2", body: "Very long body..."),
                   .init(date: Date(), subject: "Subject1.3", body: "Very long body..."),
                   .init(date: Date(), subject: "Subject1.4", body: "Very long body...") ],
        "Sent": [ .init(date: Date(), subject: "Subject2.1", body: "Very long body..."),
                  .init(date: Date(), subject: "Subject2.2", body: "Very long body..."),
                  .init(date: Date(), subject: "Subject2.3", body: "Very long body..."),
                  .init(date: Date(), subject: "Subject2.4", body: "Very long body..."),
                  .init(date: Date(), subject: "Subject2.5", body: "Very long body..."),
                  .init(date: Date(), subject: "Subject2.6", body: "Very long body...")],
    ]
}

Trong đó:

  • Kế thừa thêm ObservableObject Protocol để tạo thành nguồn dữ liệu
  • @Published cập nhật giá trị cho các View ràng buộc với thuộc tính này. Từ đó, View sẽ tự động cập nhật khi dữ liệu thay đổi.

Custom SideBar

Ta sẽ tạo một file mới và tiến hành custom một SideBar nhoé. Bạn xem ví dụ code sau:

struct Sidebar: View {
    @ObservedObject var store: MailStore
    @Binding var selectedFolder: String?
    @Binding var selectedMail: Mail?
    
    var body: some View {
        List {
            ForEach(Array(store.allMails.keys), id: \.self) { folder in
                NavigationLink(
                    destination: FolderView(
                        title: folder,
                        mails: store.allMails[folder, default: []],
                        selectedMail: $selectedMail
                    ),
                    tag: folder,
                    selection: $selectedFolder
                ) {
                    Text(folder).font(.headline)
                }
            }
        }
        .listStyle(.sidebar)
        .navigationTitle("Folders")
    }
}

Trong đó:

  • Bạn chú ý tới các thuộc tính lên quan tới The single source of truth nhoé
  • store sẽ liên kết với dữ liệu dummy data
  • selectedFolder & selectedMail lưu trữ dữ liệu cho thư mục mail được chọn
  • .listStyle(.sidebar) xác định kiểu là một SideBar cho List

Việc hiển thị chính là sử dụng List. Việc điều hướng sử dụng NavigationLink, khi đó đối tượng FolderView sẽ được đẩy vào NaviagtionView (ở view chính đầu tiên). Dựa vào các dữ liệu ràng bộ để load đúng dữ liệu cho FolderView.

Master View

Ta sẽ tiếp tục custom tiếp FolderView, nó đại diện cho Master View. Bạn tham khảo code như sau.

struct FolderView: View {
    let title: String
    let mails: [Mail]
    @Binding var selectedMail: Mail?
    
    var body: some View {
        List {
            ForEach(mails) { mail in
                NavigationLink(
                    destination: MailView(mail: mail),
                    tag: mail,
                    selection: $selectedMail
                ) {
                    VStack(alignment: .leading) {
                        Text(mail.subject)
                        Text(mail.date, style: .date)
                    }
                }
            }
        }.navigationTitle(title)
    }
}

Trong đó:

  • Bạn chú ý tới thuộc tính @Binding đóng vai trò truyền dữ liệu xuyên qua các lớp phân cấp View
  • Hiển thị danh sách vẫn bình thường với List

Việc điều hướng từ Master View sang Detail View vẫn sử dụng NavigationLink. Đối tượng truyền đi là mail, item trong array Mail. Đích của NaviagtionLink sẽ là MailView.

Detail View

MailView đóng vài trong điểm đến cuối cùng và là Detail View trong toàn bộ phân cấp nhoé. Bạn tham khảo code ví dụ như sau:

struct MailView: View {
    let mail: Mail
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(mail.subject)
                .font(.headline)
            Text(mail.date, style: .date)
            Text(mail.body)
        }
    }
}

Vì là View cuối cùng, nên mọi thứ chỉ là chỉ đọc và hiển thị dữ liệu mà thôi. Nên cấu tạo của nó sẽ khá là đơn giản.

Combine Views

Bạn đã có đầy đủ các dữ liệu và các View rồi. Ta tiến hành kết hợp chúng vào View lớn nhất. Đây cũng được xem là View gốc cho toàn bộ ứng dụng. Bạn tham khảo đoạn code nhoé.

struct SideBarDemo2: View {
    @StateObject var store = MailStore()
    @State private var selectedLabel: String? = "Inbox"
    @State private var selectedMail: Mail?
    
    var body: some View {
        NavigationView {
            Sidebar(
                store: store,
                selectedFolder: $selectedLabel,
                selectedMail: $selectedMail
            )
        }
    }
}

Khá đơn giản phải không nào. Bạn chỉ cần chuẩn bị các đối tượng State & StateObject thôi. Chúng nó đóng vài trò là nguồn dữ liệu chính. Mọi sự biến đổi đều tác động tới chúng. Và bạn cũng đã học qua nhiều với The single source of truth rồi. Nên cũng không quá khó hiểu nhĩ.

Bạn bấm Live Preview và test trên iPad trước nha.

SideBar

Nhìn qua cũng khá ổn nhĩ. Sau khi test xong cho iPad, thì bạn chuyển sang iPhone để trải nghiệm nha. Nó đem lại một trải nghiệm hoàn toàn khác cho bạn.

Toolbar

Một View phụ đi kèm với NavigationView nữa đó là Toolbar. Mình sẽ giới thiệu chúng trong này, vì

  • Với macOS bạn sẽ lợi dụng nó để tạo giao diện cho button ẩn hiện SideBar
  • Mình đã có trình bày nó ở 1 bài viết trước đây rồi. Bạn có thể tham khảo lại nha.

Về mặt hiển thị, Toolbar sẽ chứa các NavigationBarItem (như trong iOS). Giúp bạn thực hiện nhiều tác vụ khác nhau. Với SwiftUI, Toolbar cũng tương tự như vậy. Bạn sẽ cần dùng tới modifier .toolbar để tạo các BarItem nhoé.

Create ToolbarItem

Mình sẽ lợi dụng luôn Detail View ở trên cho công việc này. Bạn tham khảo đoạn code này nha.

var body: some View {
    VStack(alignment: .leading) {
        Text(mail.subject)
            .font(.headline)
        Text(mail.date, style: .date)
        Text(mail.body)
    }
    .toolbar {
        ToolbarItem {
            Image(systemName: "person.crop.circle")
        }
    }
}

Trong đó:

  • Modifier .toolbar để tạo một Toolbar trong NavigationView
  • Để có 1 item, ta sử dụng đối tượng ToolbarItem và Image là nội dung của nó.

Đơn giản phải không nào, bạn bấm Live Preview và test lại nhoé!

ToolbarItem Placement

Mặc định, ToolbarItem sẽ ở vị trí phái trên bên phải như vậy. Nhưng bạn có thể thay đổi vị trí của chúng nhoé.

ToolbarItem(placement: .bottomBar) {
    Button(action: {}) {
        Image(systemName: "person.crop.circle")
    }
}

Xét lại với vị trí ở dưới với tham số placement và thay Image bằng Button để có sự kiện. Và ta sẽ thêm một combo như thế này.

struct MailView: View {
    let mail: Mail
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(mail.subject)
                .font(.headline)
            Text(mail.date, style: .date)
            Text(mail.body)
        }
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                Button(action: {
                    dismiss()
                }) {
                    Image(systemName: "chevron.backward")
                }
            }
            ToolbarItem(placement: .bottomBar) {
                Button(action: {}) {
                    Image(systemName: "person.crop.circle")
                }
            }
        }
        .navigationTitle(mail.subject)
        .navigationBarBackButtonHidden(true)
    }
}

Trong đó:

  • placement: .navigationBarLeading sẽ ở vị trị trên thanh NavigationBar và bên trái
  • .navigationBarBackButtonHidden(true) sẽ ẩn đi Back Button mặc định
  • @Environment(\.dismiss) var dismiss dùng để pop View trong NavigationLink

Như vậy, bạn kết hợp combo trên bạn sẽ có đc một custom cho Back Button mặc định nha. Xem kết quả nhoé!

ToolbarItemGroup

Cuối cùng, bạn có 1 nhóm các ToolbarItem và có ý định gôm chúng nó lại. Bạn sẽ cần sử dụng tới ToolbarItemGroup. Thay vì 1 View, thì bạn có thể tạo nhiều View vào trong đó.

Bạn thêm đoạn code này vào trong .toolbar nha.

ToolbarItemGroup(placement: .bottomBar) {
    Image(systemName: "person")
    Spacer()
    Image(systemName: "ellipsis")
    Spacer()
    Image(systemName: "trash")
}

Bấm Live Preview và xem kết quả nha.

Bây giờ nhìn chuyên nghiệp hơn rồi!

Tạm kết

  • Giới thiệu về SideBar trong SwiftUI
  • Cách tạo SideBar trong NavigationView
  • Cách biến đổi List sang kiểu SideBar
  • Điều hướng trong ứng dụng với SideBar
  • Giới thiệu và cách sử dụng Toolbar

 

Okay! Tới đây, mình xin kết thúc bài viết về SideBar & Toolbar 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.

  • Bạn có thể checkout code tại đây.
  • Bài viết tiếp theo tại đây.

Cảm ơn bạn đã đọc bài viết này!

FacebookTweetPinYummlyLinkedInPrintEmailShares7

Related Posts:

  • feature_bg_swiftui_7
    Giới thiệu MapKit trên SwiftUI
Tags: SwiftUI, SwiftUI Notes
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

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?
  • Khi Cô Đơn Gặp Python

You may also like:

  • Giới thiệu MapKit trên SwiftUI
    feature_bg_swiftui_7

Archives

  • May 2025 (1)
  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • 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)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (43)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (86)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.