SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)

版本记录

版本号 时间
V1.0 2019.11.21 星期四

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)

开始

首先看下主要内容

在本教程中,您将使用SwiftUI实现主从应用程序的导航。 您将学习如何实现导航堆栈,导航栏按钮,上下文菜单和模式表(modal sheet)

下面看下写作环境

Swift 5, iOS 13, Xcode 11

注意:本教程假定您熟悉使用Xcode开发iOS应用。 您需要Xcode11。要查看SwiftUI预览,您需要macOS 10.15。 熟悉UIKitSwiftUI将有所帮助。

PublicArt-Starter文件夹中打开PublicArt项目。 您将使用此项目中已经包含的Artwork.swiftMapView.swift文件构建主从应用程序。


SwiftUI Basics in a Nutshell

SwiftUI允许您忽略Interface Builderstoryboards,而无需编写详细的分步说明来布局UI。 您可以将SwiftUI视图及其代码并排预览-更改一侧会更新另一侧,因此它们始终保持同步。 没有任何标识符字符串会出错。 它是代码,但比您为UIKit编写的要少得多,因此更易于理解,编辑和调试。 这不是很好吗?

画布预览意味着您不需要storyboard。 子视图会保持更新,因此您也不需要视图控制器。 实时预览意味着您几乎不需要启动模拟器。

SwiftUI不会取代UIKit,就像SwiftObjective-C一样,您可以在同一应用程序中同时使用两者。 在本教程的最后,您将看到在SwiftUI应用程序中使用UIKit视图有多么容易。

1. Declarative App Development

SwiftUI使您可以进行声明式(declarative)应用程序开发:您可以声明希望UI中的视图的外观以及它们所依赖的数据。 SwiftUI框架负责在视图应出现时创建视图,并在它们依赖的数据发生更改时对其进行更新。它重新计算视图及其所有子级,然后呈现已更改的内容。

视图的状态取决于其数据,因此您可以为视图声明可能的状态,以及每种状态下视图的外观-视图如何对数据更改做出反应或数据如何影响视图。是的,SwiftUI绝对具有反应性!因此,如果您已经在使用一种反应式编程框架,那么使用SwiftUI可能会更轻松。

2. Declaring Views

SwiftUI视图是您的UI的一部分:您可以合并较小的视图以构建较大的视图。有许多原始视图,例如TextColor,您可以将其用作自定义视图的基本构建块。

打开ContentView.swift,并确保其画布处于打开状态(Option-Command-Return)。然后单击+按钮或按Command-Shift-L打开库:

第一个选项卡列出了用于布局和控制的基本视图,以及“其他视图”和“绘画”。 其中许多工具(尤其是控件视图)作为UIKit元素是您熟悉的,但其中一些是SwiftUI特有的。

第二个选项卡列出了用于布局,效果,文本,事件和其他用途(例如演示,环境和可访问性)的修饰符。 修饰符是一种从现有视图创建新视图的方法。 您可以像管道一样链接修饰符以自定义任何视图。

SwiftUI鼓励您创建小的可重用视图,然后使用修饰符针对使用它们的特定上下文自定义它们。 不用担心,SwiftUI将修改后的视图折叠为有效的数据结构,因此您将获得所有便利,而不会产生明显的性能损失。


Creating a Basic List

首先为您的主从应用程序的主视图创建一个基本列表。 在UIKit应用中,这将是UITableViewController

编辑ContentView看起来像这样:

struct ContentView: View {
  let disciplines = ["statue", "mural", "plaque"]
  var body: some View {
    List(disciplines, id: \.self) { discipline in
      Text(discipline)
    }
  }
}

您创建一个字符串的静态数组,并在列表List视图中显示它们,该视图在数组上进行迭代,显示为每个项目指定的内容。 结果看起来就像一个UITableView

确保画布是打开的,然后刷新预览(单击Resume或按Option-Command-P):

就像您期望看到的一样,这里有您的清单。 那有多容易? 在tableView(_:cellForRowAt :)中,没有实现UITableViewDataSource的方法,没有要配置的UITableViewCell,也没有要拼写错误的UITableViewCell标识符!

1. The List id Parameter

List的参数是数组(很明显)和id(不太明显)。 List希望每个项目都有一个标识符,因此它知道有多少个唯一项目(而不是tableView(_:numberOfRowsInSection :))。 参数\ .self告诉List每个项目都是由其自身标识的。 只要该项的类型符合所有内置类型都遵循的Hashable协议,就可以这样做。

