Within our app written in Swift and using SwiftUI, we needed a way to read our data from Google Firestore into the app, and then write some data back into Firestore.
在用Swift编写并使用SwiftUI的应用程序中,我们需要一种方法将数据从Google Firestore读取到应用程序中,然后再将一些数据写回到Firestore中。
Initially, I followed this guide from the Firestore documentation, although it involved a lot of mapping dictionary key/values to the type I was after. Writing data was much the same. For each property in my Swift class I had to specify the name of the corresponding Firestore field, and then assign the appropriate value to the field. I knew there must be a better way.
最初,我遵循了Firestore文档中的本指南 ,尽管该指南涉及将字典键/值映射到我后来使用的类型上。 写入数据几乎相同。 对于我的Swift类中的每个属性,我必须指定相应的Firestore字段的名称,然后将适当的值分配给该字段。 我知道一定有更好的方法。
Fortunately, the Firestore documentation recognises this problem and offers a way to bring your document into a Swift class, and allow you to use it as you would any other piece of data. This is through the use of the Pod FirebaseFirestoreSwift
, which adds an extension method to the document.data()
call so you can specify what type the data is coming in as. There is also a similar method on the document.setData()
method for converting your Swift object back to a Firestore object.
幸运的是,Firestore文档认识到了此问题,并提供了一种将文档带入Swift类的方法 ,并允许您像处理其他任何数据一样使用它。 这是通过使用Pod FirebaseFirestoreSwift
,该类将扩展方法添加到document.data()
调用中,以便您可以指定输入数据的类型。 document.setData()
方法上还有一个类似的方法 ,用于将您的Swift对象转换回Firestore对象。
However, I wasn’t sure how this would work where the data types weren’t simple String to String or Number to Int mappings. Specifically, there were five situations I wasn’t sure about (in order of least to most complicated):
但是,我不确定在数据类型不是简单的String到String或Number到Int映射的情况下这将如何工作。 具体来说,有五种我不确定的情况(从最小到最复杂):
- Arrays/lists 数组/列表
- Complex/custom objects 复杂/自定义对象
- Where the names of the fields didn’t match 字段名称不匹配的地方
- Enums 枚举
- Other data transformations 其他数据转换
如何将Swift对象转换为Firestore,反之亦然? (How do I convert my Swift object to Firestore and vice versa?)
The most basic example is when your object only has simple data types.
最基本的示例是对象仅具有简单数据类型时。
The corresponding Swift struct could look like this
相应的Swift结构可能看起来像这样
To get your Firestore object into Swift, you need to use the .data(as: <type>)
method. In the scenario below, I am getting all the documents in the Firestore collection websites
and putting them into a property called self.websites
要将您的Firestore对象放入Swift,您需要使用.data(as: <type>)
方法。 在以下情况下,我将在Firestore集合websites
获取所有文档,并将它们放入一个名为self.websites
的属性中。
A few things to note:
注意事项:
Line 12 is where I call the
document.data(as: <type>)
method. I pass the type I want to turn the document into here.第12行是我调用
document.data(as: <type>)
方法的地方。 我输入了我想把文档变成这里的类型。On line 11, I am using a
flatMap
. This is like a map in that it transforms every object in the array to another object. In this case I am transforming each document (which is essentially a dictionary or key/value pairs) to my Website struct. Except, if a result returns nil, which happens when it is unable to convert it into the specified struct, it will remove it from the array.在第11行,我正在使用
flatMap
。 就像映射一样,它将数组中的每个对象都转换为另一个对象。 在这种情况下,我要将每个文档(本质上是字典或键/值对)转换为我的网站结构。 除非结果返回nil(在无法将其转换为指定的结构时发生),否则它将从数组中将其删除。An equivalent way of doing this would be to first apply a filter on
一种等效的方法是首先在
documents
to remove any items which weren’t able to be converted into the Website struct. Because Firestore itself doesn’t require each document in a collection to look the same, it is possible to have documents which can’t be converted to the struct you have specified.documents
以删除所有无法转换为“网站”结构的项目。 由于Firestore本身并不需要集合中的每个文档看起来都一样,因此可能存在无法转换为指定结构的文档。
There is also the opposite method, which works much the same way but for saving data to Firestore. Note I am setting just the one document here.
还有另一种方法,其工作方式大致相同,但用于将数据保存到Firestore。 注意,我在这里仅设置一个文档。
All the code we’ve looked at won’t work yet. We are missing one small ingredient.
我们查看的所有代码尚无法使用。 我们缺少一种小成分。
We need to let Swift know how to convert the document, at this point just a dictionary, into the struct we would like. For this simple example, this is really easy. We only need to do three things to our struct.
我们需要让Swift知道如何将文档(目前只是字典)转换为我们想要的结构。 对于这个简单的例子,这确实很容易。 我们只需要对我们的结构做三件事。
Conform our struct to
Identifiable
. This requires it to have an id property, which we will add…使我们的结构符合可
Identifiable
。 这要求它具有一个id属性,我们将添加它。。。Add an id property and annotate it with
@DocumentID
. This is where Firestore will store the id of the document itself. This is required and is useful for saving the right document back to Firestore.添加一个id属性,并使用
@DocumentID
对其进行注释。 Firestore将在此处存储文档本身的ID。 这是必需的,对于将正确的文档保存回Firestore非常有用。Conform our struct to
Codable
. This is the essential step that allows Swift to convert to and from our struct to Firestore.使我们的结构符合
Codable
。 这是允许Swift在我们的结构与Firestore之间进行转换的关键步骤。
可编码 (Codable)
Codable is the awesome protocol (made up of two protocols: Decodable and Encodable) that adds two key functions:
Codable是令人敬畏的协议(由两个协议组成:Decodable和Encodable),它增加了两个关键功能:
- An init() method, which allows us to create the struct from the dictionary that Firestore returns. This is the decoder. 一个init()方法,它使我们能够从Firestore返回的字典中创建结构。 这是解码器。
- A method which allows us to convert our struct back to the dictionary format that Firestore is expecting. This is the encoder. 一种允许我们将结构转换回Firestore期望的字典格式的方法。 这是编码器。
Each of these methods separately comes from the Decodable and Encodable protocols respectively. If you have a struct that you only ever need to retrieve from Firestore, but never save back to Firestore, you could simply conform to the Decodable protocol.
这些方法分别分别来自“可解码”和“可编码”协议。 如果您有一个仅需要从Firestore检索但从未保存回Firestore的结构,则可以简单地遵循Decodable协议。
Fortunately Codable is quite smart, and if you have a struct like ours where there are only simple types like String, Int or Bool, that is all you need to do. Our code will now work.
幸运的是,Codable非常聪明,如果您拥有像我们这样的结构,而其中只有简单类型(如String,Int或Bool),那么您所需要做的就是。 我们的代码现在可以使用了。
1.如果我的对象有数组怎么办? (1. What if my object has an array?)
Fortunately, arrays are supported just as well as a standard type. See line 5 below.
幸运的是,数组和标准类型一样受支持。 请参阅下面的第5行。
This will convert an array of strings from Firestore straight into an array of strings in Swift. No extra code required.
这会将Firestore中的字符串数组直接转换为Swift中的字符串数组。 无需额外的代码。
2.如果我的对象具有自定义数据类型怎么办? (2. What if my object has custom data types?)
A common scenario is supporting custom data types you’ve written yourself. For example, my website could have an author attached to it, which is its own data type. Let’s view how this would be represented in Firestore.
一种常见的情况是支持您自己编写的自定义数据类型。 例如,我的网站可能附有作者,这是它自己的数据类型。 让我们看看如何在Firestore中表示它。
Luckily implementing this in Swift is also trivial. We just need to define another struct containing the fields we need in our custom data type. Importantly, this also needs to conform to Codable. In fact, you can nest custom objects as deep as you like, so long as every struct conforms to Codable. Let’s see this in action:
幸运的是,在Swift中实现这一点也是微不足道的。 我们只需要定义另一个结构,其中包含我们自定义数据类型中所需的字段。 重要的是 ,这也需要符合Codable。 实际上,只要每个结构都符合Codable,您就可以嵌套任意深度的自定义对象。 让我们来看看实际情况:
As you can see, I’m using the custom Author
type in our Website
struct. You’ll notice Author
doesn’t need to conform to Identifiable
, as we are only keeping track of one single author, and author isn’t a Firestore document — it’s simply a field within our website.
如您所见,我在Website
结构中使用自定义Author
类型。 您会注意到Author
不需要遵循Identifiable
,因为我们只跟踪一个作者,而且author不是Firestore文档,它只是我们网站中的一个字段。
You can also make arrays of custom objects in the same way as you’d make an array of strings.
您还可以按照与创建字符串数组相同的方式来创建自定义对象数组。
3.如果我的Firestore对象与Swift结构具有不同的字段名称怎么办? (3. What if my Firestore object has different field names compared to my Swift struct?)
This is a fairly common scenario and can come about particularly if it is challenging to update your Firestore database because it already contains data, is being consumed by other clients that you can’t update, or simply because you don’t have access to update Firestore.
这是一个相当普遍的情况,尤其是在由于要更新Firestore数据库而遇到挑战的情况下,尤其是因为该数据库已经包含数据,正被其他无法更新的客户端使用或者仅仅是因为您无权更新而导致的情况消防站。
In the first instance, I’d try to keep the field names the same, if only for your own sanity. If in your Firestore you refer to something as “author” but in your Swift code you refer to the same data as “person”, you can and will get confused.
在第一种情况下,如果只是出于您的理智考虑,我将尝试使字段名称保持相同。 如果在Firestore中您将某些内容称为“作者”,而在Swift代码中您将相同的数据称为“人员”,那么您将会并且会感到困惑。
But, if you’re unable to keep them in sync, fortunately the Codable protocol allows you to tell Swift that for a particular property, the key in the dictionary from Firebase will be called something else. This is done by defining an enum called CodingKeys
.
但是,如果您无法使它们保持同步,那么幸运的是,可编码协议允许您告诉Swift,对于特定属性,Firebase字典中的键将被称为其他键。 这是通过定义一个称为CodingKeys
的枚举来完成的。
For our website example, in Firestore we may have a field called websiteDescription
, but in Swift we want the property to be called description
as we know it is going to be describing the website.
对于我们的网站示例,在Firestore中,我们可能有一个名为websiteDescription
的字段,但是在Swift中,我们希望将该属性称为description
因为我们知道它将描述网站。
Here you can see I am defining the enum called CodingKeys
. It conforms to the protocol String
as the keys/field names in our Firestore are all strings. If for some reason our field names were numbers in Firestore, we’d conform to the Int
protocol. Importantly we also conform to the CodingKey
protocol, which lets Swift know that this enum is to be used within this Codable struct to define the names of the fields.
在这里,您可以看到我正在定义名为CodingKeys
的枚举。 它符合协议String
因为我们的Firestore中的键/字段名称都是字符串。 如果由于某种原因,我们的字段名称是Firestore中的数字,则表明我们符合Int
协议。 重要的是,我们还遵循CodingKey
协议,该协议使Swift知道该枚举将在此Codable结构中用于定义字段名称。
So, within the CodingKeys
enum, we need to create a case for every single field we are using. Unfortunately we can’t just define the case for the one field where the names don’t match — we need to define every single field.
因此,在CodingKeys
枚举中,我们需要为正在使用的每个字段创建一个案例。 不幸的是,我们不能只为名称不匹配的一个字段定义大小写,而是需要定义每个字段。
When we get to the one where it doesn’t match, all we need to do is give a string value to the case where the name doesn’t match. As you can see in the code example, we want the Swift property to be called description
, but in Firestore it is called websiteDescription
. So, I’ve set the string value of the case description
to be websiteDescription
, and that’s it! Done.
当我们找到一个不匹配的名称时,我们所要做的就是为名称不匹配的情况提供一个字符串值。 如代码示例所示,我们希望将Swift属性称为description
,但在Firestore中将其称为websiteDescription
。 因此,我将案例description
的字符串值设置为websiteDescription
,就是这样! 做完了
4.如果我的对象有枚举怎么办? (4. What if my object has enums?)
Ah, enums. This was the type I was dreading and the reason why I originally did my own mapping within the Firestore methods instead of relying on Codable. But, I actually found it wasn’t too complicated to get going.
嗯,枚举。 这就是我所恐惧的类型,也是我最初在Firestore方法中进行自己的映射而不是依赖于Codable的原因。 但是,我实际上发现开始并不太复杂。
Let’s go back to the website example. I want an enum called typeOfWebsite
, with the values blog
, app
and forum
. Firestore actually supports quite a number of different data types, but none of them are enums, so I’m going to have to store them as a string in Firestore.
让我们回到网站示例。 我想要一个名为typeOfWebsite
的枚举,其值包括blog
, app
和forum
。 Firestore实际上支持许多不同的数据类型 ,但是它们都不是枚举,因此我将不得不将它们作为字符串存储在Firestore中。
What I want to happen is to convert my string to an enum when I decode the Firestore object, so I can use the typesafe enum across my Swift app instead of having to rely on magic strings. But, when I encode the object to save back in Firestore, I need it to go back to a string so it can be saved.
我想要发生的是在解码Firestore对象时将字符串转换为枚举,因此我可以在Swift应用程序中使用类型安全枚举,而不必依赖魔术字符串。 但是,当我编码对象以将其保存回Firestore时,我需要将其返回到字符串以进行保存。
So naturally, like I said before, any types within your struct need to also be Codable. So, I need to conform my enum to Codable.
就像我之前说的那样,自然地,您结构中的任何类型也都必须是可编码的。 因此,我需要使我的枚举符合Codable。
If your enum uses either String
or Int
for its raw values, it can actually conform to Codable with no extra work. Here’s an example:
如果您的枚举将String
或Int
用作其原始值,则它实际上可以符合Codable,而无需进行额外的工作。 这是一个例子:
Because these examples use Int and String raw values under the hood, Swift is able to encode and decode these. For the Int example, Firestore would save the number 0 for blog, 1 for app and 2 for forum within the document. For the String example, it would actually save blog
for blog, app
for app and forum
for forum. For 90% of cases, this will work fine.
由于这些示例在后台使用了Int和String原始值,因此Swift能够对它们进行编码和解码。 对于Int示例,Firestore将在博客中保存数字0(博客),1(应用程序)和2(论坛)。 对于字符串例如,它实际上保存blog
的博客, app
的应用程序和forum
为论坛。 对于90%的情况,这可以正常工作。
However, I had a slightly more complicated example, where the enum values within Firestore didn’t exactly match to the raw values of my enum. So, what I needed to do was implement my own encoder and decoder.
但是,我有一个稍微复杂的示例,其中Firestore中的枚举值与我的枚举的原始值不完全匹配。 因此,我需要做的是实现自己的编码器和解码器。
Bear in mind, you can do this for absolutely any data type — it doesn’t necessarily need to be on an enum. If you need to do some kind of transformation on data that isn’t supported out of the box with the features I’ve already described, this is the way to do it.
请记住,您绝对可以对任何数据类型执行此操作-不一定需要使用枚举。 如果您需要使用我已经描述的功能对开箱即用的数据不进行某种转换,这就是这样做的方法。
So, my exact problem was a third party API was putting enum values into my Firestore which didn’t exactly match the names of my enums. Back to our website example, even though blog
is the enum value I have defined in Swift, this third party API was putting it in as Blog
. Even though that is only a one letter case change, this breaks the encoding and decoding, so we need to write our own methods.
因此,我的确切问题是第三方API将枚举值放入我的Firestore中,该值与我的枚举名称不完全匹配。 回到我们的网站示例,即使blog
是我在Swift中定义的枚举值,该第三方API仍将其作为Blog
放入。 即使只是一个字母的大小写更改,这也会破坏编码和解码,因此我们需要编写自己的方法。
There is quite a bit to unpack here.
这里有很多要解压的东西。
- First, I’ve created two methods within the enum itself to convert to and from our string representation. 首先,我在枚举本身中创建了两个方法来与我们的字符串表示形式进行相互转换。
I’ve created an extension to
WebsiteType
which conforms toCodable
. This isn’t necessary, and you could move all the code from within this extension into the enum — I just thought it provided a nice separation of the enum to the Codable implementation.我创建了一个扩展
WebsiteType
这符合Codable
。 这不是必需的,您可以将所有代码从此扩展名内移到枚举中,我只是认为它为Endable与Codable实现提供了很好的分离。As explained earlier, Codable is actually two protocols,
Encodable
andDecodable
. Each of them has a method for doing which you can override with your own, as I’ve done here. So…如前所述,Codable实际上是两个协议,
Encodable
和Decodable
。 每个人都有一个执行方法,您可以使用自己的方法来覆盖,就像我在这里所做的那样。 所以…On line 40 we have the init method. This creates the WebsiteType enum given a value. So, on lines 41 and 42 we get the value, as a string, and then on line 43 we take that string and pass it into a static function (because the enum hasn’t yet been initialised as that’s what we’re doing now), which gives us the enum corresponding to our string value. This is the decodable part.
在第40行,我们有init方法。 这将创建给定值的WebsiteType枚举。 因此,在第41和42行上,我们将值作为字符串获取,然后在第43行上 ,将该字符串并传递给静态函数(因为尚未初始化枚举,因为这就是我们现在正在做的事情) ),这将为我们提供与字符串值相对应的枚举。 这是可解码的部分。
On line 46 we have a function for converting the enum back to a string. On line 48, we are using the
self.description
method to get the string representation of the enum, and then we are encoding it. Notice on line 47 we are usingsingleValueContainer()
. This ensures that when we are encoding our enum it outputs as a single value, such asapp
.在第 46 行,我们有一个将枚举转换回字符串的函数。 在第48行 ,我们使用
self.description
方法获取枚举的字符串表示形式,然后对其进行编码。 注意,在第47行,我们正在使用singleValueContainer()
。 这样可以确保在对枚举进行编码时,它会将输出作为单个值输出,例如app
。A lot of other tutorials I read online had this as a
我在网上阅读的许多其他教程都将其作为
container(keyedBy: CodingKey.self)
, and then they encoded the value likecontainer.encode(self.description, keyBy: .rawValue)
. This worked, but outputted the enum within an object, like:{rawValue: "app"}
.container(keyedBy: CodingKey.self)
,然后他们将值编码为container.encode(self.description, keyBy: .rawValue)
。 这{rawValue: "app"}
,但将对象内的枚举输出,如:{rawValue: "app"}
。
That’s it!
而已!
5.如果我需要进行其他类型的数据转换怎么办? (5. What if I need to do some other kind of data transformation?)
Good question. Well, you actually have all the tools you need to go ahead and do this. In the same way that I created a custom encoder and decoder for the WebsiteType
enum, you can actually create your own encode and decode functions for any object conforming to Codable.
好问题。 好吧,您实际上拥有继续进行此操作所需的所有工具。 与我为WebsiteType
枚举创建自定义编码器和解码器的方式WebsiteType
,您实际上可以为符合Codable的任何对象创建自己的编码和解码功能。
Some examples of things you could do:
您可以做的一些事例:
- Get only the first value of an array 仅获取数组的第一个值
- Remove extra properties you don’t need (in either direction) 删除多余的属性(双向)
- Change the casing of string values 更改字符串值的大小写
- Convert Ints into Floats 将整数转换为浮点数
- Change date time zones 更改日期时区
Because you can write your own functions, the opportunities are limitless.
因为您可以编写自己的函数,所以机会是无限的。
Hopefully this article was useful in your Swift journey. Within our app, we are essentially using Firestore and Firebase Functions as our logic layer, and despite having both native Android and iOS apps, it really feels like a cross platform app because they both share the same data API.
希望本文对您的Swift旅程很有用。 在我们的应用程序中,我们实际上是使用Firestore和Firebase函数作为我们的逻辑层,尽管同时具有本机Android和iOS应用程序,但由于它们都共享相同的数据API,因此它确实感觉像是跨平台应用程序。
Reading and saving data is a breeze once you’ve conformed your data types to Codable, and I’d recommend not doing any manual mapping at all. Now, all you need to do is read in the object, update a value, and save it again. This could potentially take all of about 3 lines of code, something that would take much much more if you had to hit an API or go through an ORM. Codable can also be used for JSON decoding/encoding, so the lessons in this story are not limited to usage in Firestore.
将数据类型与Codable一致后,读取和保存数据变得轻而易举,我建议您完全不要进行任何手动映射。 现在,您需要做的就是读入对象,更新值,然后再次保存。 这可能需要大约3行代码,如果您必须使用API或通过ORM,则需要花费更多。 Codable也可以用于JSON解码/编码,因此本故事中的课程不仅限于Firestore中的用法。