Realm: Bundling a Realm with an App - swift

I want to ship my app with several databases.
1) I have a random generated ".csv" file converted with Realm Browser to "default.realm".
2) I put it in /project_name/project_name/Resources and drop it inside Xcode to project files.
3) I checked "Copy bundle resources"
4)Created
import RealmSwift
class CarItem: Object {
dynamic var id = String()
dynamic var first_name = String()
}
In ViewDidLoad wanted to return results from file
override func viewDidLoad() {
super.viewDidLoad()
let conf = Realm.Configuration(
fileURL: NSBundle.mainBundle().URLForResource("default", withExtension: "realm"),
readOnly: true)
let realm = try! Realm(configuration: conf)
let results = realm.objects(Item.self)
print(results)
}
5) But results are empty (database have 1000 rows all filled with data).... what am i down wrong?

CarItem.self replace Item.self ?
Open default.realm by Realm Browser(Your can download it from App Store).
Open .realm file by Realm Browser screenshot
Check Class Name and Class Members.
Example

Related

How do I create system integration tests using Firebase and Swift/Xcode?

I want to create tests for user account creation, so I can confirm my app is uploading the correct data, but I'm struggling to find the right way to do it. So far I've been unsuccessful in writing a test that works. This error prevents me from getting any further:
caught error: "An error occurred when accessing the keychain. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo dictionary will contain more information about the error encountered"
I've not written tests in Xcode before, and I've had lots of trouble finding guides on this sort of issue. Here is my test code:
import XCTest
import Firebase
import FirebaseFirestoreSwift
import SwiftUI
#testable import Baseline
class AccountTests: XCTestCase {
var baselineApp: BaselineApp!
override func setUpWithError() throws {
baselineApp = BaselineApp()
}
override func tearDownWithError() throws {
async {
let idString = baselineApp.authenticationState.loggedInUser?.uid
if let idString = idString {
try await baselineApp.authenticationState.deleteUser(id: idString)
}
baselineApp.authenticationState.signout()
}
}
func testAccountCreation() async throws {
let firstName = "Test"
let lastName = "Xcode"
let email = "testdummy5684+XCtest#gmail.com"
let password = "testingForXcode1234"
let gender = UserDocument.Gender.male
let birthdate = Timestamp(date: Date())
let suggestedRank = 0
let image = UIImage(named: "social")?.jpegData(compressionQuality: 1.0)
try await baselineApp.authenticationState.signupNewUser(email: email, password: password, firstName: firstName, lastName: lastName, gender: gender, jpegImageData: image, suggestedRank: suggestedRank, birthdate: birthdate)
let foundUserResult = baselineApp.authenticationState.loggedInUser
let foundUserDocumentResult = baselineApp.authenticationState.loggedInUserDocument
let foundUser = try XCTUnwrap(foundUserResult)
let foundUserDocument = try XCTUnwrap(foundUserDocumentResult)
XCTAssertNotNil(foundUserDocument, "Failed to get user document")
XCTAssertEqual(firstName, foundUserDocument.firstName, "Tried \(firstName), but got \(foundUserDocument.firstName)")
}
}
I'm creating an instance of my app where my #main is, because I have observable objects instantiated there, alongside Firebase's config settings. I have keychain sharing enabled, and my app works otherwise (like logging into a pre-existing account).
I'm running Xcode 13.0 beta (13A5155e) and targeting iOS 15.

Link app object to file on disk with metadata