现在,仔细研究id的工作原理:向disciplines添加另一个statue

let disciplines = ["statue", "mural", "plaque", "statue"]

刷新预览:将显示所有四个项目。 但是,根据id:\ .self,只有三个唯一项。 断点可能会有所启发。

Text(discipline)处添加一个断点。

2. Starting Debug Preview

实时预览(Live Preview)按钮是画布设备右下角附近的“播放”按钮。 它在画布上运行视图,但是普通的实时预览不会在断点处停止。 右键单击或按住Control键单击“实时预览”按钮,然后从菜单中选择“调试预览”(Debug Preview)

第一次运行Debug Preview时,将花费一些时间来加载所有内容。 最终,执行将在您的断点处停止,并且Variables View显示discipline

单击Continue program execution按钮:现在discipline = "mural"

再次单击Continue以查看discipline = "plaque"

现在,下次您单击Continue按钮时,您认为会发生什么?又是statue!这是第四个清单项目吗?

好吧,再单击两次Continue以再次看到“mural”“plaque”。然后,最后一个继续显示四个项目的列表。因此,不,第四个列表项不会停止执行。

您刚刚看到的是:执行两次访问了三个唯一项; “statue”在每次运行中仅出现一次。因此List只会看到三个独特的项目。对于这个简单的字符串列表来说,这不是问题,但是您很快就会看到一个非唯一id问题的示例。

您还将学习处理id参数的更好方法。但是首先,您将看到导航到详细视图的简便性。

单击Live Preview按钮将其停止,然后删除断点。


Navigating to the Detail View

您刚刚看到了显示主视图有多么容易。导航到详细视图几乎一样容易。

首先,将List嵌入到NavigationView中,如下所示:

NavigationView {
  List(disciplines, id: \.self) { discipline in
    Text(discipline)
  }
  .navigationBarTitle("Disciplines")
}

这就像将视图控制器嵌入导航控制器中:现在,您可以访问所有导航内容,例如导航栏标题。 请注意,.navigationBarTitle修改List,而不是NavigationView。 您可以在NavigationView中声明多个视图,每个视图可以具有自己的.navigationBarTitle

刷新预览以查看外观:

真好! 默认情况下,您会得到一个大标题。 这对于主列表很好,但是您将对详细视图的标题进行其他操作。

1. Creating a Navigation Link

NavigationView还启用了NavigationLink,它需要一个destination视图和一个label-就像在storyboard中创建segue,但没有那些烦人的segue标识符。

因此,首先,创建您的DetailView。 现在,只需在ContentView结构体下面的ContentView.swift中声明它:

struct DetailView: View {
  let discipline: String
  var body: some View {
    Text(discipline)
  }
}

它具有单个属性,并且像任何Swift结构一样,具有默认的初始化程序-在这种情况下为DetailView(discipline:String)。 该视图只是String本身,以Text视图显示。

现在,在ContentViewList闭包内部,将行视图Text(discipline)放入NavigationLink按钮中:

List(disciplines, id: \.self) { discipline in
  NavigationLink(
    destination: DetailView(discipline: discipline)) {
      Text(discipline)
  }
}

没有prepare(for:sender :)-您只需将当前列表项传递给DetailView即可初始化其discipline属性。

刷新预览以在每行的后沿看到disclosure箭头:

启动实时预览(Live Preview),然后点击一行以显示其详细信息视图:

而且,可以正常工作! 注意,您也获得了正常的后退按钮。

但是视图看起来很普通-甚至没有标题。

因此,添加标题,如下所示:

var body: some View {
  Text(discipline)
    .navigationBarTitle(Text(discipline), displayMode: .inline)
}

该视图由NavigationLink呈现,因此不需要它自己的NavigationView即可显示navigationBarTitle。 但是,此版本的navigationBarTitle的标题参数需要使用Text视图-如果仅使用discipline字符串进行尝试,则会收到毫无意义的错误消息。 按住Option键单击两个NavigationBarTitle修饰符,以查看titletitleKey参数类型的不同。

displayMode:.inline参数显示常规尺寸的标题。

再次启动Live-preview,然后点击一行以查看标题:

