如何在SwiftUI中创建一个自定义、可折叠的侧边栏

1,825 阅读12分钟

目录

我们在移动应用程序中使用侧边栏,为用户提供应用程序中最常用和最重要的屏幕的顶层导航。在这篇文章中,你将学习如何使用SwiftUI创建一个自定义侧边栏。让我们开始吧!

创建一个新的SwiftUI项目

在本教程中,我们将从头开始创建一个新的SwiftUI项目,但你也可以在你现有的项目中实现这一点。

要创建一个新的SwiftUI项目,打开Xcode并点击创建一个新的Xcode项目

Xcode

你会被提示为你的项目选择一个模板。选择iOS > App并点击Next

App Selected

现在,输入你的项目名称和你想使用的语言和框架。在Product Name 字段中输入项目名称,并选择一个相关的团队

因为我们将使用Swift来创建我们的iOS应用程序,我们将在界面下拉菜单中选择SwiftUI

SwiftUI Dropdown

点击下一步后,Xcode将询问在哪个路径上创建项目,所以选择你喜欢的位置并点击创建。现在,让我们为我们的SwiftUI应用程序创建我们的自定义侧边栏。

在Swift中设计主屏幕

我们正在设计的应用程序包括一个主屏幕和一个在同一屏幕上的侧边栏。它有各种链接供用户去点击。

最终的用户界面将看起来像这样。

Final UI

注意:本文的完整代码可以在这个资源库中找到。

让我们专注于构建主屏幕。我们将有一个图片列表,通过网络使用AsyncImage加载。

将以下代码添加到ContentView.swift

struct ContentView: View {

    var body: some View {
          NavigationView {
              List {
                  ForEach(0..<8) { _ in
                      AsyncImage(
                              url: URL(
                                string: "https://picsum.photos/600"
                                )) { image in
                          image
                              .resizable()
                              .scaledToFill()
                              .frame(height: 240)
                      } placeholder: {
                          ZStack {
                              RoundedRectangle(cornerRadius: 12)
                                  .fill(.gray.opacity(0.6))
                                  .frame(height: 240)
                              ProgressView()
                          }
                      }
                      .aspectRatio(3 / 2, contentMode: .fill)
                      .cornerRadius(12)
                      .padding(.vertical)
                      .shadow(radius: 4)
                    }
                  }
                  .listStyle(.inset)
                  .navigationTitle("Home")
                  .navigationBarTitleDisplayMode(.inline)
          }
    }
}