Following this topic : iOS PDFkit cannot add custom attribute to pdf document
My app is using PDFKit to save files
I'm trying to set custom key metadata to PDFDocument I save on the device.
The object in my app ('Test') has two important properties :
id: a UUID to be able to retrieve the file on disk (the linked file on disk URL is this_UUID.jpg).
name: a human-readable string set by the user.
This cause some problems :
the file name is a UUID not human readable, so it's bad user experience.
If the user renames the file, the app won't be able to get the file.
So the id is to have a human-readable label for the file. So when the user opens the File app he can find it easily. And add metadata with the id so my app can retrieve it even if renamed. Looks like a nice solution right?
// First I create my own attribute
fileprivate extension PDFDocumentAttribute {
static let testId = PDFDocumentAttribute(rawValue: "com.sc.testID")
}
// Then I set the news attributes before saving the file, in the 'test' class
func setDocument(pdf: PDFDocument) {
let fileURL = self.getPDFDocumentURL()
print("the metadata is \(pdf.documentAttributes)") // print an empty dictionary
pdf.documentAttributes?[PDFDocumentAttribute.testId] = self.id
pdf.documentAttributes?[PDFDocumentAttribute.titleAttribute] = self.name // I suppose the ddisplay name of the document ? It's not, so what is that ?
print("the metadata is now \(pdf.documentAttributes)") // both are printed, it looks ok
//pdf.write(to: fileURL) // I tested this one too, same issues
let data = pdf.dataRepresentation()
do {
try data?.write(to: fileURL, options: .completeFileProtection)
} catch {
print(error.localizedDescription)
}
}
From here it looks ok, when I want to retrieve the pdf document I will check in the folder the id of each doc and return the doc when id match. But the problem is when I get the documentAttributes the attribute 'testId' isn't in. Note the native title, is set correctly.
So I could get the id from there but that looks pretty inappropriate
//still in 'Test' class
func getPDFDocument() -> PDFDocument? {
// get all docs in the folder ad check metadata for each
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
do {
let fileURLs = try fileManager.contentsOfDirectory(at: SchoolTest.getSubjectFolderURL(subject: self.subject!), includingPropertiesForKeys: nil)
for url in fileURLs {
print("the doc attributes are : \(PDFDocument(url: url)?.documentAttributes)") // contain title and others preset by Apple but not my custom 'testId'
if let doc = PDFDocument(url: url), doc.documentAttributes?[PDFDocumentAttribute.titleAttribute/*testId*/] as? String == self.documentName {
return doc // this work temporary
}
}
} catch {
print("Error while enumerating files \(documentsURL.path): \(error.localizedDescription)")
}
return nil
}
Display name:
Currently, the display name/label displayed in the File app is the file name (from URL).
This can cause problems too because if two 'Test' have the same name, their linked file gonna have the same URL. So when the most recent one will be saved on disk it will overwrite the other.
That's a problem I don't have when using the 'Test' id property for the file URL.
If I could set a display name for the file and keep the URL with the UUID that should resolve the problem.
Directories have the same localizing issue, I have named them with English but Apple native directories are localized. A localized name could be nice for user experience.
After hours of research, I can't find a way to localize or apply a display name to my files/directories.
// I tried that without positive result
var url = fileURL as NSURL
try url.setResourceValue("localized label", forKey: .localizedLabelKey)
print("localized name is \(try url.resourceValues(forKeys: [.localizedLabelKey]))")
let newURL = url as URL
try data?.write(to: newURL, options: .completeFileProtection)
Am I doing something badly? How should we do when adding custom metada to a file?

Realm Swift Bundle Data

I converted a csv file to a realm file and I want to use it in my app.
This is my code atm:
func inLibrarayFolder(fileName: String) -> URL {
return URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0], isDirectory: true)
.appendingPathComponent(fileName)
}
func copyPreBundleDataCompeletely() {
let mainRealmUrl = inLibrarayFolder(fileName: "main.realm")
let bundleUrl = Bundle.main.url(forResource: "treesFull", withExtension: "realm")!
//After launch after fresh install (if main.realm never created)
if (!FileManager.default.fileExists(atPath: mainRealmUrl.path)){
//copy bundled data into writable location compeletely
try! FileManager.default.copyItem(
at: bundleUrl, to: mainRealmUrl)
print(mainRealmUrl)
}
}
During the first launch, it creates the new file, but the file is a bit different from the original:
original db
copied db
the Tree object:
class Tree: Object {
#objc dynamic var id: Int32 = 0
#objc dynamic var br = ""
#objc dynamic var nm1 = ""
#objc dynamic var nm2 = ""
#objc dynamic var nm3 = ""
#objc dynamic var longitude = 0.0
#objc dynamic var latitude = 0.0
// override static func primaryKey() -> String? {
// return "id"
// }
}
It looks like I have 2 databeses in the new file, how can I access the second one with the data or how can I copy the file properly?
Also, whats gonna happen when I make the id to a primary key? Obviously I dont have a parameter like that in the original downloaded file, so I guess I will need to migrate the data somehow...
When it comes to importing, the file being imported has to be in a very specific format along with a specific file name
Your Realm object name is Tree, so the imported file name needs to match
Tree.csv
along with that the first line of the file needs to match the classes property names, comma separated
id,br,nm1...
I would suggest creating a very small test file to import with 3-4 lines to get it working. Then, once you mastered that then import the big file.

