XML only displays last item in array in Swift - arrays

Here is an example of the XML file I have. I have everything parsed, but can't get the array to display all items.
<dish>
<title> Pancakes </title>
<calories> 350 calories </calories>
<ingredients>
<item>ingredient 1</item>
<item>ingredient 2</item>
<item>ingredient 3</item>
</ingredients>
</dish>
I have a struct of Recipes and a struct of Ingredients. The ingredients only has a variable of "item" in it. I've removed some of the recipe code to include the ingredients portion of the parsing code, since everything else works and prints.
struct Ingredients {
var item = ""
}
var tableViewDataSource = [Recipes]()
var tableViewIngSource = [Ingredients]()
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
thisName = elementName
// elementName == "recipe" code
if elementName == "ingredients" {
var recipeItem = ""
}
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
let data = string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if data.count != 0 {
switch thisName
{
case "title": recipeTitle = data
case "calories": recipeCalories = data
case "ingredients": recipeIngredients = data
case "item": recipeItem = data
default:
break
}
}
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if elementName == "dish" {
var recipe = Recipes()
recipe.title = recipeTitle
recipe.duration = recipeDuration
recipe.calories = recipeCalories
recipe.ingredients = recipeIngredients
recipe.description = recipeDescription
//print(recipe)
tableViewDataSource.append(recipe)
}
if elementName == "ingredients" {
var ingredients = Ingredients()
ingredients.item = recipeItem
for item in tableViewIngSource {
print(item)
}
print(ingredients)
print(tableViewIngSource.count)
tableViewIngSource.append(ingredients)
}
}
This is the output. It results in the last two print statement outputs, the for loop does not print anything
Ingredients(item: "ingredient 3")
0
From what I've seen, a custom XML parser needs to be used. I've looked into SWXMLHash, but it seems the XML code needs to be in the swift file

Your code needs a good bit of reorganizing to properly parse the XML data.
The following is more like what you want to have:
Your Recipe structure (Note it is Recipe, not Recipes since it represents one recipe):
struct Recipe {
let title: String
let calories: String
let ingredients: [String]
}
Your parsing code:
var currentText: String?
var recipeTitle: String?
var recipeCalories: String?
var recipeIngredients: [String]?
var tableViewDataSource = [Recipe]()
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
switch elementName {
case "dish":
recipeTitle = nil
recipeCalories = nil
recipeIngredients = nil
case "ingredients":
recipeIngredients = []
case "title", "calories", "item":
currentText = ""
default:
break
}
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
switch elementName {
case "dish":
if let recipeTitle = recipeTitle, let recipeCalories = recipeCalories, let recipeIngredients = recipeIngredients {
let recipe = Recipe(title: recipeTitle, calories: recipeCalories, ingredients: recipeIngredients)
tableViewDataSource.append(recipe)
}
recipeTitle = nil
recipeCalories = nil
recipeIngredients = nil
case "title":
recipeTitle = currentText?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
currentText = nil
case "calories":
recipeCalories = currentText?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
currentText = nil
case "item":
if let currentText = currentText {
recipeIngredients?.append(currentText.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))
}
currentText = nil
default:
break
}
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
currentText?.append(string)
}

Related

Store XML data as an array, Swift

