Swift Realm. How to put 3 millions objects in database - ios

I make offline dictionary app. And now I convert dictionary file to realm database.
Convert function:
if let path = Bundle.main.path(forResource: "dictionary", ofType: "dsl") {
do {
let data = try String(contentsOfFile: path, encoding: .utf8)
let myStrings = data.components(separatedBy: .newlines)
for (index, row) in myStrings.enumerated() {
if(row.containsChineseCharacters)
{
let firstWord = CNDict()
firstWord.word = row
firstWord.pinyin = myStrings[index+1]
firstWord.translate = myStrings[index+2]
try! realm.write {
realm.add(firstWord)
}
}
}
print("The task end.")
} catch {
print(error)
}
}
When I try to convert the dictionary immediately, the database file becomes a lot of gigabytes and crashes around the middle
Splitting the dictionary in parts is not an option, because there are about 3 million lines. It will take very much... (realm plugin crashes)
I need help on how to add as many values to the database with our crashes.

The issue is that your file is large and at this point, you load it into the memory:
let data = try String(contentsOfFile: path, encoding: .utf8)
And then you double the memory footprint here:
let myStrings = data.components(separatedBy: .newlines)
So my guess is that you received out of memory signal from the system.
Instead of loading into the memory all the data and than double it, you can use lazy collection. It will read and parse the line only when it needed for writing. It will not load all lines at once. One downside of lazy collections in Swift is that they could not provide all the functions that we get used to.
Here the full code for playground that solves your issue. You could and maybe should optimize some parts of it, but anyway it just showing how it could be done with lazy collection. (I changed some names, but it's still what you want).
import Foundation
extension String {
var containsOneSymbol: Bool {
return contains("1")
}
}
extension Character {
var isNewLine: Bool {
let string = String(self)
let set = CharacterSet(charactersIn: string)
return !set.isDisjoint(with: CharacterSet.newlines)
}
}
/// Add Object subclass for Realm
#objcMembers
final class CNDict {
dynamic var word = ""
dynamic var pinyin = ""
dynamic var translate = ""
}
final class ModelWriterWrapper {
private let bufferCapacity = 3
private var buffer: [String] = []
init() {
buffer.reserveCapacity(bufferCapacity)
}
func proccess(line: String) {
guard buffer.count == bufferCapacity else {
assert(buffer.count < bufferCapacity, "Buffer failer count: \(buffer.count)!")
buffer.append(line)
return
}
if let firstLine = buffer.first, firstLine.containsOneSymbol {
let dict = CNDict()
dict.word = firstLine
dict.pinyin = buffer[1]
dict.translate = buffer[2]
print("Ready for writing into DB \n word: \(dict.word) pinyin: \(dict.pinyin) translate: \(dict.translate)")
}
buffer.removeFirst()
buffer.append(line)
}
}
let data = stride(from: 0, to: 100_000, by: 1).map { "Line number \($0)\n"}.joined()
var line: String = ""
let myLines = data.lazy.map { char -> String in
line.append(char)
guard !char.isNewLine else {
defer { line = "" }
return line
}
return ""
}.filter { !$0.isEmpty }
let databaseWritter = ModelWriterWrapper()
myLines.forEach {
databaseWritter.proccess(line: $0)
}
If have any questions regarding the code, please ask.

Related

Get data from function that returns Any

Hello I'm new to swift and this is giving me some problems.
I have a function from SDK that returns Any as a result, this is what the actual data looks like:
/*{
code = 1;
msg = success;
result = {
macAddress = "E6:1D:4D:71:64:9B";
};
}*/
I figured out a way to get the data I need, but it seems quite convoluted.
sucBlock: {
(mac) in
if let macAddress = ((mac as AnyObject)["result"] as AnyObject )["macAddress"] as! Optional<String>{
print("macAddress:" + macAddress)
}
}
Is there a better way to achieve this result? I tried to create a struct and type cast this object, but somehow it always failed.
You need to avoid using AnyObject and as! in such cases
if let macAddress = mac as? [String:Any] , let item = macAddress["result"] as? [String:String] , let str = item["macAddress"] {
print("str:" + str)
}
If you need a struct ( Don't think it really deserves for your simple json )
do {
let data = try JSONSerialization.data(withJSONObject:mac)
let res = try JSONDecoder().decode(Root.self, from:data)
print(res.result.macAddress)
}
catch {
print(error)
}
struct Root: Codable {
let code: Int
let msg: String
let result: Result
}
// MARK: - Result
struct Result: Codable {
let macAddress: String
}

How to store multiple back dates in a Swift Struct?

I would like to store the previous 4 days closing event in an individual struct so that i can make reference to them later on in the program. How would you go about storing the the closing event for each 4 days after sorting them from the JSON API.
The code below has sorted the previous 4 days but i am unable to figure how to store each day to use them separately
class DailyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "https://www.alphavantage.co/query?function=TIME_SERIES_DAILY&symbol=MSFT&apikey=demo"
let urlObj = URL(string: jsonUrlString)
URLSession.shared.dataTask(with: urlObj!) {(data, response, error) in
guard let data = data else { return }
do {
let forex = try JSONDecoder().decode(Root.self, from: data)
let sortedKeys = forex.timeSeriesDaily.keys.sorted(by: >)
let requestedKeys = sortedKeys.prefix(4)
var requestedPrices = [String:Forex]()
requestedKeys.forEach{ requestedPrices[$0] = forex.timeSeriesDaily[$0] }
print(requestedPrices)
print()
} catch {
print(error)
}
}.resume()
}
struct Root: Codable {
let metaData: [String: String]
let timeSeriesDaily: [String:Forex]
enum CodingKeys: String, CodingKey {
case timeSeriesDaily = "Time Series (Daily)"
case metaData = "Meta Data"
}
}
struct Forex: Codable {
let open, high, low, close: String
enum CodingKeys: String, CodingKey {
case open = "1. open"
case high = "2. high"
case low = "3. low"
case close = "4. close"
}
}
}
One way is to create a struct with four properties for this and add a specific init that takes an array
struct LastFour {
var close1: String
var close2: String
var close3: String
var close4: String
init?(_ closes: [String]) {
guard closes.count >= 4 else {
return nil
}
close1 = closes[0]
close2 = closes[1]
close3 = closes[2]
close4 = closes[3]
}
}
and then use map when initialising the struct from the dictionary
let lastFour = LastFour(requestedPrices.values.map {$0.close})
Note that the init is optional and returns nil in case the array is to short, another option could be to throw an error for instance.
Maybe a more flexible solution would be to use an array internally in the struct and then access the data via a method or perhaps computed properties
struct LastFour {
private var closeEvents: [String]
func close(at index: Int) -> String {
}
}
This would of course require similar code for init and checking the correct size but it would be easier to change if more or less elements are needed
My suggestion is to create another struct with the date and the close price
struct CloseData {
let date, price : String
}
and populate it
do {
let forex = try JSONDecoder().decode(Root.self, from: data)
let sortedKeys = forex.timeSeriesDaily.keys.sorted(by: >)
let requestedKeys = sortedKeys.prefix(4)
let requestedPrices = requestedKeys.map{ CloseData(date: $0, price: forex.timeSeriesDaily[$0]!.close) }
The result is an array of CloseData items

Persist data between app launches

I have a class to handle a simple note creator in my app. At the moment, notes are stored using an array of custom Note objects. How can I save the contents of this array when the app closes and load them again when the app is re-opened? I've tried NSUserDefaults, but I can't figure out how to save the array since it isn't just comprised of Strings.
Code:
Note.swift
class Note {
var contents: String
// an automatically generated note title, based on the first line of the note
var title: String {
// split into lines
let lines = contents.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
// return the first
return lines[0]
}
init(text: String) {
contents = text
}
}
var notes = [
Note(text: "Contents of note"),]
There are different approaches to this.
NSCoding
The easiest would be to adopt NSCoding, let Note inherit from NSObject and use NSKeyedArchiver and NSKeyedUnarchiver to write to/from files in the app's sandbox.
Here is a trivial example for this:
final class Feedback : NSObject, NSCoding {
private static let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let content : String
let entry : EntryId
let positive : Bool
let date : NSDate
init(content: String, entry: EntryId, positive : Bool, date :NSDate = NSDate()) {
self.content = content
self.entry = entry
self.positive = positive
self.date = date
super.init()
}
#objc init?(coder: NSCoder) {
if let c = coder.decodeObjectForKey("content") as? String,
let d = coder.decodeObjectForKey("date") as? NSDate {
let e = coder.decodeInt32ForKey("entry")
let p = coder.decodeBoolForKey("positive")
self.content = c
self.entry = e
self.positive = p
self.date = d
}
else {
content = ""
entry = -1
positive = false
date = NSDate()
}
super.init()
if self.entry == -1 {
return nil
}
}
#objc func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeBool(self.positive, forKey: "positive")
aCoder.encodeInt32(self.entry, forKey: "entry")
aCoder.encodeObject(content, forKey: "content")
aCoder.encodeObject(date, forKey: "date")
}
static func feedbackForEntry(entry: EntryId) -> Feedback? {
let path = Feedback.documentsPath.stringByAppendingString("/\(entry).feedbackData")
if let success = NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? Feedback {
return success
}
else {
return nil
}
}
func save() {
let path = Feedback.documentsPath.stringByAppendingString("/\(entry).feedbackData")
let s = NSKeyedArchiver.archiveRootObject(self, toFile: path)
if !s {
debugPrint("Warning: did not save a Feedback for \(self.entry): \"\(self.content)\"")
}
}
}
Core Data
The more efficient but more complex solution is using Core Data, Apple's ORM-Framework - which's usage is way beyond the scope of a SO answer.
Further Reading
NSHipster article
Archiving programming guide
Core Data programming guide