How to sort the filemanager array by creation date

I have written code that retrieves CSV paths in the documents directory and loads them into a tableview. I am trying to sort the files by creation date so that they list from newest to oldest in the tableview. Anyone got any advice on how to accomplish this?
I've not tried anything yet, because I am a little stuck
override func viewDidLoad() {
super.viewDidLoad()
csvFiles = listCsvs()
tblViewDataCSV.dataSource = self
tblViewDataCSV.delegate = self
}
func listCsvs() -> [URL] {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let files = try? fileManager.contentsOfDirectory(
at: documentDirectory,
includingPropertiesForKeys: nil,
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]
).filter {
$0.lastPathComponent.hasSuffix(".csv")
}
print(files as Any)
return files ?? []
}
I need the array sorted by .creationdate and not alphanumerically. Many thanks for your help.
You need to declare a struct that has a URL (or filename String) and a Date. Populate an array of this struct from the files (and their creation dates) you query from FileManager.
Use that array of struct as your data model for the table view. You can sort the array on filename or date (or any other attributes you might add in the future).
You can get the creation date of each file by first adding [.creationDateKey] to the includingPropertiesForKeys parameter of contentsOfDirectory. Then access the creation date using resourceValues on each URL. See How can I get the file creation date using URL resourceValues method in Swift 3? for more details on getting the creation date.
It may help to use the enumerator method of FileManager instead of contentsOfDirectory. This will make it easier to get the needs URL attributes and populate the array of struct.
You can build a struct like the below:
struct yourStruct {
var path:URL
var filedate:Date
}
For each file in folder, append your struct into array.
let s = yourStruct(
path: csvUrl,
filedate: csvFileDate)
myArray.append(s)
At this point, you have an array with your files and filedates.
And finally sort the array with:
let newArr = myArray.sorted { $0.filedate < $1.filedate }

Update whole class in Realm Swift in 1 time wont work

Imagine this code:
class StoredVersions: Object{
#objc dynamic var minimumAppVersion = 0.0
#objc dynamic var sets = 0.0
}
class LoadViewController: UIViewController {
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
let db = Firestore.firestore()
var newestVersions = StoredVersions()
if let resultsStoredVersion = self.realm.objects(StoredVersions.self).first{
print("found stored versions: \(resultsStoredVersion)")
self.storedVersions = resultsStoredVersion
}else{
try! self.realm.write {
print("no stored versions")
self.realm.add(self.storedVersions)
}
}
db.collection("data").document("version").getDocument(completion: { (data, someError) in
if let versions = data.flatMap({StoredVersions(value: $0.data()) }) {
try! self.realm.write {
self.storedVersions = versions
}
}
})
}
storedVersions is updated but when I restart the application, storedVersions is back to its initial state. I do see the print "found stored versions".
If I write just 1 variable at a time, it works. That looks like this:
try! self.realm.write {
self.storedVersions.sets = versions.sets
}
How can I update a whole class without having to put in variables one at a time?
When you do this:
if let versions = data.flatMap({StoredVersions(value: $0.data()) }) {
try! self.realm.write {
self.storedVersions = versions
}
}
You're creating a new, unmanaged StoredVersions object. You need to call realm.add(_:) to add it to the Realm, otherwise the object only exists in memory.
If you want to update the existing StoredVersions object rather than creating a new one, you should instead use Realm.add(_:update:), specifying true for the update argument. Note that this requires your type have a primary key property declared so that Realm knows which existing object to update.