现在,您知道了如何创建基本的主从应用程序。 您使用了String对象,以避免任何可能使列表和导航的工作变得混乱的混乱情况。 但是列表项通常是您定义的模型类型的实例。 现在该使用一些实际数据了。


Revisiting Honolulu Public Artworks

入门项目包含Artwork.swift文件。 Artwork是具有八个属性的结构,除最后一个属性外,所有常量都可以由用户设置:

struct Artwork {
  let artist: String
  let description: String
  let locationName: String
  let discipline: String
  let title: String
  let imageName: String
  let coordinate: CLLocationCoordinate2D
  var reaction: String
}

结构下面是artDataArtwork对象的数组。

一些artData项的reaction属性是💕,🙏或🌟,但是对于大多数项目而言,它只是一个空字符串。 这个想法是当用户访问艺术品时,他们在应用程序中对其做出反应。 因此,如果出现空字符串reaction,则表示用户尚未访问过该艺术品。

现在开始更新项目以使用ArtworkartData:在ContentView中,添加以下属性:

let artworks = artData

删除disciplines数组。

artworks替换disciplines

List(artworks, id: \.self) { artwork in
  NavigationLink(
    destination: DetailView(artwork: artwork)) {
      Text(artwork.title)
  }
}
.navigationBarTitle("Artworks")

并编辑DetailView以使用Artwork

struct DetailView: View {
  let artwork: Artwork