How to delete last path component of a String in Swift?

I have a String 11/Passion/01PassionAwakening.mp3 and I need to delete the last path component 01PassionAwakening.mp3 in order to get 11/Passion.
How can I do this while saving both components?
You can separate your url into two parts like so:
let str : NSString = "www.music.com/Passion/PassionAwakening.mp3"
let path : NSString = str.stringByDeletingLastPathComponent
let ext : NSString = str.lastPathComponent
print(path)
print(ext)
Output
www.music.com/Passion
PassionAwakening.mp3
For more info please have a look at this link.
You should really do away with legacy NS Objective-C classes and manual path string splitting where possible. Use URL instead:
let url = URL(fileURLWithPath: "a/b/c.dat", isDirectory: false)
let path = url.deletingLastPathComponent().relativePath // 'a/b'
let file = url.lastPathComponent // 'c.dat'
That being said, Apple has an explicit FilePath type starting with macOS 11, but with no path manipulation methods. For those you'd have to include the external system package
If you are on macOS 12, the methods from the external package are now also available on the system.
Swift 4+:
let components = path.split(separator: "/")
let directory = components.dropLast(1).map(String.init).joined(separator: "/")
Swift 3:
let str = "11/Passion/01PassionAwakening.mp3"
if !str.isEmpty {
let components = str.characters.split("/")
let head = components.dropLast(1).map(String.init).joinWithSeparator("/")
let tail = components.dropFirst(components.count-1).map(String.init)[0]
print("head:",head,"tail:", tail) // head: 11/Passion tail: 01PassionAwakening.mp3
} else {
print("path should not be an empty string!")
}
This works for Swift 3.0 as well:
let fileName = NSString(string: "11/Passion/01PassionAwakening.mp3").lastPathComponent
Swift 3.0 version
if !str.isEmpty {
let components = str.characters.split(separator: "/")
let head = components.dropLast(1).map(String.init).joined(separator: "/")
let words = components.count-1
let tail = components.dropFirst(words).map(String.init)[0]
print("head:",head,"tail:", tail) // head: 11/Passion tail: 01PassionAwakening.mp3
} else {
print("path should not be an empty string!")
}
rolling back to NSString:
extension String {
var ns: NSString {
return self as NSString
}
var pathExtension: String {
return ns.pathExtension
}
var lastPathComponent: String {
return ns.lastPathComponent
}
var stringByDeletingLastPathComponent: String {
return ns.deletingLastPathComponent
}
}
so you can do:
let folderPath = pathString.stringByDeletingLastPathComponent
Just improvised the solution for URL String. Thank you so much ingconti
extension String {
var ns: URL? {
return URL.init(string: self)
}
var pathExtension: String {
return ns?.pathExtension ?? ""
}
var lastPathComponent: String {
return ns?.lastPathComponent ?? ""
}
var stringByDeletingLastPathComponent: String {
return ns?.deletingLastPathComponent().absoluteString ?? ""
}
}