So I am working on a project which will allow users to do multiple different quizzes.
My XML, which is hosted online, is of the following format:
<questions>
<question>
<clue> sample clue 1 </clue>
<correct_answer>2</correct_answer>
<enumeration>1</enumeration>
<info> sample info 1 </info>
<location_clue>Sample locationClue (5,5)</location_clue>
<option_a>Ans1</option_a>
<option_b>Ans2</option_b>
<option_c>Ans3</option_c>
</question>
<question>
<clue> sample clue 2 </clue>
<correct_answer>3</correct_answer>
<enumeration>2</enumeration>
<info> sample info 2 </info>
<location_clue>Sample locationClue (4,2)</location_clue>
<option_a>Ans1</option_a>
<option_b>Ans2</option_b>
<option_c>Ans3</option_c>
</question>
</questions>
my parser initiation looks like this:
if let urlString = URL(string: "realURL goes here.xml -- This has an actual url in my code obviously.")
{
if let parser = XMLParser(contentsOf: urlString)
{
parser.delegate = self
parser.parse()
}
}
parserDidStartElement:
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:])
{
thisName = elementName
if thisName == "hunt"
{
}
}
ParserFoundCharacter:
func parser(_ parser: XMLParser, foundCharacters string: String)
{
let data = string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
if data.count != 0
{
switch thisName
{
case "clue": questionClue = data
break
case "info": questionInfo = data
break
case "location_clue": locationClue = data
break
case "option_a": questionAnswerA = data
break
case "option_b": questionAnswerB = data
break
case "option_c": questionAnswerC = data
default:
break
}
}
}
This is the HuntDetail.swift class which creates a scruct named QUIZ, inside of this scruct there are currently 4 variables, question, answerA, answerB and answerC:
import Foundation
struct QUIZ {
var question = ""
var answerA = ""
var answerB = ""
var answerC = ""
}
Essentially the app will allow users to take multi-choice quizzes. When answers are selected a progress bar at the top of the interface will indicate their progress through the current quiz.
I was wondering if it would be possible to store the value of: clue,info,location_clue,option_a,b,c... inside of an array, from the array I will start to formulate the actual quiz functionality.
As it stands right now, the application will only display the last element of the data previously mentioned.
I know this is long and probably hard to understand what I'm trying to do but if anyone can help with this it would be greatly appreciated. It should also be noted that, yes, I am fairly new to Swift and iOS development as a whole.
Yes you can do this fairly easily, although I haven't used XMLParser in a while.
Note: In my code below I have renamed your QUIZ to Question as the struct represents a single question, not a whole quiz (list of questions)
So you want an empty array to each item to as you parse it:
var quiz = [Question]() // quiz is a list of questions.
Then you want to track the current question you are working on
var currentQuestion: Question?
So then each time you start and end a Question element, you know you have finished parsing a single question, so add it to the list.
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
if elementName == "Question" {
currentQuestion = Question()
}
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if elementName == "Question", let question = currentQuestion {
quiz.append(question)
}
}
So you are just parsing the XML document top to bottom. Once you come upon a opening Question element, create a question object, fill the rest in and then when you encounter a closing question element, add the current question to the list.
By the end of the document your quiz variable should contain all of the questions in the document.
EDIT:
So I had to make a few changes, the foundCharacters can come in parts so we need to keep track of it.
Here is a working playground which returns 2 questions (based on your sample XML above). Answer C is always blank, It looks like this is because of a newline char and the trim is cutting of the text, you may need to remove newlines and then just trim whitespace but this code will give you a good start.
import Foundation
let xmlData = """
<questions>
<question>
<clue> sample clue 1 </clue>
<correct_answer>2</correct_answer>
<enumeration>1</enumeration>
<info> sample info 1 </info>
<location_clue>Sample locationClue (5,5)</location_clue>
<option_a>Ans1</option_a>
<option_b>Ans2</option_b>
<option_c>Ans3</option_c>
</question>
<question>
<clue> sample clue 2 </clue>
<correct_answer>3</correct_answer>
<enumeration>2</enumeration>
<info> sample info 2 </info>
<location_clue>Sample locationClue (4,2)</location_clue>
<option_a>Ans1</option_a>
<option_b>Ans2</option_b>
<option_c>Ans3</option_c>
</question>
</questions>
""".data(using: .utf8)!
struct Question {
var question: String?
var clue: String?
var info: String?
var locationClue: String?
var answerA: String?
var answerB: String?
var answerC: String?
}
class MySuperXMLParser: NSObject, XMLParserDelegate {
private let parser: XMLParser
private var quiz = [Question]()
private var currentQuestion: Question?
private var currentElement: String?
private var foundCharacters = ""
init(data: Data) {
parser = XMLParser(data: data)
super.init()
parser.delegate = self
}
func parse() -> [Question] {
parser.parse()
return quiz
}
func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
if elementName == "question" {
currentQuestion = Question()
}
print("Started element: \(elementName)")
currentElement = elementName
}
func parser(_ parser: XMLParser, foundCharacters string: String) {
print("found characters: \(string)")
foundCharacters += string
}
func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
print("ended element: \(elementName), text = \(foundCharacters)")
let text = foundCharacters.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
switch currentElement
{
case "clue":
currentQuestion?.clue = text
break
case "info":
currentQuestion?.info = text
break
case "location_clue":
currentQuestion?.locationClue = text
break
case "option_a":
currentQuestion?.answerA = text
break
case "option_b":
currentQuestion?.answerB = text
break
case "option_c": currentQuestion?.answerC = text
default:
break
}
foundCharacters = ""
if elementName == "question", let question = currentQuestion {
print("adding question: \(question)")
quiz.append(question)
}
}
}
let parser = MySuperXMLParser(data: xmlData)
let quiz = parser.parse()
print(quiz.count, quiz)
If you don't mind using an external library, you can try XMLMapper.
Just use the following model classes:
class Questions: XMLMappable {
var nodeName: String!
var questions: [Question]?
required init(map: XMLMap) {
}
func mapping(map: XMLMap) {
questions <- map["question"]
}
}
class Question: XMLMappable {
var nodeName: String!
var clue: String?
var correct_answer: Int?
var enumeration: Int?
var info: String?
var location_clue: String?
var option_a: String?
var option_b: String?
var option_c: String?
required init(map: XMLMap) {
}
func mapping(map: XMLMap) {
clue <- map["clue"]
correct_answer <- map["correct_answer"]
enumeration <- map["enumeration"]
info <- map["info"]
location_clue <- map["location_clue"]
option_a <- map["option_a"]
option_b <- map["option_b"]
option_c <- map["option_c"]
}
}
And map your XML by calling map(XMLString:) function in XMLMapper:
let object = XMLMapper<Questions>().map(XMLString: xmlString)
print(object?.questions?.first?.clue ?? "nil")
Hope this helps.