请注意,我们已经将我们的整个View ,即从我们的body 变量返回的ContentView ,包裹在一个 [NavigationView](https://developer.apple.com/documentation/swiftui/navigationview),然后用一个 [List](https://developer.apple.com/documentation/swiftui/list)视图来创建一个要显示的图片列表。

在这个列表中,我们循环通过一个 [range](https://developer.apple.com/documentation/swift/range)0到8 -(0..<8) - 这将从08 迭代八次。

在这个循环中,我们遍历一个 [AsyncImage](https://developer.apple.com/documentation/swiftui/asyncimage)视图,它被用来加载和显示网络上的图片。然后我们在AsyncImage 视图上使用了一个自定义的初始化方法,给出一个placeholder 的值,这个值将一直显示到图像从网络上下载完毕。我们还为视图添加了一些修改器,以达到风格化的目的。

然后,我们在List 视图上添加了几个修改器,listStyleListStyle.inset ,用于对列表的嵌入风格而不是默认的iOS风格的列表。我们还设置了navigationTitle("Home") ,作为这个屏幕的导航标题,显示在导航栏上。

最后一个修改器是navigationBarTitleDisplayMode(.inline) 。它将导航栏的显示模式设置为内嵌式,也就是说,它将永远停留在顶部,以保持我们的UI与侧边栏一致。

下面是上述代码的输出。

Output Home Images

在Swift中创建自定义侧边栏视图

现在我们的主屏幕用户界面已经完成了,让我们来创建侧边栏视图。为了显示和隐藏侧边栏,我们需要一个@State 变量,这将是我们的视图知道它是否应该显示或隐藏侧边栏的真相来源。

让我们把@State 变量添加到我们的ContentView

struct ContentView: View {

    // Add the below line
    @State private var isSidebarOpened = false

    var body: some View {

现在,让我们通过创建一个新的SwiftUI文件来创建侧边栏视图。进入文件→新建→文件或按⌘+N创建一个新文件。

File Select

选择你想要创建的文件类型,选择SwiftUI视图,然后点击下一步

Select SwiftUI View

Xcode会问你想要创建的文件的名称。输入Sidebar,在下拉菜单中选择你的项目组,然后点击创建。

现在我们有一个空白的SwiftUI视图,让我们开始设计我们的侧边栏。

我们将向我们的Sidebar 视图添加一个@Binding 变量,这样我们就可以从Sidebar 视图本身更新我们的@State 变量isSidebarOpened, 的值。

添加下面的代码到Sidebar.swift

struct Sidebar: View {
    @Binding var isSidebarVisible: Bool

    var body: some View {
        if isSidebarVisible {
            Text("Sidebar visible")
                .bold()
                .font(.largeTitle)
                .background(.purple)
        }
    }
}

在这里,我们添加了一个@Binding 变量,isSidebarVisible ,它将来自于ContentView 的变量isSidebarOpened 。我们还添加了一个条件语句,说如果isSidebarVisible 的值是true ,那么我们将显示Text 视图,它包含的文字是 "侧边栏可见"。

我们需要对我们的ContentView 文件做一些修改。首先,将NavigationView 包裹在ZStack 中,这样Sidebar 视图就能得到完整的屏幕绑定区域。然后,给我们的导航栏添加一个Button ,以切换isSidebarOpened @State 变量的值。我们将在View 中添加Sidebar 视图,并将绑定值传递给它。

ContentView.swift 中做这些修改。

struct ContentView: View {
    @State private var isSideBarOpened = false

    var body: some View {
        ZStack {
            NavigationView {
                List {
                    ForEach(0..<8) { _ in
                        AsyncImage(
                          url: URL(
                            string: "https://picsum.photos/600"
                          )) { image in
                            image
                                .resizable()
                                .scaledToFill()
                                .frame(height: 240)
                            } placeholder: {
                                ZStack {
                                    RoundedRectangle(cornerRadius: 12)
                                        .fill(.gray.opacity(0.6))
                                        .frame(height: 240)
                                    ProgressView()
                                }
                            }
                        .aspectRatio(3 / 2, contentMode: .fill)
                        .cornerRadius(12)
                        .padding(.vertical)
                        .shadow(radius: 4)
                    }
                    }
                    .toolbar {
                        Button {
                            isSideBarOpened.toggle()
                        } label: {
                            Label("Toggle SideBar",
                          systemImage: "line.3.horizontal.circle.fill")
                        }
                    }
                    .listStyle(.inset)
                    .navigationTitle("Home")
                    .navigationBarTitleDisplayMode(.inline)
            }
            Sidebar(isSidebarVisible: $isSideBarOpened)
        }
    }
}

下面是我们的应用程序目前的样子。

Select Top Left Button

我们现在可以切换侧边栏的可见性了!让我们为它创建一些用户界面,使它看起来像一个侧边栏。

首先,让我们添加一个深色背景,在打开侧边栏后出现。在Sidebar.swift 文件中添加以下代码。

var body: some View {
    ZStack {
        GeometryReader { _ in
            EmptyView()
        }
        .background(.black.opacity(0.6))
        .opacity(isSidebarVisible ? 1 : 0)
        .animation(.easeInOut.delay(0.2), value: isSidebarVisible)
        .onTapGesture {
            isSidebarVisible.toggle()
        }
    }
    .edgesIgnoringSafeArea(.all)
}

我们将我们的视图包裹在一个ZStack ,并给它添加了一个edgesIgnoringSafeArea 修改器,然后给它一个值all 。这将使我们的ZStack 遍布可用的屏幕,包括iPhone的缺口和底角下的区域。

我们在 中添加了一个 [GeometryReader](https://developer.apple.com/documentation/swiftui/geometryreader)``ZStack 视图,这样我们就可以为我们的背景视图使用所有可用的设备屏幕空间,同时还为我们的GeometryReader 视图添加了以下视图修改器。

  • background: 设置视图的背景。在我们的例子中,我们将其设置为黑色,不透明度为60%。
  • opacity: 设置视图本身的不透明度,如果侧边栏是可见的,我们将通过三元表达式1 ,如果不可见,0 来计算其值。
  • animation动画:使我们在这个视图上执行的变化成为动画。在这种情况下,我们正在用一个easeInOut 动画对视图的opacity ,延迟为0.2 。我们还在动画中添加了一个value 标签,告诉Swift我们需要在这个值变化时对视图进行动画处理(这个变化在iOS 15.0中引入)
  • onTapGesture:当用户点击背景墙的时候,侧边栏应该关闭

侧边栏的背景将看起来像这样。

Sidebar Backdrop

让我们确保我们的侧边栏可以滑动。在Sidebar.swift 文件中做以下突出的修改。

struct SideMenu: View {
    @Binding var isSidebarVisible: Bool
    var sideBarWidth = UIScreen.main.bounds.size.width * 0.7
    var bgColor: Color = 
          Color(.init(
                  red: 52 / 255,
                  green: 70 / 255,
                  blue: 182 / 255,
                  alpha: 1))

    var body: some View {
        ZStack {
            GeometryReader { _ in
                EmptyView()
            }
            .background(.black.opacity(0.6))
            .opacity(isSidebarVisible ? 1 : 0)
            .animation(.easeInOut.delay(0.2), value: isSidebarVisible)
            .onTapGesture {
                isSidebarVisible.toggle()
            }            
            content
        }
        .edgesIgnoringSafeArea(.all)
    }

    var content: some View {
        HStack(alignment: .top) {
            ZStack(alignment: .top) {
                bgColor
            }
            .frame(width: sideBarWidth)
            .offset(x: isSidebarVisible ? 0 : -sideBarWidth)
            .animation(.default, value: isSidebarVisible)

            Spacer()
        }
    }
}

我们引入了两个新的变量,名为sideBarWidthbgColor 。顾名思义,sideBarWidth 存储显示的侧边栏的宽度。注意,我们没有给这个变量任何类型,这是因为Swift通过使用类型推理来处理这个问题。

第二个变量是bgColor ,它是一个简单的Color ,使用RGB 的值进行初始化。

为了简化body 中的代码并使我们的代码更易读,我们创建了一个名为content 的新变量,该变量返回一个Viewcontent 视图包含一个HStack ,其中有两个视图:实际的侧边栏(ZStack),它显示在屏幕上,还有一个间隔视图,它占据了一个视图的最大空间。这使得我们的蓝色侧边栏向左移动。

ZStack ,我们现在有一个varbgColor ,它给了视图一个背景颜色。我们也向视图添加了重要的修改器。使用frame ,我们将视图的宽度设置为我们之前声明的变量的值,然后我们使用了 [offset](https://developer.apple.com/documentation/swiftui/view/offset(x:y:)),使侧边栏从左到右滑入屏幕。

Offset 是用来移动一个特定视图的X和Y坐标的。如果边栏不可见,我们将边栏从X轴上移到其宽度的负值,如果边栏可见,则将其设置为 。0

最后,为了获得一个无缝的用户界面,我们使用animation ,给它一个default ,并针对isSidebarVisible var制作动画。

做完上述调整后,我们的Swift侧边栏的用户界面将看起来像这样。

Sidebar Popup

现在,将下面的代码添加到Sidebar.swift

var secondaryColor: Color = 
              Color(.init(
                red: 100 / 255,
                green: 174 / 255,
                blue: 255 / 255,
                alpha: 1))

var content: some View {
    HStack(alignment: .top) {
        ZStack(alignment: .top) {
            bgColor
            MenuChevron
        }
        .frame(width: sideBarWidth)
        .offset(x: isSidebarVisible ? 0 : -sideBarWidth)
        .animation(.default, value: isSidebarVisible)

        Spacer()
    }
}

var MenuChevron: some View {
    ZStack {
        RoundedRectangle(cornerRadius: 18)
            .fill(bgColor)
            .frame(width: 60, height: 60)
            .rotationEffect(Angle(degrees: 45))
            .offset(x: isSidebarVisible ? -18 : -10)
            .onTapGesture {
                isSidebarVisible.toggle()
            }

        Image(systemName: "chevron.right")
            .foregroundColor(secondaryColor)
            .rotationEffect(
              isSidebarVisible ?
                Angle(degrees: 180) : Angle(degrees: 0))
            .offset(x: isSidebarVisible ? -4 : 8)
            .foregroundColor(.blue)
    }
    .offset(x: sideBarWidth / 2, y: 80)
    .animation(.default, value: isSidebarVisible)
}

我们已经创建了新的变量MenuChevron ,它包含一个ZStack 和两个视图,一个RoundedRectangle 和一个Image

Sidebar Click

Tada!我们有一个工作的侧边栏。让我们向它添加一些内容吧!

在Swift侧边栏中添加内容

最后一步是向侧边栏添加内容。

在我们的侧边栏中,有一个带有导航链接的用户资料,可以链接到应用程序的不同部分。让我们从用户资料视图开始。

为了创建用户资料部分的用户界面,在Sidebar.swift 文件中添加以下代码。

var content: some View {
    HStack(alignment: .top) {
        ZStack(alignment: .top) {
            bgColor
            MenuChevron

            VStack(alignment: .leading, spacing: 20) {
                userProfile
            }
            .padding(.top, 80)
            .padding(.horizontal, 40)
        }
        .frame(width: sideBarWidth)
        .offset(x: isSidebarVisible ? 0 : -sideBarWidth)
        .animation(.default, value: isSidebarVisible)

        Spacer()
    }
}

var userProfile: some View {
    VStack(alignment: .leading) {
        HStack {
            AsyncImage(
              url: URL(
                string: "https://picsum.photos/100")) { image in
                  image
                      .resizable()
                      .frame(width: 50, height: 50, alignment: .center)
                      .clipShape(Circle())
                      .overlay {
                          Circle().stroke(.blue, lineWidth: 2)
                      }
                  } placeholder: {
                      ProgressView()
                  }
                  .aspectRatio(3 / 2, contentMode: .fill)
                  .shadow(radius: 4)
                  .padding(.trailing, 18)

              VStack(alignment: .leading, spacing: 6) {
                  Text("John Doe")
                      .foregroundColor(.white)
                      .bold()
                      .font(.title3)
                  Text(verbatim: "john@doe.com")
                      .foregroundColor(secondaryColor)
                      .font(.caption)
              }
          }
          .padding(.bottom, 20)
    }
}

注意我们在侧边栏中添加了一个VStack ,它有一个userProfile 视图,包含以下内容。

  • 一个VStack ,有leading 的对齐方式
  • 一个HStack ,其中包含另外两个视图
  • HStack ,我们有用户的个人资料图片,它是通过网络加载的,使用了AsyncImage
  • 就在个人资料图片之后,我们有另一个VStack ,显示用户的名字和电子邮件。电子邮件文本被初始化为verbatim ,因为Swift用默认的重点颜色来突出链接,所以,为了绕过这一点,我们用verbatim

让我们接着看菜单链接部分。为了创建链接,我们首先需要为一个MenuItem 创建一个struct ,这个 将包含关于该项目的所有信息。

struct MenuItem: Identifiable {
    var id: Int
    var icon: String
    var text: String
}

MenuItem 只有三个属性。id,icon, 和text 。让我们在我们的用户界面中应用这个。

import SwiftUI

var secondaryColor: Color = 
              Color(.init(
                red: 100 / 255,
                green: 174 / 255,
                blue: 255 / 255,
                alpha: 1))

struct MenuItem: Identifiable {
    var id: Int
    var icon: String
    var text: String
}

var userActions: [MenuItem] = [
    MenuItem(id: 4001, icon: "person.circle.fill", text: "My Account"),
    MenuItem(id: 4002, icon: "bag.fill", text: "My Orders"),
    MenuItem(id: 4003, icon: "gift.fill", text: "Wishlist"),
]

var profileActions: [MenuItem] = [
    MenuItem(id: 4004,
              icon: "wrench.and.screwdriver.fill",
              text: "Settings"),
    MenuItem(id: 4005,
              icon: "iphone.and.arrow.forward",
              text: "Logout"),
]

struct SideMenu: View {
    @Binding var isSidebarVisible: Bool
    var sideBarWidth = UIScreen.main.bounds.size.width * 0.7
    var bgColor: Color = Color(.init(
                                  red: 52 / 255,
                                  green: 70 / 255,
                                  blue: 182 / 255,
                                  alpha: 1))

    var body: some View {
        ZStack {
            GeometryReader { _ in
                EmptyView()
            }
            .background(.black.opacity(0.6))
            .opacity(isSidebarVisible ? 1 : 0)
            .animation(.easeInOut.delay(0.2), value: isSidebarVisible)
            .onTapGesture {
                isSidebarVisible.toggle()
            }

            content
        }
        .edgesIgnoringSafeArea(.all)
    }

    var content: some View {
        HStack(alignment: .top) {
            ZStack(alignment: .top) {
                menuColor
                MenuChevron

                VStack(alignment: .leading, spacing: 20) {
                    userProfile
                    Divider()
                    MenuLinks(items: userActions)
                    Divider()
                    MenuLinks(items: profileActions)
                }
                .padding(.top, 80)
                .padding(.horizontal, 40)
            }
            .frame(width: sideBarWidth)
            .offset(x: isSidebarVisible ? 0 : -sideBarWidth)
            .animation(.default, value: isSidebarVisible)

            Spacer()
        }
    }

    var MenuChevron: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 18)
                .fill(bgColor)
                .frame(width: 60, height: 60)
                .rotationEffect(Angle(degrees: 45))
                .offset(x: isSidebarVisible ? -18 : -10)
                .onTapGesture {
                    isSidebarVisible.toggle()
                }

            Image(systemName: "chevron.right")
                .foregroundColor(secondaryColor)
                .rotationEffect(isSidebarVisible ? 
                    Angle(degrees: 180) : Angle(degrees: 0))
                .offset(x: isSidebarVisible ? -4 : 8)
                .foregroundColor(.blue)
        }
        .offset(x: sideBarWidth / 2, y: 80)
        .animation(.default, value: isSidebarVisible)
    }

    var userProfile: some View {
        VStack(alignment: .leading) {
            HStack {
                AsyncImage(
                  url: URL(
                      string: "https://picsum.photos/100")) { image in
                    image
                        .resizable()
                        .frame(width: 50,
                                height: 50,
                                alignment: .center)
                        .clipShape(Circle())
                        .overlay {
                            Circle().stroke(.blue, lineWidth: 2)
                        }
                } placeholder: {
                    ProgressView()
                }
                .aspectRatio(3 / 2, contentMode: .fill)
                .shadow(radius: 4)
                .padding(.trailing, 18)

                VStack(alignment: .leading, spacing: 6) {
                    Text("John Doe")
                        .foregroundColor(.white)
                        .bold()
                        .font(.title3)
                    Text(verbatim: "john@doe.com")
                        .foregroundColor(secondaryColor)
                        .font(.caption)
                }
            }
            .padding(.bottom, 20)
        }
    }
}

struct MenuLinks: View {
    var items: [MenuItem]
    var body: some View {
        VStack(alignment: .leading, spacing: 30) {
            ForEach(items) { item in
                menuLink(icon: item.icon, text: item.text)
            }
        }
        .padding(.vertical, 14)
        .padding(.leading, 8)
    }
}

struct menuLink: View {
    var icon: String
    var text: String
    var body: some View {
        HStack {
            Image(systemName: icon)
                .resizable()
                .frame(width: 20, height: 20)
                .foregroundColor(secondaryColor)
                .padding(.trailing, 18)
            Text(text)
                .foregroundColor(.white)
                .font(.body)
        }
        .onTapGesture {
            print("Tapped on \(text)")
        }
    }
}

在这里,我们引入了两个新的视图,MenuLinksmenuLinkMenuLinksitems ,当MenuLinks 视图被初始化时,它被分配一个值。视图遍历每个items 的实例,并为每个项目返回一个新的视图menuLink

menuLink 在创建视图时,有两个变量被初始化: 和 ,它们被包裹在一个 中并被显示。我们还捕捉了 视图上的点击手势。目前,我们是将项目值打印到控制台,但你可以添加一个 ,并将用户导航到内部屏幕。icon text HStack menuLink NavigationLink

这就是了!我们现在有了一个可以工作的侧边栏。

Working Sidebar

总结

在本教程中,你看到了在SwiftUI中创建一个自定义侧边栏是多么容易。当然,你还可以做许多改变来进一步定制你的侧边栏。谢谢您的阅读!