  var body: some View {
    Text(artwork.title)
      .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

啊,Artwork不可Hashable! 因此,将\ .self更改为\ .title

List(artworks, id: \.title) { artwork in

您很快就会为DetailView创建一个单独的文件,但是现在就可以了。

现在,再来看一下List视图中的id参数。

1. Creating Unique id Values With UUID()

id参数的参数可以使用列表项的Hashable属性的任意组合。 但是,就像为数据库选择主键一样,很容易弄错它,然后找出使标识符不像您想象的那样唯一的困难方法。

Artwork title是唯一的,但是要查看id值不是唯一的情况,请在List中用\ .discipline替换\ .title

List(artworks, id: \.discipline) { artwork in

刷新预览(Option-Command-P)

artData中的标题各不相同,但列表认为所有statues均为“ Jonah Kuhio Kalanianaole”,所有壁画均为“The Makahiki Festival Mauka Mural”,所有匾额均为“Amelia Earhart Memorial Plaque”。 这些都是artData中出现的该discipline的第一项。 如果您的列表项没有唯一的id值,就会发生这种情况。

幸运的是,该解决方案很容易-它几乎可以完成许多数据库的工作:在模型类型中添加id属性,并使用UUID()为每个新对象生成唯一的标识符。

Artwork.swift中,将此属性添加到Artwork属性列表的顶部:

let id = UUID()

您可以使用UUID()来让系统生成唯一的ID值,因为您无需担心ID的实际值。 这个唯一的ID以后将非常有用!

然后,在ContentView.swift中,将List中的id参数更改为\ .id

List(artworks, id: \.id) { artwork in

刷新预览

现在,每个artwork都有一个唯一的id值,因此列表可以正确显示所有内容。

注意:如果仅刷新预览不能解决该列表,请构建项目(Command-B),然后刷新预览。

2. Conforming to Identifiable

但是有一种更好的方法:返回Artwork.swift,并在Artwork结构之外添加此扩展名:

extension Artwork: Identifiable { }

id属性是使Artwork符合Identifiable所需的全部,并且您已经添加了该属性。

现在,您可以完全删除id参数:

List(artworks) { artwork in

现在看起来更整洁了! 由于Artwork符合Identifiable,因此List知道它具有id属性,并自动将此属性用作其id参数。

刷新预览(Option-Command-P)

而且仍然可以正常工作。


Showing More Detail

Artwork对象具有很多可以显示的信息,因此请更新DetailView以显示更多详细信息。

首先,创建一个新的SwiftUI View文件:Command-N ▸ iOS ▸ User Interface ▸ SwiftUI View。 将其命名为DetailView.swift

ContentView.swift中的DetailView替换新文件中的DetailView。 确保从ContentView.swift中将其删除。

预览需artwork参数,因此添加它:

struct DetailView_Previews: PreviewProvider {
  static var previews: some View {
    DetailView(artwork: artData[0])
  }
}

然后,向视图添加许多新内容:

struct DetailView: View {
  let artwork: Artwork

  var body: some View {
    VStack {
      Image(artwork.imageName)
        .resizable()
        .frame(maxWidth: 300, maxHeight: 600)
        .aspectRatio(contentMode: .fit)
      Text("\(artwork.reaction)  \(artwork.title)")
        .font(.headline)
        .multilineTextAlignment(.center)
        .lineLimit(3)
      Text(artwork.locationName)
        .font(.subheadline)
      Text("Artist: \(artwork.artist)")
        .font(.subheadline)
      Divider()
      Text(artwork.description)
        .multilineTextAlignment(.leading)
        .lineLimit(20)
    }
    .padding()
    .navigationBarTitle(Text(artwork.title), displayMode: .inline)
  }
}

您正在以垂直布局显示多个视图,因此所有内容都在VStack中。

首先是图像ImageartData图像的大小和宽高比都不同,因此您可以指定宽高比适合,并将frame限制为最多300点宽,600点高。 但是,除非您首先将Image修改为可调整resizable大小,否则这些修改器不会生效。

您修改Text视图以指定字体大小和multilineTextAlignment,因为某些标题和描述对于一行来说太长了。

最后,在堆栈周围添加一些填充。

刷新预览:

还有Prince Jonah! 以防万一,Kalanianaole中有七个音节,最后六个字母中有四个。

当您预览甚至实时预览DetailView时,导航栏不会出现,因为它不知道它在导航堆栈中。

返回ContentView.swift并启动Live Preview,然后点击一行以查看完整的详细信息视图:


Handling Split View

到目前为止,我一直在向您展示iPhone 8 scheme的预览。 但是,当然,您可以在iPad上(甚至在Mac上,作为Mac Catalyst应用程序)查看此内容。

要查看在iPad上的外观,请选择一个iPad scheme,然后重新启动Live Preview

这是iPad,因此SwiftUI会显示分割视图(split view)。 当iPad处于纵向时,您必须从前端滑动以打开主列表视图,然后选择一个项目:

为避免在启动时显示空白详细视图,只需在ContentView中的List之后添加特定的DetailView。 在.navigationBarTitle(“ Artworks”)之后添加以下内容:

DetailView(artwork: artworks[0])

刷新预览(不必实时预览):

现在,split view将使用默认的详细视图加载。

将方案改回iPhone,可以看到这个DetailView不会弄乱您的主列表视图!

注意:Xcode的Master-Detail模板通过使用.navigationViewStyle(DoubleColumnNavigationViewStyle())修改NavigationView来明确显示这一点。 如果您根本不想split view,请指定StackNavigationViewStyle()强制执行iPhone样式的导航堆栈行为。


Declaring Data Dependencies

您已经了解了声明UI的简便性。现在是时候了解SwiftUI的另一个重要功能:声明性数据依赖项(declarative data dependencies)

1. Guiding Principles

SwiftUI有两个指导原则来管理数据如何通过您的应用程序流动:

  • Data access = dependency:读取视图中的一条数据会为该视图中的数据创建依赖关系。每个视图都是其数据依赖关系的函数-输入或状态。
  • Single source of truth:视图读取的每条数据都有一个事实来源,该来源要么由视图拥有,要么位于视图外部。无论事实的来源在哪里,您都应该始终有一个事实的来源。您可以通过传递对事实源的绑定来对其进行读写访问。

UIKit中,视图控制器使模型和视图保持同步。在SwiftUI中,声明性视图层次结构加上事实的单一来源意味着您不再需要视图控制器。

2. Tools for Data Flow

SwiftUI提供了多种工具来帮助您管理应用程序中的数据流。

属性包装器(Property wrappers)增强了变量的行为。特定于SwiftUI的包装器-@ State,@ Binding,@ ObservedObject@EnvironmentObject-声明了视图对变量表示的数据的依赖关系。

每个包装器指示不同的数据源:

  • 视图拥有@State变量。@State var分配持久性存储,因此您必须初始化其值。 Apple建议您将这些标记为private,以强调@State变量专门由该视图拥有和管理。
  • @Binding声明对另一个视图拥有的@State var的依赖关系,该变量使用$前缀将对此状态变量的绑定传递给另一个视图。在接收视图中,@ Binding var是对数据的引用,因此不需要初始化。该引用使视图可以编辑依赖于此数据的任何视图的状态。
  • @ObservedObject声明对符合ObservableObject协议的引用类型的依赖:它实现了objectWillChange属性以发布对其数据的更改。
  • @EnvironmentObject声明对某些共享数据的依赖-这些数据对于应用程序中的所有视图都是可见的。这是一种间接传递数据的简便方法,而不是将数据从父视图传递到子视图与孙子视图,尤其是在子视图不需要时。

现在继续练习使用@State@Binding进行导航。


Adding a Navigation Bar Button

如果Artworkreaction值为💕,🙏或🌟,则表明用户已经访问了该艺术品。 一个有用的功能是让用户隐藏他们访问过的艺术品,以便他们随后可以选择其他人之一进行访问。

在本部分中,您将在导航栏中添加一个按钮,以仅显示用户尚未访问的艺术品。

首先在艺术品标题旁边的列表行中显示reaction值:将Text(artwork.title)更改为以下内容:

Text("\(artwork.reaction)  \(artwork.title)")

刷新预览以查看哪些项目有非空reaction

现在,将这些属性添加到ContentView的顶部:

@State private var hideVisited = false
var showArt: [Artwork] {
  hideVisited ? artworks.filter { $0.reaction == "" } : artworks
}

@State属性包装器声明了数据依赖关系:更改此hideVisited属性的值将触发对此视图的更新。 在这种情况下,更改hideVisited的值将隐藏或显示已访问的艺术品。 您将其初始化为false,因此启动应用程序时,列表将显示所有艺术品。

如果hideVisitedfalse,则计算的属性showArt是所有artworks; 否则,它是artworks的子阵列,仅包含艺术品中具有空字符串reaction的那些物品。

现在,将List声明的第一行替换为:

List(showArt) { artwork in

现在,在.navigationBarTitle(“ Artworks”)之后,在列表List中添加navigationBarItems修饰符:

.navigationBarItems(trailing:
  Toggle(isOn: $hideVisited, label: { Text("Hide Visited") }))

您要在导航栏的右侧(后缘)添加导航栏项。 此项目是带有标签“Hide Visited”的切换视图。

您将绑定$ hideVisited传递给Toggle。 绑定允许读写访问,因此Toggle能够在用户点击时更改hideVisited的值。 此更改将通过更新列表视图进行。

启动实时预览以查看此工作:

轻触切换开关,即可查看所访问的artworks消失:仅保留具有空字符串reactions的艺术品。 再次点击以查看再次出现的参观艺术品。

您刚刚实现的Toggle的另一种选择是:tab view! 当我告诉您在SwiftUI中轻松实现标签视图时,您不会感到惊讶。 为用户设置对艺术品的反应方式后,您将立即执行此操作,因为这将使未访问的标签更加有趣。


Reacting to Artwork

该应用程序缺少的一项功能是用户对艺术品进行反应的一种方式。 在本部分中,您将在列表行中添加一个上下文菜单,以允许用户设置对该作品的反应。

1. Adding a Context Menu

仍在ContentView.swift中,将artworks设为@State变量:

@State var artworks = artData

ContentView结构是不可变的,因此您需要此@State属性包装器才能将值分配给Artwork属性。

接下来,将此辅助方法存根添加到ContentView

private func setReaction(_ reaction: String, for item: Artwork) { }

然后将contextMenu修饰符添加到列表行Text视图中:

Text("\(artwork.reaction)  \(artwork.title)")
  .contextMenu {
    Button("Love it: 💕") {
      self.setReaction("💕", for: artwork)
    }
    Button("Thoughtful: 🙏") {
      self.setReaction("🙏", for: artwork)
    }
    Button("Wow!: 🌟") {
      self.setReaction("🌟", for: artwork)
    }
}

注意:每当在闭包内部使用view属性或方法时,都必须使用self。 —不用担心,如果您忘记了,Xcode会告诉您并提出修复它。

上下文菜单显示三个按钮,每个反应一个。 每个按钮都使用适当的表情符号调用setReaction(_:for :)

最后,实现setReaction(_:for :)帮助器方法:

private func setReaction(_ reaction: String, for item: Artwork) {
  if let index = self.artworks.firstIndex(
    where: { $0.id == item.id }) {
    artworks[index].reaction = reaction
  }
}

这就是唯一ID值的用途! 您可以比较id值,以在artworks数组中找到该项目的索引,然后设置该数组项目的reaction值。

注意:您可能会想,直接设置Artwork.reaction =“💕”会更容易。 不幸的是,artwork列表迭代器是一个let常量。

刷新实时预览(Option-Command-P),然后触摸并按住一个项目以显示上下文菜单。 点击上下文菜单按钮以选择reaction,或点击菜单外部以将其关闭。

那让你感觉如何? 💕 🙏 🌟!


Creating a Tab View App

现在,您可以构建一个替代应用,该应用使用tab view列出所有艺术品或仅列出未访问的艺术品。

首先创建一个新的SwiftUI View文件来创建您的备用主视图。 将其命名为ArtTabView.swift

接下来,复制ContentView内部的所有代码-而不是结构ContentView行或右括号-并将其粘贴到结构体ArtTabView闭包内,替换样板代码。

现在,在画布处于打开状态(Option-Command-Return)的同时,单击Command-单击List,然后从菜单中选择Extract Subview

命名新的子视图ArtList

接下来,删除navigationBarItems开关。 第二个选项卡将替换此功能。

现在将这些属性添加到ArtList中:

@Binding var artworks: [Artwork]
let tabTitle: String
let hideVisited: Bool

您将传递一个绑定到@State变量艺术品,从ArtTabViewArtList。 这样,上下文菜单仍然可以使用。

每个标签都需要一个导航栏标题。 您将使用hideVisited来控制显示哪些项目,尽管它不再需要是@State变量。

接下来,将showArtsetReactionArtTabView移到ArtList,以处理ArtList中的这些工作。

然后将.navigationBarTitle(“ Artworks”)替换为:

.navigationBarTitle(tabTitle)

几乎存在:在ArtTabViewbody中,向ArtList添加必要的参数:

ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)

刷新预览以检查所有内容是否仍然有效:

看起来不错! 现在,通过将ArtTabViewbody定义替换为如下部分,从而使TabView具有两个tabs

TabView {
  NavigationView {
    ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)
    DetailView(artwork: artworks[0])
  }
  .tabItem({
    Text("Artworks 💕 🙏 🌟")
  })
  
  NavigationView {
    ArtList(artworks: $artworks, tabTitle: "Unvisited Artworks", hideVisited: true)
    DetailView(artwork: artworks[0])
  }
  .tabItem({ Text("Unvisited Artworks") })
}

第一个标签是未过滤的列表,第二个标签是未访问的艺术品的列表。tabItem修饰符指定每个选项卡上的标签。

启动实时预览体验您的替代应用程序:

Unvisited Artworks标签中,使用快捷菜单向艺术品添加reaction:由于不再参观,该艺术品从此列表中消失了!

注意:要使用此视图启动应用,请打开SceneDelegate.swift并将let contentView = ContentView()替换为let contentView = ArtTabView()


Displaying a Modal Sheet

此应用程序缺少的另一个功能是地图-您想访问此艺术品,但是它在哪里,以及如何到达那里?

SwiftUI没有地图基元视图,但是Apple的Interfacing With UIKit教程中有一个。我对其进行了修改,添加了pin annotation,并将其包含在入门项目中。

1. UIViewRepresentable Protocol

打开MapView.swift:这是一个托管MKMapView的视图。 makeUIViewupdateUIView中的所有代码都是标准的MapKitSwiftUI的神奇之处在于UIViewRepresentable协议及其必需的方法中-您猜对了:makeUIViewupdateUIView。这显示了在SwiftUI项目中显示UIKit视图有多么容易。它也适用于您的任何自定义UIKit视图。

现在尝试预览MapView(Option-Command-P)。好吧,它正在尝试显示地图,但它并不在那里。诀窍是:您必须启动Live Preview才能查看地图:

预览使用artData [5] .coordinate作为样本数据,因此地图图钉显示了檀香山动物园大象展览的位置,您可以在其中参观长颈鹿雕塑。

2. Adding a Button

现在回到DetailView.swift,它需要一个按钮来显示地图。 您可以将一个放置在导航栏中,但是在艺术品位置旁边也是放置“显示地图”按钮的合理位置。

要将Button放置在Text视图旁边,您需要一个HStack。 确保画布处于打开状态(Option-Command-Return),然后在此代码行中按Command并单击Text

Text(artwork.locationName)

然后从菜单中选择Embed in HStack

现在,要将按钮放置在位置文本的左侧,请将其添加到HStack中的Text之前:打开库(Shift-Command-L),然后将Button拖到您的代码中Text(artwork.locationName)的上方。

注意:拖动Button时,将鼠标悬停在Text附近,直到在Text上方打开新行,然后释放Button

您的代码现在如下所示:

Button(action: {}) {
  Text("Button")
}
Text(artwork.locationName)
  .font(.subheadline)

Text("Button")是按钮的标签。 更改为:

Image(systemName: "mappin.and.ellipse")

刷新预览

注意:此系统图像来自Apple的新SFSymbols系列。 要查看完整的套件,请从Apple下载并安装SF Symbols应用程序。 至少有两个符号似乎已被弃用:我尝试使用mappin.circle及其填充版本,但没有出现。

因此标签看起来正确。 现在,按钮的action应该怎么做?

3. Showing a Modal Sheet

您将以模式表的形式显示地图。 它在SwiftUI中的工作方式是使用Bool值,该值是模式表的参数。 SwiftUI仅在该值为true时显示模式表。

操作如下:在DetailView顶部,添加以下@State属性:

@State private var showMap = false

同样,您要声明一个数据依赖性:更改showMap的值会触发显示和关闭模式表。 您将showMap初始化为false,这样在加载DetailView时地图不会出现。

接下来,在按钮的action中,将showMap设置为true。 所以您的Button现在看起来像这样:

Button(action: { self.showMap = true }) {
  Image(systemName: "mappin.and.ellipse")
}

好的,您的按钮已准备就绪。 现在,您在哪里声明模态表? 好吧,您将其附加为修饰符。 任何看法! 您不必将其附加到按钮上,但这是放置按钮最明显的地方。 因此,修改您的新按钮:

Button(action: { self.showMap = true }) {
  Image(systemName: "mappin.and.ellipse")
}
.sheet(isPresented: $showMap) {
  MapView(coordinate: self.artwork.coordinate)
}

您将绑定传递给showMap作为工作表的isPresented参数,因为必须将其值更改为false才能关闭工作表。 系统或工作表视图都会进行此更改。

注意:修改器的isPresented参数是显示或隐藏工作表的一种方法。 触发器也可以是可选对象。 在这种情况下,修饰符的item参数将绑定到可选对象。 该对象变为非nil时,该工作表出现,而该对象变为nil时,该工作表消失。

您将MapView指定为要显示的视图,并将该artwork的位置坐标作为coordinate参数传递。

要测试新按钮,请切换到ContentView.swift,然后运行实时预览。 然后点击一个项目以查看其DetailView,然后点击地图按钮:

还有地图钉(map pin)

注意:创建alertaction sheet or popover的过程与创建过程相同。您可以在修饰符-.alert,.actionSheet或.popover中声明工作表。要显示或隐藏工作表,您可以将绑定传递给Bool变量作为isPresented的参数,或传递给可选对象作为item的参数。然后,创建带有标题,消息和按钮的AlertActionSheet.popover修饰符仅需要显示一个视图。

4. Dismissing the Modal Sheet

现在,如何移除modal sheet?通常,在iPhone上,您只需向下滑动模式视图即可将其关闭。这个手势告诉SwiftUI将Bool值设置为false,模态消失。

但是,当您滑动时,此MapView会滚动!公平地说,这可能就是您想要的,这正是您的用户所期望的。因此,您必须提供一个按钮来手动关闭地图。

为此,您需要将MapView包裹在另一个视图中,您可以在其中添加Done按钮。在使用时,您将添加标签以显示艺术品的locationName

首先,创建一个新的SwiftUI View文件,并将其命名为LocationMap.swift

接下来,将这些属性添加到LocationMap中:

@Binding var showModal: Bool
var artwork: Artwork

您需要将$ showMap作为showModal参数传递给LocationMap。 这是@Binding,因为LocationMap会将showModal更改为false,并且此更改必须流回到DetailView才能关闭模式表。

然后,您将整个artwork对象传递给LocationMap,使它可以访问coordinatelocationName属性。

现在,预览需要showModalartwork的值,因此添加以下参数:

LocationMap(showModal: .constant(true), artwork: artData[0])

注意:showModal的参数必须是绑定,而不是纯值。 您可以使用.constant()将任何普通值更改为绑定。

接下来,将body替换为以下内容:

var body: some View {
  VStack {
    MapView(coordinate: artwork.coordinate)
    HStack {
      Text(self.artwork.locationName)
      Spacer()
      Button("Done") { self.showModal = false }
    }
    .padding()
  }
}

内部HStack包含位置名称和Done按钮。 Spacer将两个视图分开。

VStackMapView放置在HStack上方,HStack周围有一些填充。

启动实时预览以查看其外观:

正是您所期望的样子!

现在,回到DetailView.swift:用以下这一行替换MapView(coordinate:self.artwork.coordinate)

LocationMap(showModal: self.$showMap, artwork: self.artwork)

您正在显示LocationMap而不是MapView,并向showMapartwork对象传递了绑定。

现在再次实时预览ContentView,点击一个项目,然后点击地图按钮。

然后点击Done以关闭地图。 做得好!


Bonus Section: Eager Evaluation

SwiftUI应用程序启动时发生了一件奇怪的事情:它初始化出现在ContentView中的每个对象。 例如,它会在用户点击导航到该视图的任何内容之前初始化DetailView。 它将初始化List中的每个项目,无论该项目在窗口中是否可见。

这是一种eager evaluation方式,也是编程语言的常见策略。 这是个问题吗? 好吧,如果您的应用程序包含大量项目,并且每个项目都下载了一个大型媒体文件,则您可能不希望初始化程序开始下载。

要模拟正在发生的事情,请向Artwork添加一个init()方法,以便您可以包含一条打印语句:

init(
  artist: String, 
  description: String, 
  locationName: String, 
  discipline: String,
  title: String, 
  imageName: String, 
  coordinate: CLLocationCoordinate2D, 
  reaction: String
) {
  print(">>>>> Downloading \(imageName) <<<<<")
  self.artist = artist
  self.description = description
  self.locationName = locationName
  self.discipline = discipline
  self.title = title
  self.imageName = imageName
  self.coordinate = coordinate
  self.reaction = reaction
}

现在,在ContentView.swift中,启动Debug Preview (Control-click the Live Preview button,然后观察调试控制台:

>>>>> Downloading 002_200105 <<<<<
>>>>> Downloading 19300102 <<<<<
>>>>> Downloading 193701 <<<<<
>>>>> Downloading 193901-5 <<<<<
>>>>> Downloading 195801 <<<<<
>>>>> Downloading 198912 <<<<<
>>>>> Downloading 196001 <<<<<
>>>>> Downloading 193301-2 <<<<<
>>>>> Downloading 193101 <<<<<
>>>>> Downloading 199909 <<<<<
>>>>> Downloading 199103-3 <<<<<
>>>>> Downloading 197613-5 <<<<<
>>>>> Downloading 199802 <<<<<
>>>>> Downloading 198803 <<<<<
>>>>> Downloading 199303-2 <<<<<
>>>>> Downloading 19350202a <<<<<
>>>>> Downloading 200304 <<<<<

毫不奇怪,它初始化了所有Artwork项目。 如果有1000个项目,并且每个项目都下载了较大的图像或视频文件,那么这对于移动应用程序可能是个问题。

这是一个可能的解决方案:将下载活动移至帮助方法,并仅在该项目出现在屏幕上时调用此方法。

Artwork.swift中,注释掉init()并添加此方法:

func load() {
  print(">>>>> Downloading \(self.imageName) <<<<<")
}

然后返回ContentView.swift,修改List行:

Text("\(artwork.reaction)  \(artwork.title)")
  .onAppear() { artwork.load() }

仅当此Artwork的行在屏幕上时,才调用load()

启动调试预览:

<code>
>>>>> Downloading 002_200105 <<<<<
>>>>> Downloading 19300102 <<<<<
>>>>> Downloading 193701 <<<<<
>>>>> Downloading 193901-5 <<<<<
>>>>> Downloading 195801 <<<<<
>>>>> Downloading 198912 <<<<<
>>>>> Downloading 196001 <<<<<
>>>>> Downloading 193301-2 <<<<<
>>>>> Downloading 193101 <<<<<
>>>>> Downloading 199909 <<<<<
>>>>> Downloading 199103-3 <<<<<
>>>>> Downloading 197613-5 <<<<<
>>>>> Downloading 199802 <<<<<
</code>

后记

本篇主要讲述了基于SwiftUI的导航的实现,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容