Map function explanation

Someone can explain me that piece of code because I can't understand well. I find this code and I can't understand notably this line : Room(dict: $0)
var rooms: [Room] = [] // The globale variable
func refresh() {
let request = URLRequest(url: URL(string: "\(Config.serverUrl)/rooms")!)
NSURLConnection.sendAsynchronousRequest(request, queue: OperationQueue.main, completionHandler: { resp, data, err in
guard err == nil else {
return
}
let rooms = try! JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions()) as! [[String: AnyObject]]
self.rooms = rooms.map {
Room(dict: $0) // I can't understand this line
}
self.tableView.reloadData()
})
}
My Room struct:
struct Room {
var key: String
var title: String
var cat: String!
init(dict: [String: AnyObject]) {
title = dict["title"] as! String
key = dict["key"] as! String
cat = dict["cat"] as! String
}
init(key: String, title: String, cat: String) {
self.key = key
self.title = title
self.cat = cat
}
func toDict() -> [String: AnyObject] {
return [
"title": title as AnyObject,
"key": key as AnyObject,
"cat": cat as AnyObject
]
}
}
If someone can help me to understand and explain it, thank you
The map function loops over every item in a collection, and applies an operation to each element in the collection.
This piece of code
self.rooms = rooms.map {
Room(dict: $0)
}
is a short form of this.
// `dict` paramater is `$0` in shorter form
self.rooms = rooms.map { (dict : [String: AnyObject]) -> Room in
return Room(dict: dict)
}

extend an Array of Dictionary<String, Any> Swift 3