Parsing CSV file in Swift

I need to preload data into my tableView when the app launches. So i am using core data by parsing a .csv file. I am following this tutorial for this purpose.
Here is my parseCSV function
func parseCSV (contentsOfURL: NSURL, encoding: NSStringEncoding, error: NSErrorPointer) -> [(stationName:String, stationType:String, stationLineType: String, stationLatitude: String, stationLongitude: String)]? {
// Load the CSV file and parse it
let delimiter = ","
var stations:[(stationName:String, stationType:String, stationLineType: String, stationLatitude: String, stationLongitude: String)]?
let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error)
stations = []
let lines:[String] = content.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
for line in lines {
var values:[String] = []
if line != "" {
// For a line with double quotes
// we use NSScanner to perform the parsing
if line.rangeOfString("\"") != nil {
var textToScan:String = line
var value:NSString?
var textScanner:NSScanner = NSScanner(string: textToScan)
while textScanner.string != "" {
if (textScanner.string as NSString).substringToIndex(1) == "\"" {
textScanner.scanLocation += 1
textScanner.scanUpToString("\"", intoString: &value)
textScanner.scanLocation += 1
} else {
textScanner.scanUpToString(delimiter, intoString: &value)
}
// Store the value into the values array
values.append(value as! String)
// Retrieve the unscanned remainder of the string
if textScanner.scanLocation < textScanner.string.characters.count {
textToScan = (textScanner.string as NSString).substringFromIndex(textScanner.scanLocation + 1)
} else {
textToScan = ""
}
textScanner = NSScanner(string: textToScan)
}
// For a line without double quotes, we can simply separate the string
// by using the delimiter (e.g. comma)
} else {
values = line.componentsSeparatedByString(delimiter)
}
// Put the values into the tuple and add it to the items array
let station = (stationName: values[0], stationType: values[1], stationLineType: values[2], stationLatitude: values[3], stationLongitude: values[4])
stations?.append(station)
}
}
return stations
}
this is my sample .csv file
Rithala,Underground,Yellow Line,28.7209,77.1070
But i am getting an error on this line
let station = (stationName: values[0], stationType: values[1], stationLineType: values[2], stationLatitude: values[3], stationLongitude: values[4])
stations?.append(station)
Fatal error : Array index out of range
What am i doing wrong ? Please help me.
Here's a foolproof way of parsing a CSV file into your swift code (I'm using Swift 5 here). I'll talk through each step for any beginners in the room.
Let's assume your CSV file looks like this:
Firstname,Last name,Age,Registered
Duncan,Campbell,40,True
Tobi,Dorner,36,False
Saskia,Boogarts,29,True
1). We need a struct (or Object) to hold each row of data. Let's use this:
struct Person {
var firstName: String
var lastName: String
var age: Int
var isRegistered: Bool
}
2) We also need a variable which holds an array of each Person.
var people = [Person]()
3) Now - add the CSV file to your XCode project (you can drag and drop this into your project). Make sure it has a sensible name (e.g. data.csv).
4) You now need to "locate" the data that you want to use. Create a filepath which tells the code where to find your csv file:
guard let filepath = Bundle.main.path(forResource: "data", ofType: "csv") else {
return
}
5) Now we want to read the contents of this file. First let's convert the whole file into one long String.
var data = ""
do {
data = try String(contentsOfFile: filepath)
} catch {
print(error)
return
}
6) We now have a string with all out data in one line. We want to split this up into an array of strings, one string for each row in the data. (BTW, the \n means "new line")
let rows = data.components(separatedBy: "\n")
7) We now have an array with 4 rows - one for the header titles, and 3 rows for each person in the data. We're not interested in the first header row, so we can remove that one. (Ignore this step if you don't have a header row in your data)
rows.removeFirst()
8) Now loop around each of rows. Each row is currently a string (e.g. Duncan,Campbell,40,True) but we want to split it into an array of each of its 4 columns.
for row in rows {
let columns = row.components(separatedBy: ",")
9) We now have a array columns which has 4 strings in it. Let's convert each column to the correct data type.
let firstName = columns[0]
let lastName = columns[1]
let age = Int(columns[2]) ?? 0
let isRegistered = columns[3] == "True"
10) And now we can create the Person object, and append it to our array.
let person = Person(firstName: firstName, lastName: lastName, age: age, isRegistered: isRegistered)
people.append(person)
Here's the full code for anyone who is interested:
struct Person {
var firstName: String
var lastName: String
var age: Int
var isRegistered: Bool
}
var people = [Person]()
func convertCSVIntoArray() {
//locate the file you want to use
guard let filepath = Bundle.main.path(forResource: "data", ofType: "csv") else {
return
}
//convert that file into one long string
var data = ""
do {
data = try String(contentsOfFile: filepath)
} catch {
print(error)
return
}
//now split that string into an array of "rows" of data. Each row is a string.
var rows = data.components(separatedBy: "\n")
//if you have a header row, remove it here
rows.removeFirst()
//now loop around each row, and split it into each of its columns
for row in rows {
let columns = row.components(separatedBy: ",")
//check that we have enough columns
if columns.count == 4 {
let firstName = columns[0]
let lastName = columns[1]
let age = Int(columns[2]) ?? 0
let isRegistered = columns[3] == "True"
let person = Person(firstName: firstName, lastName: lastName, age: age, isRegistered: isRegistered)
people.append(person)
}
}
}
You are attempting to parse the file path rather than the contents of the file.
If you replace
let content = String(contentsOfURL: contentsOfURL, encoding: encoding, error: error)
with:
if let data = NSData(contentsOfURL: contentsOfURL) {
if let content = NSString(data: data, encoding: NSUTF8StringEncoding) {
//existing code
}
}
then the code will work for your example file.
Based on the error and where the error is taking place I would guess that your values array does not have 5 elements like you think it does. I would put a breakpoint on the line that is giving you an error and inspect your values variable and see how many are in it. Since your .csv file is obviously 5 elements long, then I would guess something is going wrong in your parsing.

Resources