版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2018.09.26 星期三 |
前言
數(shù)據(jù)是移動(dòng)端的重點(diǎn)關(guān)注對(duì)象,其中有一條就是數(shù)據(jù)存儲(chǔ)。CoreData是蘋果出的數(shù)據(jù)存儲(chǔ)和持久化技術(shù)筝闹,面向?qū)ο筮M(jìn)行數(shù)據(jù)相關(guān)存儲(chǔ)。感興趣的可以看下面幾篇文章划提。
1. iOS CoreData(一)
2. iOS CoreData實(shí)現(xiàn)數(shù)據(jù)存儲(chǔ)(二)
3. Core Data詳細(xì)解析(三) —— 一個(gè)簡(jiǎn)單的入門示例(一)
4. Core Data詳細(xì)解析(四) —— 一個(gè)簡(jiǎn)單的入門示例(二)
開始
首先看一下寫作環(huán)境
Swift 4.2, iOS 12, Xcode 10
managed object context
是用于處理managed objects
的內(nèi)存中暫存器。大多數(shù)應(yīng)用只需要一個(gè)托managed object context
邢享。大多數(shù)Core Data應(yīng)用程序中的默認(rèn)配置是與主隊(duì)列關(guān)聯(lián)的單個(gè)managed object context
鹏往。多個(gè)managed object context
使您的應(yīng)用程序更難調(diào)試;在任何情況下骇塘,它都不是你在每個(gè)應(yīng)用程序中使用的東西伊履。
話雖如此,某些情況確實(shí)需要使用多個(gè)managed object context
款违。例如唐瀑,長(zhǎng)時(shí)間運(yùn)行的任務(wù)(例如導(dǎo)出數(shù)據(jù)(exporting data)
)將阻塞僅使用單個(gè)主隊(duì)列managed object context
的App的主線程并導(dǎo)致UI遲鈍。
在其他情況下插爹,例如在對(duì)用戶數(shù)據(jù)進(jìn)行編輯時(shí)介褥,將managed object context
視為一組更改是有好處的,如果應(yīng)用程序不再需要它們递惋,則可以將其丟棄柔滔。使用子上下文使這成為可能。
在本教程中萍虽,您將通過(guò)為沖浪者提供日記應(yīng)用程序并通過(guò)添加多個(gè)上下文以多種方式改進(jìn)它來(lái)了解多個(gè)managed object context
睛廊。
本教程的入門項(xiàng)目是一個(gè)簡(jiǎn)單的日記應(yīng)用程序,適合沖浪者杉编。 在每次沖浪活動(dòng)之后超全,沖浪者可以使用該應(yīng)用程序創(chuàng)建一個(gè)記錄海洋參數(shù)的新日記帳分錄,例如膨脹高度或周期邓馒,并將活動(dòng)評(píng)分為1到5嘶朱。
Introducing SurfJournal - 引入SurfJournal
新建立SurfJournal
入門項(xiàng)目。 打開項(xiàng)目光酣,然后構(gòu)建并運(yùn)行應(yīng)用程序疏遏。
啟動(dòng)時(shí),應(yīng)用程序會(huì)列出所有以前的沖浪會(huì)話日記帳分錄救军。 點(diǎn)擊一行可以顯示具有編輯功能的沖浪會(huì)話的詳細(xì)視圖财异。
如您所見,示例應(yīng)用程序可以運(yùn)行并具有數(shù)據(jù)唱遭。 點(diǎn)擊左上角的Export
按鈕可將數(shù)據(jù)導(dǎo)出為逗號(hào)分隔值(CSV)
文件戳寸。 點(diǎn)擊右上角的加號(hào)(+)按鈕可添加新的日記帳分錄。 點(diǎn)擊列表中的一行將以編輯模式打開條目拷泽,您可以在其中更改或查看沖浪會(huì)話的詳細(xì)信息疫鹊。
盡管示例項(xiàng)目看起來(lái)很簡(jiǎn)單袖瞻,但它實(shí)際上做了很多工作,并且可以作為添加多上下文支持的良好基礎(chǔ)拆吆。 首先虏辫,讓我們確保您對(duì)項(xiàng)目中的各個(gè)類有很好的理解。
打開項(xiàng)目導(dǎo)航器锈拨,查看項(xiàng)目中的完整文件列表:
在進(jìn)入代碼之前砌庄,請(qǐng)花一點(diǎn)時(shí)間來(lái)了解每個(gè)類的內(nèi)容。
-
AppDelegate:首次啟動(dòng)時(shí)奕枢,app委托創(chuàng)建
Core Data
堆棧并在主視圖控制器JournalListViewController
上設(shè)置coreDataStack
屬性娄昆。 -
CoreDataStack:此對(duì)象包含稱為
stack
的Core Data
對(duì)象的的主要部分。 這次堆棧會(huì)在首次啟動(dòng)時(shí)安裝已包含數(shù)據(jù)的數(shù)據(jù)庫(kù)缝彬。 暫時(shí)不用擔(dān)心萌焰,你會(huì)很快看到它是如何運(yùn)作的。 -
JournalListViewController:示例項(xiàng)目是一個(gè)基于表的單頁(yè)應(yīng)用程序谷浅。 該文件代表該表扒俯。 如果您對(duì)其UI元素感到好奇,請(qǐng)轉(zhuǎn)到
Main.storyboard
一疯。 有一個(gè)嵌入在導(dǎo)航控制器中的table view
控制器和一個(gè)SurfEntryTableViewCell
類型的單個(gè)原型單元撼玄。 -
JournalEntryViewController:此類處理創(chuàng)建和編輯沖浪日記條目。 您可以在
Main.storyboard
中查看其UI墩邀。 -
JournalEntry:此類表示沖浪日記條目掌猛。 它是一個(gè)
NSManagedObject
子類,具有六個(gè)屬性屬性:date, height, location, period, rating and wind
眉睹。 如果您對(duì)此類的實(shí)體定義感到好奇荔茬,請(qǐng)查看SurfJournalModel.xcdatamodel
。
-
JournalEntry + Helper:這是
JournalEntry
對(duì)象的擴(kuò)展竹海。 它包括CSV導(dǎo)出方法csv()
和stringForDate()
輔助方法慕蔚。 這些方法在擴(kuò)展中實(shí)現(xiàn),以避免在更改Core Data模型時(shí)被銷毀斋配。
首次啟動(dòng)應(yīng)用時(shí)孔飒,已經(jīng)有大量數(shù)據(jù)。 此示例項(xiàng)目附帶了seeded Core Data
數(shù)據(jù)庫(kù)许起。
The Core Data Stack - Core Data堆棧
打開CoreDataStack.swift
并在seedCoreDataContainerIfFirstLaunch():
中找到以下代碼:
// 1
let previouslyLaunched =
UserDefaults.standard.bool(forKey: "previouslyLaunched")
if !previouslyLaunched {
UserDefaults.standard.set(true, forKey: "previouslyLaunched")
// Default directory where the CoreDataStack will store its files
let directory = NSPersistentContainer.defaultDirectoryURL()
let url = directory.appendingPathComponent(
modelName + ".sqlite")
// 2: Copying the SQLite file
let seededDatabaseURL = Bundle.main.url(
forResource: modelName,
withExtension: "sqlite")!
_ = try? FileManager.default.removeItem(at: url)
do {
try FileManager.default.copyItem(at: seededDatabaseURL,
to: url)
} catch let nserror as NSError {
fatalError("Error: \(nserror.localizedDescription)")
}
如您所見十偶,本教程的CoreDataStack.swift
版本略有不同:
1) 首先檢查
UserDefaults
以獲取先前已啟動(dòng)的布爾值。 如果當(dāng)前執(zhí)行確實(shí)是應(yīng)用程序的首次啟動(dòng)园细,則Bool將為false,使if語(yǔ)句為true接校。 在首次啟動(dòng)時(shí)猛频,您要做的第一件事就是將之前的啟動(dòng)設(shè)置為true狮崩,以便seeding
操作再也不會(huì)發(fā)生。2) 然后鹿寻,將應(yīng)用程序包中包含的
SQLite
種子seed
文件SurfJournalModel.sqlite
復(fù)制到Core Data提供的方法NSPersistentContainer.defaultDirectoryURL()
返回的目錄中睦柴。
現(xiàn)在查看seedCoreDataContainerIfFirstLaunch()
的其余部分:
// 3: Copying the SHM file
let seededSHMURL = Bundle.main.url(forResource: modelName,
withExtension: "sqlite-shm")!
let shmURL = directory.appendingPathComponent(
modelName + ".sqlite-shm")
_ = try? FileManager.default.removeItem(at: shmURL)
do {
try FileManager.default.copyItem(at: seededSHMURL,
to: shmURL)
} catch let nserror as NSError {
fatalError("Error: \(nserror.localizedDescription)")
}
// 4: Copying the WAL file
let seededWALURL = Bundle.main.url(forResource: modelName,
withExtension: "sqlite-wal")!
let walURL = directory.appendingPathComponent(
modelName + ".sqlite-wal")
_ = try? FileManager.default.removeItem(at: walURL)
do {
try FileManager.default.copyItem(at: seededWALURL,
to: walURL)
} catch let nserror as NSError {
fatalError("Error: \(nserror.localizedDescription)")
}
print("Seeded Core Data")
}
3) 一旦
SurfJournalModel.sqlite
的副本成功,您就可以復(fù)制支持文件SurfJournalModel.sqlite-shm
毡熏。4) 最后坦敌,復(fù)制剩余的支持文件
SurfJournalModel.sqlite-wal
。
SurfJournalModel.sqlite
痢法,SurfJournalModel.sqlite-shm
或SurfJournalModel.sqlite-wal
在首次啟動(dòng)時(shí)無(wú)法復(fù)制的唯一原因是狱窘,如果發(fā)生了一些非常糟糕的事情,例如宇宙輻射造成的磁盤損壞财搁。 在這種情況下蘸炸,設(shè)備(包括任何應(yīng)用程序)可能也會(huì)失敗。 如果文件無(wú)法復(fù)制尖奔,則繼續(xù)沒有意義搭儒,因此catch
塊會(huì)調(diào)用fatalError
。
開發(fā)人員經(jīng)常對(duì)使用
abort
和fatalError
感到不滿提茁,因?yàn)樗鼤?huì)導(dǎo)致應(yīng)用程序突然退出并且沒有解釋而使用戶感到困惑淹禾。 這是fatalError
可接受的一種情況,因?yàn)閼?yīng)用程序需要Core Data才能工作茴扁。 如果一個(gè)應(yīng)用程序需要Core Data稀拐,但是Core Data不起作用,那么讓應(yīng)用程序繼續(xù)運(yùn)行是沒有意義的丹弱,只會(huì)在以后的某個(gè)時(shí)間以非確定的方式失敗德撬。調(diào)用fatalError
至少會(huì)生成堆棧跟蹤,這在嘗試解決問題時(shí)很有用躲胳。 如果您的應(yīng)用程序支持遠(yuǎn)程日志記錄或崩潰報(bào)告蜓洪,則應(yīng)在調(diào)用fatalError
之前記錄可能對(duì)調(diào)試有幫助的任何相關(guān)信息。
為了支持并發(fā)讀取和寫入坯苹,此示例應(yīng)用程序中的持久性SQLite存儲(chǔ)使用SHM(共享內(nèi)存文件)和WAL(預(yù)寫日志記錄)文件隆檀。 您不需要知道這些額外文件的工作方式,但您確實(shí)需要知道它們的存在粹湃,并且需要在播種數(shù)據(jù)庫(kù)時(shí)復(fù)制它們恐仑。 如果您無(wú)法復(fù)制這些文件,該應(yīng)用程序?qū)⑵鹱饔梦赡軙?huì)丟失數(shù)據(jù)裳仆。
現(xiàn)在您已經(jīng)了解了從seeded
數(shù)據(jù)庫(kù)開始的事情,您將通過(guò)處理臨時(shí)私有上下文來(lái)了解多個(gè)managed object contexts
孤钦。
Doing Work in the Background - 在后臺(tái)工作
如果尚未執(zhí)行此操作歧斟,請(qǐng)點(diǎn)擊左上角的Export
按鈕纯丸,然后立即嘗試滾動(dòng)會(huì)話日記條目列表。 注意什么静袖? 導(dǎo)出操作需要幾秒鐘觉鼻,并且它會(huì)阻止UI響應(yīng)滾動(dòng)等觸摸事件。
在導(dǎo)出操作期間阻塞了UI队橙,因?yàn)閷?dǎo)出操作和UI都使用主隊(duì)列來(lái)執(zhí)行其工作坠陈。 這是默認(rèn)行為。
解決此問題的傳統(tǒng)方法是使用Grand Central Dispatch
在后臺(tái)隊(duì)列上運(yùn)行導(dǎo)出操作捐康。 但是仇矾,Core Data的managed object contexts
不是線程安全的。 這意味著您不能只調(diào)度到后臺(tái)隊(duì)列并使用相同的Core Data堆棧吹由。
解決方案很簡(jiǎn)單:使用私有后臺(tái)隊(duì)列而不是主隊(duì)列進(jìn)行導(dǎo)出操作若未。 這將使主隊(duì)列免費(fèi)供UI使用。
但在您進(jìn)入并解決問題之前倾鲫,您需要了解導(dǎo)出操作的工作原理粗合。
1. Exporting Data - 導(dǎo)出數(shù)據(jù)
首先查看應(yīng)用程序如何為JournalEntry
實(shí)體創(chuàng)建CSV
字符串。 打開JournalEntry + Helper.swift
并找到csv()
:
func csv() -> String {
let coalescedHeight = height ?? ""
let coalescedPeriod = period ?? ""
let coalescedWind = wind ?? ""
let coalescedLocation = location ?? ""
let coalescedRating: String
if let rating = rating?.int32Value {
coalescedRating = String(rating)
} else {
coalescedRating = ""
}
return "\(stringForDate()),\(coalescedHeight),\(coalescedPeriod),\(coalescedWind),\(coalescedLocation),\(coalescedRating)\n"
}
如您所見乌昔,JournalEntry
返回實(shí)體屬性的逗號(hào)分隔字符串隙疚。 因?yàn)樵试SJournalEntry屬性為nil,所以該函數(shù)使用nil coalescing
運(yùn)算符(??)來(lái)導(dǎo)出空字符串磕道,而不是屬性為nil
的無(wú)用調(diào)試消息供屉。
注意:nil合并運(yùn)算符
(??)
如果包含值,則進(jìn)行解包該值溺蕉,否則返回默認(rèn)值伶丐。 例如,以下內(nèi)容:let coalescedHeight = height疯特!= nil哗魂? height! :“”
可以使用nil合并運(yùn)算符縮短:let coalescedHeight = height ??“”
漓雅。
這就是應(yīng)用程序?yàn)閱蝹€(gè)日記帳分錄創(chuàng)建CSV
字符串的方式录别,但應(yīng)用程序如何將CSV文件保存到磁盤? 打開JournalListViewController.swift
并在exportCSVFile()
中找到以下代碼:
// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
// 2
let exportFilePath = NSTemporaryDirectory() + "export.csv"
let exportFileURL = URL(fileURLWithPath: exportFilePath)
FileManager.default.createFile(atPath: exportFilePath,
contents: Data(), attributes: nil)
下面就逐步看一下CSV
導(dǎo)出代碼:
- 1) 首先邻吞,通過(guò)執(zhí)行
fetch
請(qǐng)求來(lái)檢索所有JournalEntry
實(shí)體组题。
獲取請(qǐng)求與獲取的結(jié)果控制器使用的請(qǐng)求相同。 因此抱冷,您重復(fù)使用surfJournalFetchRequest
方法來(lái)創(chuàng)建請(qǐng)求以避免重復(fù)崔列。
- 2) 接下來(lái),通過(guò)將文件名
(“export.csv”)
附加到NSTemporaryDirectory
方法的輸出徘层,為導(dǎo)出的CSV文件創(chuàng)建URL峻呕。
NSTemporaryDirectory
返回的路徑是臨時(shí)文件存儲(chǔ)的唯一目錄利职。 這是一個(gè)很容易再次生成并且不需要由iTunes
或iCloud
備份的文件的好地方趣效。
創(chuàng)建導(dǎo)出URL后瘦癌,調(diào)用createFile(atPath:contents:attributes :)
創(chuàng)建一個(gè)空文件,您將在其中存儲(chǔ)導(dǎo)出的數(shù)據(jù)跷敬。 如果文件已存在于指定的文件路徑中讯私,則此方法將首先將其刪除。
一旦應(yīng)用程序具有空文件西傀,它就可以將CSV
數(shù)據(jù)寫入磁盤:
// 3
let fileHandle: FileHandle?
do {
fileHandle = try FileHandle(forWritingTo: exportFileURL)
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
fileHandle = nil
}
if let fileHandle = fileHandle {
// 4
for journalEntry in results {
fileHandle.seekToEndOfFile()
guard let csvData = journalEntry
.csv()
.data(using: .utf8, allowLossyConversion: false) else {
continue
}
fileHandle.write(csvData)
}
// 5
fileHandle.closeFile()
print("Export Path: \(exportFilePath)")
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
} else {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
以下是文件處理的工作原理:
3) 首先斤寇,應(yīng)用程序需要?jiǎng)?chuàng)建一個(gè)用于寫入的文件處理程序,它只是一個(gè)處理寫入數(shù)據(jù)所需的低級(jí)磁盤操作的對(duì)象拥褂。 要?jiǎng)?chuàng)建用于寫入的文件處理程序娘锁,請(qǐng)使用
FileHandle(forWritingTo :)
初始化程序。4) 接下來(lái)饺鹃,迭代所有
JournalEntry
實(shí)體莫秆。
在每次迭代期間,您嘗試使用JournalEntry
上的csv()
和String
上的data(using:allowLossyConversion:)
創(chuàng)建UTF8編碼的字符串悔详。
如果成功镊屎,則使用文件處理程序write()
方法將UTF8字符串寫入磁盤。
- 5) 最后茄螃,關(guān)閉導(dǎo)出文件寫入文件處理程序缝驳,因?yàn)椴辉傩枰?/li>
應(yīng)用程序?qū)⑺袛?shù)據(jù)寫入磁盤后,它會(huì)顯示一個(gè)包含導(dǎo)出文件路徑的警告對(duì)話框归苍。
注意:具有導(dǎo)出路徑的此alert控制器可用于學(xué)習(xí)目的用狱,但對(duì)于真實(shí)應(yīng)用程序,您需要為用戶提供檢索導(dǎo)出的CSV文件的方法拼弃,例如使用
UIActivityViewController
夏伊。
要打開導(dǎo)出的CSV
文件,請(qǐng)使用Excel
肴敛,Numbers
或您喜歡的文本編輯器導(dǎo)航到并打開alert
對(duì)話框中指定的文件署海。 如果您在Numbers
中打開文件,您將看到以下內(nèi)容:
現(xiàn)在您已經(jīng)了解了應(yīng)用程序當(dāng)前如何導(dǎo)出數(shù)據(jù)医男,現(xiàn)在是時(shí)候進(jìn)行一些改進(jìn)了砸狞。
2. Exporting in the Background - 在后臺(tái)導(dǎo)出
您希望UI在導(dǎo)出過(guò)程中繼續(xù)工作。 要修復(fù)UI問題镀梭,您將在私有后臺(tái)上下文而不是主上下文上執(zhí)行導(dǎo)出操作刀森。
打開JournalListViewController.swift
并在exportCSVFile()
中找到以下代碼:
// 1
let context = coreDataStack.mainContext
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
如前所述,此代碼通過(guò)在managed object context
中調(diào)用fetch()
來(lái)檢索所有日記條目报账。
接下來(lái)研底,使用以下代碼替換上面的代碼:
// 1
coreDataStack.storeContainer.performBackgroundTask { context in
var results: [JournalEntry] = []
do {
results = try context.fetch(self.surfJournalFetchRequest())
} catch let error as NSError {
print("ERROR: \(error.localizedDescription)")
}
您現(xiàn)在在堆棧的持久存儲(chǔ)容器上調(diào)用performBackgroundTask(_ :)
埠偿,而不是使用UI也使用的主managed object context
。 這將創(chuàng)建一個(gè)新的managed object context
并將其傳遞給閉包榜晦。
performBackgroundTask(_ :)
創(chuàng)建的上下文位于私有隊(duì)列上冠蒋,該隊(duì)列不會(huì)阻塞主UI隊(duì)列。 閉包中的代碼在該專用隊(duì)列上運(yùn)行乾胶。 您還可以手動(dòng)創(chuàng)建一個(gè)新的臨時(shí)私有上下文抖剿,其并發(fā)類型為.privateQueueConcurrencyType
,而不是使用performBackgroundTask(_ :)
识窿。
注意:
managed object context
可以使用兩種并發(fā)類型:
Private Queue指定將與專用調(diào)度隊(duì)列而不是主隊(duì)列關(guān)聯(lián)的上下文斩郎。 這是您剛剛用于將導(dǎo)出操作移出主隊(duì)列的隊(duì)列類型,因此它不再干擾UI喻频。
Main Queue是默認(rèn)類型缩宜,指定上下文將與主隊(duì)列關(guān)聯(lián)。 此類型是主上下文(coreDataStack.mainContext)
使用的類型甥温。 任何UI操作(例如為表視圖創(chuàng)建fetched
的結(jié)果控制器)都必須使用此類型的上下文锻煌。
只能從正確的隊(duì)列訪問上下文及其managed objects
。NSManagedObjectContext
執(zhí)行perform(_:)
和performAndWait(_ :)
以將工作定向到正確的隊(duì)列窿侈。 您可以將啟動(dòng)參數(shù)-com.apple.CoreData.ConcurrencyDebug 1
添加到應(yīng)用程序的scheme
中炼幔,以捕獲調(diào)試器中的錯(cuò)誤。
接下來(lái)史简,在同一方法中找到以下代碼:
print("Export Path: \(exportFilePath)")
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
} else {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
用以下代碼進(jìn)行替換:
print("Export Path: \(exportFilePath)")
// 6
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
self.showExportFinishedAlertView(exportFilePath)
}
} else {
DispatchQueue.main.async {
self.navigationItem.leftBarButtonItem =
self.exportBarButtonItem()
}
}
} // 7 Closing brace for performBackgroundTask
要完成任務(wù):
- 6) 您應(yīng)始終執(zhí)行與主隊(duì)列上的UI相關(guān)的所有操作乃秀,例如在導(dǎo)出操作完成時(shí)顯示警報(bào)視圖; 否則,可能發(fā)生不可預(yù)測(cè)的事情圆兵。 使用DispatchQueue.main.async在主隊(duì)列上顯示最終的警報(bào)視圖消息跺讯。
- 7) 最后,添加一個(gè)結(jié)束大括號(hào)殉农,通過(guò)
performBackgroundTask(_ :)
調(diào)用關(guān)閉您在步驟1中先前打開的塊刀脏。
現(xiàn)在您已將導(dǎo)出操作移動(dòng)到具有專用隊(duì)列的新上下文,構(gòu)建并運(yùn)行以查看它是否有效超凳!
您應(yīng)該看到之前看到的確切內(nèi)容:
點(diǎn)擊左上角的Export
按鈕愈污,立即嘗試滾動(dòng)瀏覽會(huì)話日記條目列表。 注意這次有什么不同嗎轮傍? 導(dǎo)出操作仍需要幾秒鐘才能完成暂雹,但table view
在此期間繼續(xù)滾動(dòng)。 導(dǎo)出操作不再阻塞UI创夜。
您剛剛目睹了如何在私有后臺(tái)隊(duì)列上工作可以改善用戶的應(yīng)用體驗(yàn)杭跪。 現(xiàn)在,您將通過(guò)檢查子上下文來(lái)擴(kuò)展多個(gè)上下文的使用。
后記
本篇主要講述了基于多上下文的Core Data簡(jiǎn)單解析示例涧尿,感興趣的給個(gè)贊或者關(guān)注~~~