var dicts = [["key1": "value1", "key2": "value2"]]
dicts.values(of: "key1") // prints - value1
I am working on a project where I want to store the array of dictionary and then fetch the data from there on condition if array of dictionary contains the particular value.
Swift 3.0
You can try this way.
var dicts:[[String:Any]] = []
var check:Bool = false
dicts = [["search_date": "17/03/17", "search_title": ""],["search_date": "17/02/19", "search_title": "parth"],["search_date": "20/02/19", "search_title": "roy"]]
for item in dicts {
if let title = item["search_title"] as? String {
if title == "parth" {
check = true
break
}else {
check = false
}
}
else {
check = false
}
}
print(check)
We can Use Model to solve the Problem
class Person: NSObject, NSCoding {
let name: String
let age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
required init(coder decoder: NSCoder) {
self.name = decoder.decodeObject(forKey: "name") as? String ?? ""
self.age = decoder.decodeInteger(forKey: "age")
}
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name")
coder.encode(age, forKey: "age")
}
}
Class
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// setting a value for a key
let newPerson = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(newPerson)
let encodedData = NSKeyedArchiver.archivedData(withRootObject: people)
UserDefaults.standard.set(encodedData, forKey: "people")
// retrieving a value for a key
if let data = UserDefaults.standard.data(forKey: "people"),
let myPeopleList = NSKeyedUnarchiver.unarchiveObject(with: data) as? [Person] {
myPeopleList.forEach({print( $0.name, $0.age)}) // Joe 10
} else {
print("There is an issue")
}
}
}
All Thanks to Leo Dabus
[Link] (https://stackoverflow.com/a/37983027/3706845)
Your question is very vague. But what I understood is that you want to filter the array of dictionaries so it only contains dictionaries that have a certain value, and this can be done this way:
let filteredDicts = dicts.filter({ $0.values.contains("value2") })

Swift when appending custom class to array all previous values overwritten

I have a custom class called Subject, which I am trying to initialize versions of and append to a array, however when I append one to a array it overwrites all the Subjects all ready in there. For example this code
var things:[Subject] = []
things.append(Subject(initName: "1", initTeacher: "1", initClassroom: "1"))
things.append(Subject(initName: "2", initTeacher: "2", initClassroom: "2"))
print(things[0].name)
print(things[1].name)
is printing Optional("2") Optional("2") when it should be printing 'Optional("1") Optional("2")'
This is the code for my custom class
class Subject: NSManagedObject{
var name: String?
var teacher: String?
var classroom: String?
init(initName: String, initTeacher: String, initClassroom: String){
name = initName
teacher = initTeacher
classroom = initClassroom
}
func save() -> Bool{
if(name == "" || teacher == "" || classroom == ""){
return false
}else{
let appDelgate = UIApplication.sharedApplication().delegate as? AppDelegate
let managedContext = appDelgate?.managedObjectContext
let entity = NSEntityDescription.entityForName("Subject", inManagedObjectContext: managedContext!)
let subject = NSManagedObject(entity: entity!, insertIntoManagedObjectContext: managedContext)
subject.setValue(name, forKey: "name")
subject.setValue(teacher, forKey: "teacher")
subject.setValue(classroom, forKey: "classroom")
do{
try managedContext?.save()
}catch let error as NSError{
print("Failed because of \(error)")
}
return true
}
}
func edit(newName: String, newTeacher: String, newClassroom: String) -> Bool{
let appDelgate = UIApplication.sharedApplication().delegate as? AppDelegate
let managedContext = appDelgate?.managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Subject")
fetchRequest.predicate = NSPredicate(format: "name = %#", name!)
do{
let fetchResults = try managedContext?.executeFetchRequest(fetchRequest)
let editingSubject = fetchResults![0] as! NSManagedObject
editingSubject.setValue(newName, forKey: "name")
editingSubject.setValue(newTeacher, forKey: "teacher")
editingSubject.setValue(newClassroom, forKey: "classroom")
do{
try managedContext?.save()
return true
}catch{
return false
}
}catch{
return false
}
}}
Thanks for any help

Parse the xml array and store the data into array or dictionary using swift

I need to parse the xml data using nsxmlparser.But i am facing issue getting the required data and storing it to array or dictionary using swift.I have implemented the the delegate function and able to find the element name like this
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [NSObject : AnyObject])
{
println(elementName)
if(elementName == "subCategories")
{
categoryName: String = [attributeDict valueForKey:"category"];
id = [attributeDict valueForKey:"id"];
name = [attributeDict valueForKey:"name"];
// Now You can store these in your preferable data models - data objects/array/dictionary
}
however m not able to implement the same using swift.My xml looks like this
<category>
<id>cat00000</id>
<name>Best Buy</name>
<active>true</active>
<path>
<category>
<id>cat00000</id>
<name>Best Buy</name>
</category>
</path>
<subCategories>
<category>
<id>abcat0100000</id>
<name>TV & Home Theater</name>
</category>
<category>
<id>abcat0200000</id>
<name>Audio</name>
</category>
<category>
<id>abcat0300000</id>
<name>Car Electronics & GPS</name>
</category>
That's a weird mix of Swift and Objective-C you posted, and it looks like you're trying to read the attributes of each <category>, but really you need to get the value of each child node of <category>. Try this:
class CategoryParser: NSObject, NSXMLParserDelegate {
var subcategories = [[String : String]]()
var currentSubcategory: [String : String]?
var currentElementName: String?
var completion: (([[String : String]]) -> ())?
func parseXML( string: String, completion: (([[String : String]]) -> ())? ) {
guard let data = string.dataUsingEncoding( NSUTF8StringEncoding ) else {
fatalError( "Base XML data" )
}
self.completion = completion
let parser = NSXMLParser(data: data )
parser.delegate = self
parser.parse()
}
func parserDidEndDocument(parser: NSXMLParser) {
self.completion?( self.subcategories )
}
func parser(parser: NSXMLParser, parseErrorOccurred parseError: NSError) {
print( "Parse error: \(parseError)" )
}
func parser(parser: NSXMLParser, foundCharacters string: String) {
if let elementName = self.currentElementName {
if [ "id", "name" ].contains( elementName ) {
self.currentSubcategory?[ elementName ] = string
}
}
}
func parser(parser: NSXMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
if elementName == "category", let subcategory = self.currentSubcategory {
self.subcategories.append( subcategory )
self.currentSubcategory = nil
}
}
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
if elementName == "category" {
self.currentSubcategory = [String : String]()
}
else {
self.currentElementName = elementName
}
}
}
let categoryParser = CategoryParser()
let xmlString = "<subCategories><category><id>someId</id><name>someName</name></category></subCategories>"
categoryParser.parseXML( xmlString ) { (categories) in
print( categories )
}

Resources