SwiftUI六组合复杂用户界面

代码下载

应用的首页是一个纵向滚动的地标类别列表,每一个类别内部是一个横向滑动列表。随后将构建应用的页面导航,这个过程中可以学习到如果组合各种视图,并让它们适配不同的设备尺寸和设备方向。

下载起步项目并跟着本篇教程一步步实践,或者查看本篇完成状态时的工程代码去学习,项目文件

添加一个首页视图

已经创建了所有在应用中需要的视图,现在给应用创建一个首页视图,把之前创建的视图整合起来。首页不仅仅包含之前创建的视图,它还提供页面间导航的方式,同时也可以展示各种地标信息。

创建一个名为CategoryHome.swift的自定义视图文件,添加NavigationSplitView,这个NavigationSplitView将会容纳应用中其它不同的视图。配合使用NavigationLink及相关的修改器,就可以构建出应用的页面间导航结构,设置导航栏标题为Featured:

struct CategoryHome: View {
    var body: some View {
        NavigationSplitView {
            Text("Hello, World!")
                .navigationTitle("Featured")
        } detail: {
            Text("Select a Landmark")
        }

    }
}

创建地标类别列表

应用为了便于用户浏览各种类别的地标,将地标按类别竖向排列形成列表视图,对于每一个类别内的具体地标,又把它们按照水平方向排列,形成横向列表。组合使用垂直栈(vertical statck)和水平栈(horizontal stack)并给列表添加滚动。

首先从landmarkData.json文件读取类别数据。

1、在Landmark中,向Landmark结构添加Category枚举和category属性。landmarkData文件已经为每个地标数据包含了一个category值,该类别值为三个字符串值之一。通过匹配数据文件中的名称,可以让结构体遵守Codable协议来加载数据:

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    var isFavorite: Bool
    
    var category: Category
    enum Category: String, CaseIterable, Codable {
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }

    private var imageName: String
    var image: Image {
        Image(imageName)
    }

    private var coordinates: Coordinates
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }

    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

2、在ModelData中,添加一个categories计算字典,使用Dictionary结构体的初始化方法init(grouping:by:),把地标数据的类别属性category传入作为分组依据,可以把地标数据按类别分组:

class ModelData {
    var landmarks: [Landmark] = load("landmarkData.json")
    var hikes: [Hike] = load("hikeData.json")
    
    var categories: [ String: [Landmark] ] {
        Dictionary(grouping: landmarks) { $0.category.rawValue }
    }
}

3、在CategoryHome中创建一个modelData属性。现在需要访问categories,稍后还需要访问其他landmark数据。使用List显示地标数据的类别。Landmark.Category是枚举类型,它的值标识列表中每一种类别,可以保证类别不会有重复定义:

struct CategoryHome: View {
    @Environment(ModelData.self) var modelData: ModelData
    
    var body: some View {
        NavigationSplitView {
            List {
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    Text(key)
                }
            }.navigationTitle("Featured")
        } detail: {
            Text("Select a Landmark")
        }

    }
}

#Preview {
    CategoryHome().environment(ModelData())
}

创建地标行

Landmarks在水平滚动的一行中显示每个类别。添加一个新的视图类型来表示行,然后在新视图中显示该类别的所有地标。

重用在创建和组合视图中创建的Landmark视图的部分,以创建熟悉的Landmark预览。

1、创建一个名为CategoryItem的新自定义视图,显示一个地标:

struct CategoryItem: View {
    var landmark: Landmark
    
    var body: some View {
        VStack(alignment: .leading) {
            landmark.image
                .resizable()
                .frame(width: 155, height: 155)
                .cornerRadius(5)
            Text(landmark.name)
                .font(.caption)
        }.padding(.leading, 15)
    }
}

#Preview {
    CategoryItem(landmark: ModelData().landmarks[0])
}

2、定义一个新的视图类型CategoryRow,用来展示地标类别行的内容。新建行视图需要存放地标具体类别的展示数据。在CategoryRow.swift中将类别的条目放入HStack中,并将其与类别名称分组到VStack中。为行内容指定一个高度,并把行内容嵌入到ScrollView中,以支持横向滑动。预览视图时,可以多增加几个地标数据,用来查看列表的滑动是否正常:

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        CategoryItem(landmark: landmark)
                    }
                }
            }.frame(height: 185)
        }
    }
}

#Preview {
    let landmarks = ModelData().landmarks
    return CategoryRow(categoryName: landmarks[0].category.rawValue, items: Array(landmarks.prefix(3)))
}

完成类别视图

将行和特色地标图像添加到类别主页。

1、更新CategoryHome的body,将类别信息传递给行类型的实例:

接下来,在视图的顶部添加一个特色地标。要做到这一点,需要从地标数据中获得更多信息。

2、在Landmark中,添加一个新的isFeatured属性,与添加的其他地标属性一样,这个布尔值已经存在于数据中,只需要声明一个新属性:

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    var isFavorite: Bool
    var isFeatured: Bool
    
    var category: Category
    enum Category: String, CaseIterable, Codable {
        case lakes = "Lakes"
        case rivers = "Rivers"
        case mountains = "Mountains"
    }

    private var imageName: String
    var image: Image {
        Image(imageName)
    }

    private var coordinates: Coordinates
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }

    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

