版本记录
版本号 | 时间 |
---|---|
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
。 熟悉UIKit
和SwiftUI
将有所帮助。
在PublicArt-Starter
文件夹中打开PublicArt
项目。 您将使用此项目中已经包含的Artwork.swift
和MapView.swift
文件构建主从应用程序。
SwiftUI Basics in a Nutshell
SwiftUI
允许您忽略Interface Builder
和storyboards
,而无需编写详细的分步说明来布局UI。 您可以将SwiftUI
视图及其代码并排预览-更改一侧会更新另一侧,因此它们始终保持同步。 没有任何标识符字符串会出错。 它是代码,但比您为UIKit编写的要少得多,因此更易于理解,编辑和调试。 这不是很好吗?
画布预览意味着您不需要storyboard
。 子视图会保持更新,因此您也不需要视图控制器。 实时预览意味着您几乎不需要启动模拟器。
SwiftUI
不会取代UIKit
,就像Swift
和Objective-C
一样,您可以在同一应用程序中同时使用两者。 在本教程的最后,您将看到在SwiftUI
应用程序中使用UIKit
视图有多么容易。
1. Declarative App Development
SwiftUI
使您可以进行声明式(declarative)
应用程序开发:您可以声明希望UI中的视图的外观以及它们所依赖的数据。 SwiftUI
框架负责在视图应出现时创建视图,并在它们依赖的数据发生更改时对其进行更新。它重新计算视图及其所有子级,然后呈现已更改的内容。
视图的状态取决于其数据,因此您可以为视图声明可能的状态,以及每种状态下视图的外观-视图如何对数据更改做出反应或数据如何影响视图。是的,SwiftUI
绝对具有反应性!因此,如果您已经在使用一种反应式编程框架,那么使用SwiftUI
可能会更轻松。
2. Declaring Views
SwiftUI
视图是您的UI的一部分:您可以合并较小的视图以构建较大的视图。有许多原始视图,例如Text
和 Color
,您可以将其用作自定义视图的基本构建块。
打开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
视图显示。
现在,在ContentView
的List
闭包内部,将行视图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
修饰符,以查看title
和titleKey
参数类型的不同。
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
}
结构下面是artData
,Artwork
对象的数组。
一些artData
项的reaction
属性是💕,🙏或🌟,但是对于大多数项目而言,它只是一个空字符串。 这个想法是当用户访问艺术品时,他们在应用程序中对其做出反应。 因此,如果出现空字符串reaction
,则表示用户尚未访问过该艺术品。
现在开始更新项目以使用Artwork
和artData
:在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
中。
首先是图像Image
:artData
图像的大小和宽高比都不同,因此您可以指定宽高比适合,并将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
如果Artwork
的reaction
值为💕,🙏或🌟,则表明用户已经访问了该艺术品。 一个有用的功能是让用户隐藏他们访问过的艺术品,以便他们随后可以选择其他人之一进行访问。
在本部分中,您将在导航栏中添加一个按钮,以仅显示用户尚未访问的艺术品。
首先在艺术品标题旁边的列表行中显示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
,因此启动应用程序时,列表将显示所有艺术品。
如果hideVisited
为false
,则计算的属性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
变量艺术品,从ArtTabView
到ArtList
。 这样,上下文菜单仍然可以使用。
每个标签都需要一个导航栏标题。 您将使用hideVisited
来控制显示哪些项目,尽管它不再需要是@State
变量。
接下来,将showArt
和setReaction
从ArtTabView
移到ArtList
,以处理ArtList
中的这些工作。
然后将.navigationBarTitle(“ Artworks”)
替换为:
.navigationBarTitle(tabTitle)
几乎存在:在ArtTabView
的body
中,向ArtList
添加必要的参数:
ArtList(artworks: $artworks, tabTitle: "All Artworks", hideVisited: false)
刷新预览以检查所有内容是否仍然有效:
看起来不错! 现在,通过将ArtTabView
的body
定义替换为如下部分,从而使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
的视图。 makeUIView
和updateUIView
中的所有代码都是标准的MapKit
。 SwiftUI
的神奇之处在于UIViewRepresentable
协议及其必需的方法中-您猜对了:makeUIView
和updateUIView
。这显示了在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)
!
注意:创建
alert
,action sheet or popover
的过程与创建过程相同。您可以在修饰符-.alert,.actionSheet或.popover
中声明工作表。要显示或隐藏工作表,您可以将绑定传递给Bool
变量作为isPresented
的参数,或传递给可选对象作为item
的参数。然后,创建带有标题,消息和按钮的Alert
或ActionSheet
。.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
,使它可以访问coordinate
和locationName
属性。
现在,预览需要showModal
和artwork
的值,因此添加以下参数:
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
将两个视图分开。
VStack
将MapView
放置在HStack
上方,HStack
周围有一些填充。
启动实时预览以查看其外观:
正是您所期望的样子!
现在,回到DetailView.swift
:用以下这一行替换MapView(coordinate:self.artwork.coordinate)
:
LocationMap(showModal: self.$showMap, artwork: self.artwork)
您正在显示LocationMap
而不是MapView
,并向showMap
和artwork
对象传递了绑定。
现在再次实时预览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的导航的实现,感兴趣的给个赞或者关注~~~