My head is going to explode :) - I've been trying to get a JSON String from my server to a Dictionary Value, and I can't get it to work.
I'm trying to get (from my Server - this is dynamic and I want my app to be able to pull new data from the server when needed):
{"1":"Location 1","2":"Location 2","3":"Location 3"}
To this Dictionary in Xcode using Swift:
var labels = [
1 : "Location 1",
2 : "Location 2",
3 : "Location 3"
]
This has got to be pretty straight forward, but for the life of me I can't figure it out...
Here's my Swift - I can get it to pull the information from the server, but I can't get it into a dictionary like I need
var postEndpoint: String = "http://www.myserver.net/app/campus.php"
Alamofire.request(.GET, postEndpoint)
.responseJSON { (request, response, data, error) in
if let anError = error
{
println("error")
println(error)
}
else if let data: AnyObject = data
{
let post = JSON(data)
println(post)
}
}
which results in:
{
"1" : "Location 1",
"2" : "Location 2",
"3" : "Location 3"
}
The End Result that I'm using this for is an iBeacon implementation with the following code:
let knownBeacons = beacons.filter{ $0.proximity != CLProximity.Unknown }
if (knownBeacons.count > 0) {
let closestBeacon = knownBeacons[0] as CLBeacon
let locationID = post[closestBeacon.minor.integerValue]
self.locationLabel.text = locationID
self.view.backgroundColor = self.colors[closestBeacon.minor.integerValue]
}
The error I'm getting is at self.locationLabel.text = locationID 'JSON' is not convertible to 'String', I do not get this error when I use the static var labels dictionary. Am I trying to get the data from the server incorrectly? What am I doing wrong??? I think the var labels having an undeclared Type allows Swift to figure out what it needs to, how do I do the same from the JSON part?
Oh you were so close!
Your problem is that your post dictionary is a [String: String] and not an [Int: String] like you think it is. You have a few ways to fix it, but the easiest for now would be to just do the following:
let locationID = post[String(closestBeacon.minor.integerValue)]!
While this will certainly work, a better solution would be to convert your post into a [Int: String] typed dictionary like you expect in the responseJSON closure. Here's how this could work.
let json = JSON(data)
var post = [Int: String]()
for (key, object) in json {
post[key.toInt()!] = object.stringValue
}
You would want to add some safety around what to do if the key or object were not able to be converted to an Int or String respectively, but I'll leave that to you.
If having a [String: String] is sufficient for anyone, he/she could try the following code:
let responseString = "{\"1\":\"Location 1\",\"2\":\"Location 2\",\"3\":\"Location 3\"}"
if let dataFromString = responseString.data(using: String.Encoding.utf8, allowLossyConversion: false) {
let json = JSON(data: dataFromString)
var labels = json.dictionaryObject! as! [String: String]
print(labels)
}
The result is:
["2": "Location 2", "1": "Location 1", "3": "Location 3"].
I solved this by working in reverse. I changed the call. Instead of getting the Dictionary of Values from the Server, I just query the Server with the Single Variable that I already had from the variable
closestBeacon.minor.integerValue
And then get the string that I needed from the server and that solved my problem. Still has the same number of calls to the server, so no additional overhead was added. Sometimes you just have to think outside the box that you put yourself into.
If anybody can solve this the other direction, I'm still eager to hear how it could work.
You need to check recursively for inner JSON as well
Use below function to convert into Dictionary or Array
static func toDictionary(from: JSON) -> Any? {
if from.type == .array {
var array = [Any]()
for _i in 0..<from.count {
array.append(toDictionary(from: from[_i]) as Any)
}
return array
} else if from.type == .dictionary {
var dictionary = [String: Any]()
for (key, value) in from {
dictionary[key] = toDictionary(from: value)
}
return dictionary
} else {
return from.stringValue
}
}
Related
Im very new with CoreData fetching/display and so far able to save into CoreData from a JSON fetch.
The fetched data is an array of Airport info with only three items; airport_code, access_point and image_url.
I need to add two more values to each fetched item - a lat and lon coordinate which is stored in another CoreData entity with a matching airport_code item/attribute.
Can anyone provide some guidance as to how to create a separate function to query this other CoreData during the loop sequence by using the predicate value of the airport_code? I have attached the code I have so far:
func saveData(context: NSManagedObjectContext){
xArray.forEach { (data) in
let entity = Airports(context: context)
entity.airport_code = data.airport_code
entity.access_points = data.access_points
entity.image_url = data.image_url
entity.lat = getLat()
entity.lon = getLon()
}
do{
try context.save()
print("Success Saving to CoreData: \(xArray.count)")
}
catch{
print("Error Saving to CoreData \(error.localizedDescription)")
}
}
func getLat() -> String {
#FetchRequest(entity: AllAirports.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \AllAirports.airport_code, ascending: true)])
var results: FetchedResults<AllAirports>
//this is where Im lost as to how to query this CoreData to fetch the LON value when there is a match to the data.airport_code in the loop above.
return latResults
}
func getLon() -> String {
#FetchRequest(entity: AllAirports.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \AllAirports.airport_code, ascending: true)])
var results: FetchedResults<AllAirports>
//this is where Im lost as to how to query this CoreData to fetch the LON value when there is a match to the data.airport_code in the loop above.
return lonResults
}
func fetchData(context: NSManagedObjectContext) {
// this function performs a JSON parse and returns the xArray above
….
First you want to take a look at the response here What is the best way to do a fetch request in CoreData? to get an idea how a programmatic fetch request may look like.
As I understand your problem now, you want to write two methods getLat() and getLon() which are going to fetch the coordinates for an airport.
I would recommend to write a single function as shown below which looks up your airport in AllAirports using the given code and returns the tuple with the coordinates found.
func getCoord(airport_code: String) -> (lon: Double, lat: Double)? {
let request: NSFetchRequest<AllAirports> = AllAirports.fetchRequest()
request.predicate = NSPredicate(format: "airport_code == %#", airport_code)
if let result = try? viewContext.fetch(request) {
print("Found \(result.count) airports matching \(airport_code)")
// Just return the first matched airport
if let first = result.first {
return (first.longitude, first.latitude)
}
}
return nil
}
A better solution would be to add an extension to you AllAirports which returns an entry for a given (hopefully unique!) airport_code:
extension AllAirports {
static func airport(byCode airportCode: String, in context: NSManagedObjectContext) -> AllAirports? {
let request: NSFetchRequest<AllAirports> = fetchRequest()
request.predicate = NSPredicate(format: "airport_code == %#", airportCode)
if let result = try? context.fetch(request) {
return result.first
}
return nil
}
}
This could then be used in your code as follows:
func saveData(context: NSManagedObjectContext){
xArray.forEach { (data) in
let entity = Airports(context: context)
entity.airport_code = data.airport_code
entity.access_points = data.access_points
entity.image_url = data.image_url
if let airport = AllAirports.airport(byCode: data.airport_code, in: context) {
entity.lat = airport.latitude
entity.lon = airport.longitude
}
}
The goal here is to retrieve all the documents of a Firestore Database, then putting the data of those retrieved documents into a Table View. I'm using Xcode and Firebase.
For example, if I had a Firestore database with this data and documents:
Document 1
Name: Bob
Email: bob#gmail.com
Phone: 408-111-1234
Document 2
Name: Joe
Email: joe#yahoo.com
Phone: 408-338-4321
I would like to have it all in a table view, maybe something like this:
Bob - bob#gmail.com - 408-111-1234
Joe - joe#yahoo.com - 408-338-4321
So far, I have this set up:
TableViewController:
class PeopleViewController: UITableViewController {
#IBOutlet var table: UITableView!
var peopleArray = [] as [Array<Any>]
private var document: [DocumentSnapshot] = []
override func viewDidLoad() {
super.viewDidLoad()
table.tableFooterView = UIView(frame: CGRect.zero)
self.table.delegate = self
self.table.dataSource = self
loadData()
}
And the loadData() func:
func loadData() {
FirebaseFirestore.root.collection("users").getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let email = data["email"] as? String ?? ""
let phone = data["phone"] as? String ?? ""
let newPerson = ["name": name, "email": email, "phone": phone]
self.peopleArray.append(newPerson)
print(self.peopleArray)
}
self.table.reloadData()
}
}
}
}
The first thing wrong here is that there is this error:
Cannot convert value of type '[String : String]' to expected argument type '[Any]'
When I try to append a newPerson in the PeopleArray. I have searched this up, but none of the answers matched this specific error. The second thing is that I have no clue how to even start adding the data from the PeopleArray into the Table View.
I would love some help for both the error and the Table View. Anything is appreciated!
––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
After doing what Vadian recommended, I am stuck with a new error.
attempt to insert row 2 into section 0, but there are only 1 rows in section 0 after the update
This is the updated func loadData:
func loadData() {
FirebaseFirestore.root.collection("users").getDocuments { (snapshot, error) in
if let error = error {
print(error.localizedDescription)
} else {
if let snapshot = snapshot {
for document in snapshot.documents {
self.table.beginUpdates()
let data = document.data()
let name = data["name"] as? String ?? ""
let zip = data["zip"] as? String ?? ""
let sport = data["sport"] as? String ?? ""
let newPerson = Person(name: name, zip: zip, sport: sport)
self.people.append(newPerson)
let newIndexPath = IndexPath(row: self.people.count, section: 0)
self.table.insertRows(at: [newIndexPath], with: .automatic)
self.table.endUpdates()
}
DispatchQueue.main.async {
self.table.reloadData()
}
}
}
}
}
I've read about this on another question about how the rows are off by 1, but even when I let indexPath equal to people.count+1 or people.count+2, the error persists. What am I doing wrong?
First of all the syntax
var peopleArray = [] as [Array<Any>]
is bad practice. If you want to declare an empty array write
var peopleArray : [Array<Any>] = []
or
var peopleArray = [Array<Any>]()
Second of all your array is a nested array which is not intended. You mean
var peopleArray = Array<Any>()
which is more descriptive using the alternative syntax
var peopleArray = [Any]()
Third of all as your data is clearly a more specific type than [Any] declare the array
var peopleArray = [[String:String]]()
This fixes the error.
Fourth of all the recommended data source is a custom struct
struct Person {
let name, email, phone: String
}
In this case declare the array (we know that people is plural so the suffix array is redundant).
var people = [Person]()
And populate it (you might reload the table view on the main thread)
for document in snapshot.documents {
let data = document.data()
let name = data["name"] as? String ?? ""
let email = data["email"] as? String ?? ""
let phone = data["phone"] as? String ?? ""
let newPerson = Person(name: name, email: email, phone: phone)
self.people.append(newPerson)
}
DispatchQueue.main.async {
self.table.reloadData()
}
I researched a lot and fixed the edited part in the question I posted. A simple -1 in the insert rows fixed the problem.
let newIndexPath = IndexPath(row: self.people.count-1, section: 0)
Vadian also helped me with the original problem, which was using
var peopleArray = [[String:String]]()
instead of the other incorrect format. I was then able to find how to insert rows by just simply using the table.insertrows function.
(I'm just posting this for a simplified answer for anyone else experiencing problems).
I already read all the answers in stack overflow for similar problems, and tried the suggestions for them, but cannot solve this error (Initializer for conditional binding must have Optional type, not '[String:Double]'), which occurs on line "if let jsonDictionary = json {". Please help! Thanks in advance!
func getPrice(cprCcy: String, ccy: String){
if let url = URL(string: "https://min-api.cryptocompare.com/data/price?fsym=" + cprCcy + "&tsyms=" + ccy){
URLSession.shared.dataTask(with: url) {(data, response, error) in
if let data = data {
print ("connected to the url")
if let json = try? JSONSerialization.jsonObject(with: data, options:[]) as? [String:Double]{
if let jsonDictionary = json {
if let price = jsonDictionary[ccy] {
print(price)
}
}
}
}
else{
print("wrong =(")
}
}.resume()
}
}
In the end, I found a simple solution:
replaced this part :
if let jsonDictionary = json {
if let price = jsonDictionary[ccy] {
print(price)
}
}
by this:
if let price = json[ccy] {
print(price)
}
that is, just eliminated the line that was causing me problems and moved the variable to the next line.
In Swift 2 the following code was working:
let request = NSFetchRequest(entityName: String)
but in Swift 3 it gives error:
Generic parameter "ResultType" could not be inferred
because NSFetchRequest is now a generic type. In their documents they wrote this:
let request: NSFetchRequest<Animal> = Animal.fetchRequest
so if my result class is for example Level how should I request correctly?
Because this not working:
let request: NSFetchRequest<Level> = Level.fetchRequest
let request: NSFetchRequest<NSFetchRequestResult> = Level.fetchRequest()
or
let request: NSFetchRequest<Level> = Level.fetchRequest()
depending which version you want.
You have to specify the generic type because otherwise the method call is ambiguous.
The first version is defined for NSManagedObject, the second version is generated automatically for every object using an extension, e.g:
extension Level {
#nonobjc class func fetchRequest() -> NSFetchRequest<Level> {
return NSFetchRequest<Level>(entityName: "Level");
}
#NSManaged var timeStamp: NSDate?
}
The whole point is to remove the usage of String constants.
I think i got it working by doing this:
let request:NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Level")
at least it saves and loads data from DataBase.
But it feels like it is not a proper solution, but it works for now.
The simplest structure I found that works in 3.0 is as follows:
let request = NSFetchRequest<Country>(entityName: "Country")
where the data entity Type is Country.
When trying to create a Core Data BatchDeleteRequest, however, I found that this definition does not work and it seems that you'll need to go with the form:
let request: NSFetchRequest<NSFetchRequestResult> = Country.fetchRequest()
even though the ManagedObject and FetchRequestResult formats are supposed to be equivalent.
Here are some generic CoreData methods that might answer your question:
import Foundation
import Cocoa
func addRecord<T: NSManagedObject>(_ type : T.Type) -> T
{
let entityName = T.description()
let context = app.managedObjectContext
let entity = NSEntityDescription.entity(forEntityName: entityName, in: context)
let record = T(entity: entity!, insertInto: context)
return record
}
func recordsInTable<T: NSManagedObject>(_ type : T.Type) -> Int
{
let recs = allRecords(T.self)
return recs.count
}
func allRecords<T: NSManagedObject>(_ type : T.Type, sort: NSSortDescriptor? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func query<T: NSManagedObject>(_ type : T.Type, search: NSPredicate?, sort: NSSortDescriptor? = nil, multiSort: [NSSortDescriptor]? = nil) -> [T]
{
let context = app.managedObjectContext
let request = T.fetchRequest()
if let predicate = search
{
request.predicate = predicate
}
if let sortDescriptors = multiSort
{
request.sortDescriptors = sortDescriptors
}
else if let sortDescriptor = sort
{
request.sortDescriptors = [sortDescriptor]
}
do
{
let results = try context.fetch(request)
return results as! [T]
}
catch
{
print("Error with request: \(error)")
return []
}
}
func deleteRecord(_ object: NSManagedObject)
{
let context = app.managedObjectContext
context.delete(object)
}
func deleteRecords<T: NSManagedObject>(_ type : T.Type, search: NSPredicate? = nil)
{
let context = app.managedObjectContext
let results = query(T.self, search: search)
for record in results
{
context.delete(record)
}
}
func saveDatabase()
{
let context = app.managedObjectContext
do
{
try context.save()
}
catch
{
print("Error saving database: \(error)")
}
}
Assuming that there is a NSManagedObject setup for Contact like this:
class Contact: NSManagedObject
{
#NSManaged var contactNo: Int
#NSManaged var contactName: String
}
These methods can be used in the following way:
let name = "John Appleseed"
let newContact = addRecord(Contact.self)
newContact.contactNo = 1
newContact.contactName = name
let contacts = query(Contact.self, search: NSPredicate(format: "contactName == %#", name))
for contact in contacts
{
print ("Contact name = \(contact.contactName), no = \(contact.contactNo)")
}
deleteRecords(Contact.self, search: NSPredicate(format: "contactName == %#", name))
recs = recordsInTable(Contact.self)
print ("Contacts table has \(recs) records")
saveDatabase()
This is the simplest way to migrate to Swift 3.0, just add <Country>
(tested and worked)
let request = NSFetchRequest<Country>(entityName: "Country")
Swift 3.0 This should work.
let request: NSFetchRequest<NSFetchRequestResult> = NSManagedObject.fetchRequest()
request.entity = entityDescription(context)
request.predicate = predicate
I also had "ResultType" could not be inferred errors. They cleared once I rebuilt the data model setting each entity's Codegen to "Class Definition". I did a brief writeup with step by step instructions here:
Looking for a clear tutorial on the revised NSPersistentContainer in Xcode 8 with Swift 3
By "rebuilt" I mean that I created a new model file with new entries and attributes. A little tedious, but it worked!
What worked best for me so far was:
let request = Level.fetchRequest() as! NSFetchRequest<Level>
I had the same issue and I solved it with the following steps:
Select your xcdatamodeld file and go to the Data Model Inspector
Select your first Entity and go to Section class
Make sure that Codegen "Class Definition" is selected.
Remove all your generated Entity files. You don't need them anymore.
After doing that I had to remove/rewrite all occurences of fetchRequest as XCode seem to somehow mix up with the codegenerated version.
HTH
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func loadItemsCategory() {
let request: NSFetchRequest<Category> = Category.fetchRequest()
do {
categoryArray = try context.fetch(request)
} catch {
print(error)
}
tableView.reloadData()
}
I'm trying to get the contact details out of the address book on the Mac. I can get the first name and last name fields etc, but I'm struggling with the syntax for ABPersonCopyImageData.
Now according to the documentation ABPersonCopyImageData takes a single parameter of type ABPerson.
Here is my code:
import AddressBook
let thisPerson : ABPerson
let addressBook = ABAddressBook.sharedAddressBook()
rec = addressBook.recordForUniqueId("0005A360-327F-4E12-BBB9-24A842497E12:ABPerson")
let firstName = rec.valueForProperty(kABFirstNameProperty) as! String
let lastName = rec.valueForProperty(kABLastNameProperty) as! String
println("\(firstName) \(lastName)")
let contactImage = ABPersonCopyImageData(thisPerson)
The last line stops the compiler with an error: Cannot invoke 'ABPersonCopyImageData' with an argument list of type (ABPerson). As far as I can tell thisPerson is of type ABPerson. What is going wrong?
I found out how to do this in ElCapitan:
import Contacts
func getContactImage(name:String) -> NSImage?
{
let store = CNContactStore()
do
{
let contacts = try store.unifiedContactsMatchingPredicate(CNContact.predicateForContactsMatchingName(name), keysToFetch:[CNContactImageDataKey])
if contacts.count > 0
{
if let image = contacts[0].imageData
{
return NSImage.init(data: image)
}
}
}
catch
{
}
return nil
}