3、在ModelData中,添加一个新的计算特色地标数组,其中只包含将isFeatured设置为true的地标:

class ModelData {
    var landmarks: [Landmark] = load("landmarkData.json")
    var hikes: [Hike] = load("hikeData.json")
    
    var categories: [ String: [Landmark] ] {
        Dictionary(grouping: landmarks) { $0.category.rawValue }
    }
    
    var features: [Landmark] {
        landmarks.filter { $0.isFeatured }
    }
}

4、在CategoryHome中,将第一个特色地标的图像添加到列表的顶部。在后面的教程中,会将这个视图修改成一个交互式轮播图。目前,这个视图仅仅展示一张缩放和剪裁后的地标图片。把视图的边距设置为0,让展示内容可以尽量贴着屏幕边沿:

struct CategoryHome: View {
    @Environment(ModelData.self) var modelData: ModelData
    
    var body: some View {
        NavigationSplitView {
            List {
                modelData.features[0]
                    .image
                    .resizable()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())
                
                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key] ?? [])
                }.listRowInsets(EdgeInsets())
            }.navigationTitle("Featured")
        } detail: {
            Text("Select a Landmark")
        }

    }
}

在各节之间添加导航

现在所有类别的地标都可以在首页视图中展示出来,用户还需要能够进入应用其它页面的方法。使用页面导航和相关API来实现用户从应用首页到地标详情页、收藏列表页及用户个人中心页的跳转。

1、在CategoryRow.swift中,把CategoryItem视图包裹在NavigationLink视图中。CategoryItem这时做为跳转按钮的内容,destination指定点击NavigationLink按钮时要跳转的目标视图:

struct CategoryRow: View {
    var categoryName: String
    var items: [Landmark]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text(categoryName)
                .font(.headline)
                .padding(.leading, 15)
                .padding(.top, 5)
            
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(alignment: .top, spacing: 0) {
                    ForEach(items) { landmark in
                        NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
                            CategoryItem(landmark: landmark)
                        }
                    }
                }
            }.frame(height: 185)
        }
    }
}

2、使用renderingMode(:)和foregroundColor(:)这两个属性修改器来改变地标类别项的导航样式。做为NavigationLink标签的CategoryItem中的文本会使用Environment中的强调颜色,图片可能以模板图片的方式渲染,这些都可以使用属性修改器来调整,达到最佳效果:

struct CategoryItem: View {
    var landmark: Landmark
    
    var body: some View {
        VStack(alignment: .leading) {
            landmark.image
                .renderingMode(.original)
                .resizable()
                .frame(width: 155, height: 155)
                .cornerRadius(5)
            Text(landmark.name)
                .foregroundStyle(.primary)
                .font(.caption)
        }.padding(.leading, 15)
    }
}

3、接下来,将修改应用程序的ContentView,以显示一个TabView,让用户在刚刚创建的类别视图和现有的地标列表之间进行选择:

  • 切换到ContentView并添加要显示的选项卡枚举。为选项卡选择添加一个状态变量,并给它一个默认值。

  • 创建一个TabView来包装LandmarkList和新的CategoryHome。每个视图上的 tag(_😃 修饰符匹配选择属性可以取的一个可能值,因此当用户在用户界面中进行选择时,TabView可以协调显示哪个视图。

  • 给每个Tab一个标签。确保实时预览打开,并尝试新的导航。

    struct ContentView: View {
    @State private var selection: Tab = .featured

      enum Tab {
          case featured
          case list
      }
      
      var body: some View {
          TabView(selection: $selection) {
              CategoryHome().tabItem { Label("Featured", systemImage: "star") }
                  .tag(Tab.featured)
              LandmarkList().tabItem { Label("List", systemImage: "list.bullet") }
                  .tag(Tab.list)
          }
      }
    

    }

    #Preview {
    ContentView().environment(ModelData())
    }

相关推荐
~央千澈~9 小时前
优雅草央千澈-关于蓝湖如何快速的标注交互原型是如何使用的-如何使用蓝湖设计交互原型和整个软件项目的流程逻辑-实践项目详细说明
ui·交互·蓝湖
东坡肘子13 小时前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
军训猫猫头14 小时前
20.抽卡只有金,带保底(WPF) C#
ui·c#·wpf
wuningw15 小时前
ant-design-ui的Select选择器多选时同时获取label与vaule值
ui·arcgis
SoraLuna20 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
恋猫de小郭1 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
晓纪同学1 天前
QT创建一个模板槽和信号刷新UI
开发语言·qt·ui
程序视点2 天前
【安全漏洞】Vue UI库Vant组件遭恶意投毒,字节RspacK也中招!请紧急修复!
前端·vue.js·ui
靴子学长2 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
m0_748238782 天前
前端使用 Konva 实现可视化设计器(20)- 性能优化、UI 美化
前端·ui·性能优化