How to store and fetch images in SQLite and in what format the images get saved? It would be more helpful if explained with an example.
Image itself cannot be stored into a database columns but you can first convert it into a string and then store it. The string is called base64 string. As far as I know, any image can be converted to that and reversely.
To encode to base 64:
let image : UIImage = UIImage(named:"imageNameHere")!
let imageData:NSData = UIImagePNGRepresentation(image)!
let strBase64 = imageData.base64EncodedString(options: .lineLength64Characters)
Now your UIImage object is converted to a String! Save strBase64 to SQLite DB. Remember to use text as column type because this string is very long.
To decode back to UIImage:
let dataDecoded:NSData = NSData(base64EncodedString: strBase64, options: NSDataBase64DecodingOptions(rawValue: 0))!
let decodedimage:UIImage = UIImage(data: dataDecoded)!
Alternative
save your image into document directory.
save your image file path or name of image in sqlite
Get the image path or name from sqlite and access that path from document directory.
Take Ref : Iphone : How to Display Document Directory images in Image View?
You can also store your image directly as a BLOB, however it depends on which framework you use for SQLite access. In case you use SQLite.swift, then there is an option:
Set up a file SQLiteHelper.swift like that:
class SQLiteHelper{
var db: Connection!
let personsTable = Table("person")
let id = Expression<Int>("id")
let firstName = Expression<String>("firstName")
let lastName = Expression<String>("lastName")
let profileImage = Expression<Data>("profileImage")
let date = Expression<Date>("savedAt")
init() {
do{
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let dbTemp = try Connection("\(path)/myDb.sqlite3") //Create db if not existing
self.db = dbTemp
}
catch {
print("Error info: \(error)")
}
}
public func insertData(firstNameVal: String,
lastNameVal: String,
profileImageVal: Data,
dateVal: Date){
do{
//Create a new table only if it does not exist yet
try db.run(personsTable.create(ifNotExists: true) { t in // CREATE TABLE "person" (
t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL,
t.column(firstName) // "firstName" TEXT,
t.column(lastName) // "lastName" TEXT,
t.column(profileImage) // "profileImage" BLOB,
t.column(date) // "savedAt" DATETIME)
})
}
catch {
print("The new SQLite3 Table could not be added: \(error)")
}
do{
try db.run(personsTable.insert(firstName <- firstNameVal,
lastName <- lastNameVal,
profileImage <- profileImageVal,
date <- dateVal
))
}
catch {
print("Could not insert row: \(error)")
}
}
public func getData() -> [Person]{
var persons = [Person]()
do{
for row in try db.prepare(personsTable) {
let person: Person = Person(firstName: row[firstName],
lastName: row[lastName],
profileImage: row[profileImage],
savedAt: row[date])
persons.append(person)
}
}
catch {
print("Could not get row: \(error)")
}
return persons
}
Now create a file Person.swift and put the following struct inside of it:
import Foundation
struct Person: Identifiable {
var id = UUID()
var firstName: String
var lastName: String
var profileImage: Data
var savedAt: Date
}
Store Data
In order to store data as a .png BLOB you would now basically do something like that:
var databaseHelper: SQLiteHelper = SQLiteHelper.init()
self.databaseHelper.insertData(firstNameVal: "yourFirstName",
lastNameVal: "yourLastName",
profileImageVal: yourImageView.pngData(),
dateVal: Date())
Retreive Data
If you want to display the image later in another Imageview you would have to do this:
var persons = self.databaseHelper.getData()
let profileImage = UIImage(data: persons[0].profileImage)
let myImageView = UIImageView(image: profileImage)
Saving UIImage as BLOB
I have saved the image as a .png because I want to use my database outside of iOS and therefore want to ensure compatibility. If you want you can also store your UIImage directly. You would roughly need to change it like that:
let profileImage = Expression<UIImage>("profileImage")
...
profileImageVal: yourImageView,
...
let myImageView = persons[0].profileImage
...
import Foundation
import UIKit
struct Person: Identifiable {
var id = UUID()
var firstName: String
var lastName: String
var profileImage: UIImage
var savedAt: Date
}
Note: SQLite.swift also supports lazy loading, which would probably make more sense in ascenario like that...
Related
I am trying to archive data and want to store it in userdefault but app getting crash.
Also tried this
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: selectedPoductDetails, requiringSecureCoding: false)
selectedPoductDetails is dict of type [String: SelectedProductDetail]
import Foundation
class SelectedProductDetail {
let product: String
var amount: Double
var title: String
init(product: String, amount: Double, title: String ) {
self.product = product
self.amount = amount
self.title = title
}
}
May i know why its not working and possible solution for same?
For this case you can use UserDefaults
struct ProductDetail: Codable {
//...
}
let encoder = JSONEncoder()
let selectedProductDetails = ProductDetail()
// Set
if let data = try? encoder.encode(selectedProductDetails) {
UserDefaults.standard.set(data, forKey: "selectedProductDetails")
}
// Get
if let selectedProductDetailsData = UserDefaults.standard.object(forKey: "selectedProductDetails") as? Data {
let selectedProductDetails = try? JSONDecoder().decode(ProductDetail.self, from: selectedProductDetailsData)
}
As mentioned in the comments to use NSKeyedArchiver the class must adopt NSSecureCoding and implement the two required methods.
The types in your class are JSON compatible, so adopt Codable and archive the data with JSONEncoder (or PropertyListEncoder). You could even use a struct and delete the init method
struct SelectedProductDetail: Codable {
let product: String
var amount: Double
var title: String
}
var productDetails = [String: SelectedProductDetail]()
// populate the dictionary
do {
let data = try JSONEncoder().encode(productDetails)
UserDefaults.standard.set(data, forKey: "productDetails")
} catch {
print(error)
}
And load it
do {
guard let data = UserDefaults.standard.data(forKey: "productDetails") else { return }
productDetails = try JSONDecoder().decode([String: SelectedProductDetail].self, from: data)
} catch {
print(error)
}
Note:
UserDefaults is the wrong place for user data. It's better to save the data in the Documents folder
I have JSON that looks like this, which returns a list of Posts:
[
{
"id" : 1,
"message": "Hello"
"urls" : {
"png" : "https://example.com/image.png",
"jpg" : "https://example.com/image.jpg",
"gif" : "https://example.com/image.gif"
}
}
]
As you can see, I need to make two classes. One for the parent object (Post), and one for the object "urls" (PostUrls).
I've done that like so:
class Post: Object, Decodable {
#objc dynamic var id = 0
#objc dynamic var message: String? = nil
#objc dynamic var urls: PostUrls? = nil
override static func primaryKey() -> String? {
return "id"
}
private enum PostCodingKeys: String, CodingKey {
case id
case message
case urls
}
convenience init(id: Int, message: String, urls: PostUrls) {
self.init()
self.id = id
self.message = message
self.urls = urls
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostCodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let message = try container.decode(String.self, forKey: .message)
let urls = try container.decode(PostUrls.self, forKey: .urls)
self.init(id: id, message: message, urls: urls)
}
required init() {
super.init()
}
}
And
#objcMembers class PostUrls: Object, Decodable {
dynamic var png: String? = nil
dynamic var jpg: String? = nil
dynamic var gif: String? = nil
private enum PostUrlsCodingKeys: String, CodingKey {
case png
case jpg
case gif
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostUrlsCodingKeys.self)
png = try container.decodeIfPresent(String.self, forKey: .png)
jpg = try container.decodeIfPresent(String.self, forKey: .jpg)
gif = try container.decodeIfPresent(String.self, forKey: .gif)
super.init()
}
required init() {
super.init()
}
}
But, the problem is that I have no relationship between Post and PostUrls, since there is no primary key to connect the two. Further, this also means that I currently won't be able to control duplicates inside the PostUrls table.
So my question is: how can I create a relationship between the two tables, and prevent duplicates in the PostUrls table?
In this case, you do have a relationship between those objects. Object Post contains object PostUrls. Realm does not require a primary key to have this kind of relationship, because it creates a primary key behind the scenes. So it uses it, even though you can't access it.
To manually set a primaryKey you have to override a func called primaryKey()
#objcMembers class DBFilterModel: Object {
// MARK: Properties
dynamic var id: Int = 0
override public static func primaryKey() -> String? {
return "id"
}
}
This way you tell to realm that you want this property to be used as a Unique Key.
To prevent duplicating them, there are 2 ways. First - try to find an object with that id already existing in your database, if it exists - don't create it.
Second - by adding conflicts handlers to Realm's save methods. You can set that objects with same ID's will be just modified, but not duplicated. Or you could say that you want to throw an error when you try to insert a duplicated object.
realm.add(objects, update: update ? .modified : .error)
The question has two questions within
How do you create a relationship between the two 'tables'
prevent duplicates
Let me address 1)
Start with a class to hold the image type (.jpg etc) and then the url
class ImageUrlClass: Object {
#objc dynamic var imageType = ""
#objc dynamic var imageUrl = ""
}
and then the main class which handles decoding
class Post: Object, Decodable {
#objc dynamic var id: Int = 0
#objc dynamic var message: String = ""
let urlList = List<ImageUrlClass>()
...edited for brevity
convenience init(id: Int, message: String, urls: [String: String]) {
self.init()
self.id = id
self.message = message
//create a ImageUrlClass from each dictionary entry
for url in urls {
let key = url.key
let value = url.value
let aUrl = ImageUrlClass(value: ["imageType": key, "imageUrl": value])
self.urlList.append(aUrl)
}
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PostCodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let message = try container.decode(String.self, forKey: .message)
let urls = try container.decode([String: String].self, forKey: .urls)
self.init(id: id, message: message, urls: urls)
}
}
The above will create a Post object with a List property that contains the image types and urls (a List behaves very similar to an array)
You could further this by adding a LinkingObjects property to the ImageUrlClass which would automagically create an inverse relationship to the Post object when objects are added to the List. Not sure if you need that but it's available.
You can this do this to print out the post properties
let decoder = JSONDecoder()
let post = try! decoder.decode(Post.self, from: encodedData)
print(post.id)
print(post.message)
for url in post.urlList {
let a = url.imageType
let b = url.imageUrl
print(a,b)
}
which would results in an output like this
1
Hello
jpg https://example.com/image.jpg
png https://example.com/image.png
gif https://example.com/image.gif
I've created two arrays (imgUrl and imgTitle). I want to save these array values in Core Data. I tried like below. However, it is not successful.
//Mark:- Method to save data in local data base(CoreData)
func saveDataInLocal(imageUrl: [String], imageTitle: [String]){
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let contactEntity = NSEntityDescription.entity(forEntityName: "Photos", in: context)
let newContact = NSManagedObject(entity: contactEntity!, insertInto: context)
for eachValue in imageTitle{
newContact.setValue(eachValue, forKey: "imgTitle")
}
for eachValue in imageUrl{
newContact.setValue(eachValue, forKey: "imgUrl")
}
do {
try context.save()
fetchData()
} catch {
print("Failed saving")
}
}
XcmodelID is shown in image.
In these two arrays one is image title and another one image URL.
Fetching I'm doing like below.
//Mark:- Method to fetch data from local database(CoreData)
func fetchData(){
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Photos")
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
imgTitleNew.append(data.value(forKey: "imgTitle") as! String)
imgUrlNew.append(data.value(forKey: "imgUrl") as! String)
}
} catch {
print("Failed")
}
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
Can somebody suggest how to save the array in Core Data?
Array data displayed below.
var imgUrl = [String]() //contains urls in array
var imgTitle = [String]() //contains titles in array
A simple solution is to save both arrays joined with tab (or other unique) characters and use computed properties for the conversion
Assuming the Core Data properties are declared as
#NSManaged public var imageURL: String
#NSManaged public var imageTitle: String
Add these two computed properties
var imageURLArray : [String] {
get { return imageURL.components(separatedBy: "\t") }
set { imageURL = newValue.joined(separator: "\t") }
}
var imageTitleArray : [String] {
get { return imageTitle.components(separatedBy: "\t") }
set { imageTitle = newValue.joined(separator: "\t") }
}
I currently have a custom object
public struct GenreData : Decodable {
public let id : NSNumber?
public let name : String
public init?(json: JSON) {
guard let name : String = "name" <~~ json
else {return nil}
self.id = "id" <~~ json
self.name = name
}
}
I have an array of the custom object and am trying to access the 'id' part of the object so I can plug it in another function :
var genreDataArray: [GenreData] = []
var posterStringArray: [String] = []
var posterImageArray: [UIImage] = []
GenreData.updateAllData(urlExtension:"list", completionHandler: { results in
guard let results = results else {
print("There was an error retrieving info")
return
}
self.genreDataArray = results
for _ in self.genreDataArray {
if let movieGenreID = self.genreDataArray[0].id {//This is where the ID is needed to access the posters
print(movieGenreID)
//Update posters based on genreID
GenrePosters.updateGenrePoster(genreID: movieGenreID, urlExtension: "movies", completionHandler: {posters in
//Must iterate through multiple arrays with many containing the same poster strings
for poster in posters {
//Check to see if array already has the current poster string, if it does continue, if not append to array
if self.posterStringArray.contains(poster){
continue
} else {
self.posterStringArray.append(poster)
//Use the poster string to download the corresponding poster
self.networkManager.downloadImage(imageExtension: "\(poster)",
{ (imageData) //imageData = Image data downloaded from web
in
if let image = UIImage(data: imageData as Data){
self.posterImageArray.append(image)
}
})
}
}
})
}
}
DispatchQueue.main.async {
self.genresTableView.reloadData()
}
})
As of right now all its doing is accessing the first array of the JSON data(there are 19 arrays)and appending every string from that first array to the posterStringArray but instead I need it to go through each array and only append the string if doesn't already exist in the posterStringArray.
I am not 100 percent sure I understood your question, but shouldn't you replace:
for _ in self.genreDataArray {
if let movieGenreID = self.genreDataArray[0].id
with
for item in self.genreDataArray {
if let movieGenreID = item.id
update
to only add one item try adding a break after getting the image:
//Use the poster string to download the corresponding poster
self.networkManager.downloadImage(imageExtension: "\(poster)",
{ (imageData) //imageData = Image data downloaded from web
in
if let image = UIImage(data: imageData as Data){
self.posterImageArray.append(image)
}
})
break
I am new to Firebase and I want to store the provider photo URL however it come out the error 'Can only store objects of type NSNumber, NSString, NSDictionary, and NSArray.' I have tried different type of the method but it seems not working for example let profilePicUrl = profile.photoURL as String or let profilePicUrl = NSString(NSURL: profile.photoURL)
It is my method
func createFirebaseUser(){
let key = ref.child("user").childByAutoId().key
if let user = FIRAuth.auth()?.currentUser{
for profile in user.providerData{
let uid = profile.uid
let providerID = profile.providerID
let displayName = profile.displayName
let email = ""
let profilePicUrl = profile.photoURL
//let userDict :[String : AnyObject] = ["name": username!"profilePicUrl": profilePicUrl!]
let profile = Profile(uid: uid, displayName: displayName!,email: email, imageUrl: profilePicUrl)
let childUpdates = ["/user/\(key)": profile]
ref.updateChildValues(childUpdates, withCompletionBlock: { (error, ref) -> Void in
// now users exist in the database
print("user stored in firebase")
})
}
}
and it is the data model
import Foundation
class Profile {
private var _uid: String
private var _displayName: String
private var _email: String?
private var _gender: String!
private var _city: String!
private var _imageUrl: String!
var uid: String {
return _uid
}
var displayName: String {
get {
return _displayName
}
set {
_displayName = newValue
}
}
var email: String {
get {
return _email!
}
set {
_email = newValue
}
}
var gender: String {
get {
return _gender
}
set {
_gender = newValue
}
}
var city: String {
get {
return _city
}
set {
_city = newValue
}
}
var imageUrl: String {
get {
return _imageUrl
}
set {
_imageUrl = newValue
}
}
init(uid: String, displayName: String, email: String, imageUrl: String) {
_uid = uid
_displayName = displayName
_email = email
_imageUrl = imageUrl
}
You can only store the 4 types of NSObjects you mentioned in Firebase. But for the most part, data is just a string and storing strings is easy.
Assuming that your photoURL is an actual NSURL, you can save it as a string
let ref = myRootRef.childByAppendingPath("photo_urls")
let thisFileRef = ref.childByAutoId()
thisFileRef.setValue(photoURL.absoluteString)
and to go from a string to an NSURL
let url = NSURL(string: urlString)!
Also, it appears you a creating a new user in Firebase. You can greatly simplify your code like this
let usersRef = self.myRootRef.childByAppendingPath("users")
let thisUserRef = usersRef.childByAutoId()
var dict = [String: String]()
dict["displayName"] = "Bill"
dict["email"] = "bill#thing.com"
dict["gender"] = "male"
dict["photo_url"] = photoURL.absoluteString
thisUserRef.setValue(dict)
I would suggest making that code part of your User class so you can
let aUser = User()
aUser.initWithStuff(displayName, email, gender etc etc
aUser.createUser()
That really reduces the amount of code and keeps it clean.
If you are storing users, you should use the auth.uid as the key to each users node. This is more challenging in v3.x than it was in 2.x but hopefully Firebase will soon have a fix.
What you should really do is to store a relative path to your data into Firebase database and the prefix of the absolute URL separately (maybe in Firebase in some other node or somewhere else). This will help you to be flexible and being able to switch to a different storage without a lot of worries. Moreover, it should solve your current problem, because you will be storing raw strings in the Firebase and then in the app, you will merge prefix and the relative path together in order to produce the complete URL.
For example, let's assume that your URL to a photo looks like that:
http://storage.example.com/photos/photo1.jpg
Then, you can decompose this URL into:
prefix = http://storage.example.com/
relativeUrl = photos/photo1.jpg
And store the prefix for example in some settings node in the Firebase database and the relativeUrl in your photos' data.
Then in order to construct the complete URL you want to concatenate them together.