SwiftUI学习100天(Day70 - 项目 14,第三部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

是时候开始将我们所有的技术付诸实践了,这意味着构建一个地图视图,我们可以在其中添加注释并与之交互。随着我们的进步,我希望你能反思一下我们的应用程序如何从 iOS 附带的所有标准设计功能中获益,以及对我们的用户意味着什么——他们已经知道如何使用地图,以及如何点击标记激活功能,
多年前,史蒂夫·乔布斯说:“设计不仅仅是它看起来和感觉起来的样子;设计就是它的工作原理。” 用户知道我们的地图是如何工作的,因为它的工作方式与 iOS 上的所有其他地图一样。这意味着他们可以快速上手我们的应用程序,也意味着我们可以专注于将他们引导到我们应用程序中独特而有趣的部分。
今天,你需要完成三个主题,在这些主题中,我们将深入探讨 MapKit 与 SwiftUI 的集成。

将用户位置添加到地图
该项目将基于地图视图,要求用户将他们想要访问的地点添加到地图中。为此,我们需要放置 一个Map
使其占据我们的整个视图,跟踪其中心坐标,然后还要确定用户是否正在查看地点详细信息、他们有哪些注释等等。
我们将从全屏Map
视图开始,然后在顶部放置一个半透明圆圈来代表中心点。尽管此视图将具有跟踪中心坐标的绑定,但我们不需要使用它来放置圆圈——一个简单的方法ZStack
将确保圆圈始终位于地图的中心。
首先,添加import
一行,以便我们访问 MapKit 的数据类型:
其次,在ContentView
其中添加一个属性,用于存储地图的当前状态。稍后我们将使用它来添加地标:
这将启动地图,以便可以看到大部分西欧和北非。
现在我们可以填写body
属性
如果你现在运行该应用程序,你会看到你可以自由移动地图,但始终有一个蓝色圆圈准确显示中心位置。
所有这些工作本身并不是很有趣,所以下一步是在右下角添加一个按钮,让我们向地图添加地点标记。我们已经在一个ZStack
内
,所以对齐此按钮的最简单方法是将它放在一个VStack
内
,并且HStack
每次都在它前面加上间隔符。这两个垫片最终占据了剩下的整个垂直和水平空间,使得最后出现的任何东西都舒适地位于右下角。
我们将很快为该按钮添加一些功能,但首先让我们将其放置到位并添加一些基本样式以使其看起来不错。
请在VStack
下面添加Circle
:
请注意我是如何在那里添加padding()
修饰符两次的——一次是为了在我们添加背景颜色之前确保按钮更大,第二次是为了将它推离后缘。
有趣的是我们如何在地图上放置位置。我们已将地图的位置绑定到 ContentView
中的属性
,但现在我们需要发送要显示的位置数组。
这需要几个步骤,从我们在应用程序中创建的位置类型的基本定义开始。这需要符合一些协议:
Identifiable
,因此我们可以在地图中创建许多位置标记。Codable
,这样我们就可以轻松加载和保存地图数据。Equatable
,所以我们可以在一组位置中找到一个特定的位置。
就它将包含的数据而言,我们将为每个位置提供名称和描述,以及纬度和经度。我们还需要添加一个唯一标识符,以便 SwiftUI 乐于从动态数据创建它们。
因此,创建一个名为 Location.swift 的新 Swift 文件,并为其提供以下代码:
单独存储纬度和经度让我们Codable
开箱即用,这总是很好的。我们很快就会添加更多内容,但这足以让我们继续前进。
现在我们有了可以存储单个位置的数据类型,我们需要一个数组来存储用户想要访问的所有位置。我们暂时把它放到ContentView
我们可以开始的地方,但我们很快会再次回到它来添加更多内容。
因此,首先将此属性添加到ContentView
:
接下来,我们希望在点击 + 按钮时为其添加一个位置,因此将// create a new location
注释替换为:
最后,更新ContentView
以便它发送locations
要转换为注释的数组:
现在地图工作已经足够了,所以继续并再次运行你的应用程序——你应该能够根据需要四处移动,然后按 + 按钮添加位置。
我知道设置需要大量工作,但至少你可以看到应用程序的基础知识整合在一起!



改进我们的地图注释
现在我们正在使用MapMarker
在我们的Map
视图中放置位置,但 SwiftUI 允许我们将任何类型的视图放置在我们的地图之上,这样我们就可以拥有完全的可定制性。因此,我们将使用它来显示一个自定义 SwiftUI 视图,其中包含一个图标和一些文本来显示位置的名称,然后查看底层数据类型以了解可以在那里进行哪些改进。
多亏了 SwiftUI 的出色表现,这几乎不需要任何代码——MapMarker
用这个替换你现有的代码:
这已经是一个立竿见影的改进,因为现在每个标记代表什么都一目了然了——位置名称直接出现在下方。但是,我想超越 SwiftUI 视图:我想看看Location
结构本身,并应用一些改进使其变得更好。
首先,我不是特别喜欢必须CLLocationCoordinate2D
在我们的 SwiftUI 视图中创建一个,我更愿意将这种逻辑移动到我们的Location
结构中。因此,我们可以将其移至计算属性中以清理我们的代码。首先,将 MapKit 的导入添加到 Location.swift 中,然后将其添加到Location
:
现在我们的ContentView
代码更简单了:
我想做的第二个改变是我鼓励每个人在构建用于 SwiftUI 的自定义数据类型时做的改变:添加一个示例!这使得预览变得非常容易,因此我鼓励你在可能的情况下向你的类型添加一个example
静态属性,其中包含一些可以很好预览的示例数据。
因此,将第二个属性添加到Location
现在:
我想在这里做的最后一个更改是向==
结构添加自定义函数。我们已经让Location
符合Equatable
,这意味着我们已经可以使用==
将一个位置与另一个位置进行比较
。在幕后,Swift 会通过将每个属性与其他属性进行比较来为我们编写此函数,这是相当浪费的——我们所有的位置都已经有一个唯一的标识符,因此如果两个位置具有相同的标识符,那么我们可以确定它们是同样没有检查其他属性。
因此,我们可以通过为Location
编写自己的
,该函数只比较两个标识符:==
函数来节省大量工作
我非常喜欢让结构符合Equatable
标准,即使你不能像上面那样使用优化的比较函数——结构是简单的值,比如字符串和整数,我认为我们应该将相同的状态扩展到我们自己的自定义结构也是。
有了它,我们项目的下一步就完成了,所以请现在运行它——你应该可以放下一个标记并看到我们的自定义注释,但现在在幕后知道我们的代码也更整洁了!



选择和编辑地图注释
用户现在可以将标记放到我们的 SwiftUI Map
上
,但他们不能对它们做任何事情——他们不能附上自己的名字和描述。解决这个问题需要几个步骤,并在此过程中学习一些东西,但它确实将整个应用程序整合在一起,正如你将看到的那样。
首先,我们希望在用户选择地图注释时显示某种工作表,让他们有机会查看或编辑有关位置的详细信息。
我们之前处理工作表的方式意味着创建一个布尔值来确定工作表是否可见,然后发送一些其他数据供工作表显示或编辑。不过这一次,我们将采用不同的方法:我们将使用一个属性来处理所有问题。
因此,将其添加到ContentView
现在:
我们要说的是,我们可能有一个选定的位置,也可能没有——这就是 SwiftUI 需要知道的所有内容,以便呈现工作表。一旦我们将一个值放入该可选值中,我们就会告诉 SwiftUI 显示工作表,并且该值将自动设置回nil
,工作
表被关闭时的值。更好的是,SwiftUI 会自动为我们解包可选,因此当我们创建工作表的内容时,我们可以确保我们有真正的价值可以使用。
要尝试一下,请将此修饰符附加到ContentView
的
ZStack
:
如你所见,它需要一个可选绑定,还有一个函数,当它有一个值集时,该函数将接收解包的可选。因此,在里面我们的工作表可以直接引用place.name
而不需要打开可选的包装或使用 nil 合并。
现在要使整个事物栩栩如生,我们只需要通过向地图注释中selectedPlace
的 VStack
中添加点击手势来赋予一个值:
就是这样!我们现在可以显示一张显示所选位置名称的表格,而且只需要少量代码。这种可选绑定并不总是可行的,但我认为在可能的情况下它会产生更自然的代码——SwiftUI 自动解包可选的行为非常有用。
当然,仅仅显示地名并没有太大用处,因此这里的下一步是创建一个详细视图,用户可以在其中查看和调整地名和描述。这需要接收一个要编辑的位置,允许用户调整该位置的两个值,然后将使用调整后的数据发回一个新位置——它会像一个函数一样有效地工作,接收数据并发回转换后的东西。
与往常一样,我们将从小处着手,逐步推进,因此请创建一个名为“EditView”的新 SwiftUI 视图,然后为其提供以下代码:
该代码无法编译,因为我们遇到了一个难题:我们应该为name
和description
属性使用什么初始值?以前我们使用@State
过初始值,但我们不能在这里这样做——它们的初始值应该来自传入的位置,以便用户看到保存的数据。
解决方案是创建一个接受位置的新初始化程序,并使用它来创建State
使用位置数据的结构。这使用了我们在初始化器中创建获取请求时使用的相同下划线方法,它允许我们创建属性包装器的实例而不是包装器内的数据。
所以,为了解决我们的问题,我们需要将这个初始化器添加到EditView
:
你需要修改预览代码以使用该初始化程序:
这使得代码可以编译,但是我们有第二个问题:当我们完成位置编辑后,我们如何将新的位置数据传回?我们可以使用类似@Binding
传入远程值的方法,但这会给我们的可选输入带来问题ContentView
——我们希望EditView
绑定到一个真实值而不是可选值,否则会造成混淆。
我们将采用最简单的解决方案:我们将需要一个函数来调用我们可以传回我们想要的任何新位置的地方。这意味着任何其他 SwiftUI 都可以向我们发送一些数据,并取回一些新数据以进行我们想要的处理。
首先将此属性添加到EditView
:
这需要一个接受单个位置且不返回任何内容的函数,这非常适合我们的使用。我们需要在初始化程序中接受它,如下所示:
请记住,@escaping
意味着该函数稍后会被隐藏起来供用户使用,而不是立即被调用,这里需要它,因为onSave
只有当用户按下保存时才会调用该函数。
说到这里,我们需要更新保存按钮以使用修改后的详细信息创建一个新位置,并将其发回onSave()
:
通过获取原始位置的可变副本,我们可以访问其现有数据——标识符、纬度和经度。
也不要忘记更新你的预览代码——在这里只传递一个占位符闭包就可以了:
现在已经完成EditView
,但还有一些工作要做,ContentView
因为我们需要在我们的工作表中显示新的 UI,发送选定的位置,并处理更新更改。
好吧,由于我们构建代码的方式,这只需要几行代码——将其放入ContentView
中的
:sheet()
修饰符中
因此,它将位置传递给EditView
,并且还传递了一个闭包以在按下“保存”按钮时运行。它接受新位置,然后查找当前位置并在数组中替换它。这将使我们的地图立即更新为新数据。
继续尝试该应用程序——看看你是否发现我们的代码有问题。希望它相当明显:重命名实际上不起作用!
这里的问题是,我们告诉 SwiftUI 如果两个地方的 ID 相同,那么这两个地方是相同的,现在不再是这样了——当我们更新一个标记使其具有不同的名称时,SwiftUI 将比较旧标记和新标记,看到他们的ID是一样的,也就懒得去换地图了。
这里的解决方法是使id
属性可变,如下所示:
现在我们可以在创建新位置时进行调整:
对于什么时候最好从头开始制作一个全新的对象,或者只是复制一个现有的对象并像我们在这里做的那样更改你想要的比特,没有硬性规定;我鼓励你尝试并找到你喜欢的方法。
无论如何,你现在可以再次运行你的代码。当然,它还没有保存任何数据,但你现在可以根据需要添加任意数量的位置并为它们指定有意义的名称。
不过,还有最后一件事,这完全有可能在未来的 SwiftUI 更新中不存在,所以你自己试试吧:现在我发现给一个位置一个简短的名字,比如“家”,然后把它改成一个长名称,例如“这是我的家”,将导致其标签被剪裁,直到你与地图进行交互。
我们可以用一个名为 fixedSize()
的新修饰符来解决这个问题
,它强制任何视图都被赋予其自然大小,而不是试图适应其父视图提供的空间量。在这种情况下,MapAnnotation
并不能很好地处理调整子项的大小,这会导致剪裁,但fixedSize()
让我们绕过它,以便文本自动增长到所需的空间。
因此,要完成此步骤,请将你的地图注释内容修改为:
这是一个小改动,再次希望它能在未来的 SwiftUI 版本中得到解决,但它暂时解决了我们的问题。


