I'm trying to parse a JSON in my app and it's not working.
Here is a look at the JSON I'm trying to parse:
Check out the following screenshots. I get the error "Unexpectedly found nil while unwrapping an optional value" on the line
editorialElement.title = nodeElement!.valueForKey("title") as! String
Can anyone help me properly parse this JSON ?
EDIT 1: Here is more code. It shows what class I'm using (i.e. what objects I am creating based on the JSON file). I imagine this is what you meant by where I initialize JSON objects.
Here is my router as well (build using Alamofire). I feel like something might be wrong with it but at the same time, it makes a lot of sense and I don't know what missing:
EDIT 2: Here is the actual code:
func populateEditorials() {
if populatingEditorials {
return
}
populatingEditorials = true
Alamofire.request(GWNetworking.Router.Editorials(self.currentPage)).responseJSON() { response in
if let JSON = response.result.value {
/*
if response.result.error == nil {
*/
/* Creating objects for every single editorial is long running work, so we put that work on a background queue, to keep the app very responsive. */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
/*
// Making an array of all the node IDs from the JSON file
let nodeIDArray : [String]
var nodeCounter : Int = 0 */
for node in JSON as! NSDictionary {
var nodeElement = JSON.valueForKey(String(node))
self.nodeIDArray.addObject(String(node.key))
var editorialElement : EditorialElement = EditorialElement(title: "init", nodeID: 0, timeStamp: 0, imageURL: "init", author: "init", issueNumber: "init", volumeNumber: "init", articleContent: "init")
editorialElement.title = nodeElement!.valueForKey("title") as! String
editorialElement.nodeID = nodeElement!.valueForKey("nid") as! Int
editorialElement.timeStamp = nodeElement!.valueForKey("revision_timestamp") as! Int
editorialElement.imageURL = nodeElement!.valueForKey("image_url") as! String
editorialElement.author = nodeElement!.valueForKey("author") as! String
editorialElement.issueNumber = nodeElement!.valueForKey("issue_int") as! String
editorialElement.volumeNumber = nodeElement!.valueForKey("volume_int") as! String
editorialElement.articleContent = nodeElement!.valueForKey("html_content") as! String
self.editorialObjects.addObject(editorialElement)
/*
nodeIDArray[nodeCounter] = jsonValue{nodeCounter}.string
let editorialInfos : EditorialElement = ((jsonValue as! NSDictionary).valueForKey("\(nodeIDArray[nodeCounter])") as! [NSDictionary]).map { EditorialElement(title: $0["title"] as! String, nodeID: $0["nid"] as! Int, timeStamp: $0["revision_timestamp"] as! Int, imageURL: $0["image_url"] as! String, author: $0["author"], issueNumber: $0["issue_int"] as! Int, volumeNumber: $0["volume_int"] as! Int, articleContent: $0["html_content"] as! String) /* I am going to try to break this line down to simplify it and fix the build errors */
*/
}
print(self.editorialObjects)
}
/* Sorting the elements in order of newest to oldest (as the array index increases) */
self.editorialObjects.sort({ $0.timeStamp > $1.timeStamp })
let lastItem = self.editorialObjects.count
let indexPaths = (lastItem..<self.editorialObjects.count).map { NSIndexPath(forItem: $0, inSection: $0) }
dispatch_async(dispatch_get_main_queue()) {
self.editorialsTableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Automatic) // Animation implemented for testing, to be removed for version 1.0
}
self.currentPage++
/* } */
}
self.populatingEditorials = false
}
}
Here is the code for my Class and Router:
class EditorialElement: NSObject {
var title: String // title
var nodeID: Int // nid
var timeStamp: Int // revision_timestamp
var imageURL: String? // image_url
var author: String // author
var issueNumber: String // issue_int
var volumeNumber: String // volume_int
var articleContent: String // html_content
/* To get an NSDate objec from Unix timestamp
var date = NSDate(timeIntervalSince1970: timeStamp) */
init(title: String, nodeID: Int, timeStamp: Int, imageURL: String, author: String, issueNumber: String, volumeNumber: String, articleContent: String) {
self.title = title
self.nodeID = nodeID
self.timeStamp = timeStamp
self.imageURL = imageURL
self.author = author
self.issueNumber = issueNumber
self.volumeNumber = volumeNumber
self.articleContent = articleContent
}
override func isEqual(object: AnyObject!) -> Bool {
return (object as! EditorialElement).nodeID == self.nodeID
}
override var hash: Int {
return (self as EditorialElement).nodeID
}
}
enum Router : URLRequestConvertible {
static let baseURLString = MyGlobalVariables.baseURL
case Issue
case Editorials(Int)
case News(Int)
case Random(Int)
case Pictures(Int)
var URLRequest: NSMutableURLRequest {
let path : String
let parameters: [String: AnyObject]
(path) = {
switch self {
case .Issue:
return ("/issue")
case .Editorials (let editorialsSection): /* If section == 0, this will return the first ten editorials. If section == 1, then section * 10 = 10, and we will get the ten editorials after that. */
return ("/list/editorials/\(editorialsSection * 10)")
case .News (let newsSection):
return ("/list/news/\(newsSection * 10)")
case .Random (let randomSection):
return ("/list/random/\(randomSection * 10)")
case .Pictures (let page):
return ("/list/pictures/\(page)")
}
}()
let URL = NSURL(string: Router.baseURLString)
let GoldenWordsURLRequest = NSMutableURLRequest(URL: URL!.URLByAppendingPathComponent(path))
/* let encoding = Alamofire.ParameterEncoding.URL */
return GoldenWordsURLRequest
/* return encoding.encode(URLRequest, parameters: parameters).0 */
}
}
}
That's because you take an optional reference (editorialElement returned from a failable initializer) and call valueForKey("title") on it. It stumbles over the access to "title" because it goes first in you code while the target of the call is nil. I would recommend organizing your code as follows:
if let editorialElement = EditorialElement(title:..., nodeID: and such)
{
... assigning new values to the properties
}
and you will notice that you don't enter the if-let scope. You will avoid the crash and make sure the problem is inside the arguments you initialize the editorialElement with.
Related
I have this function searchMoviesOnJson:
func searchMoviesOnJson(imdbTitle: String, completionHandler: #escaping (Dictionary<String, Any>?) -> ()) {
let urlByName: String = "https://www.omdbapi.com/?s=\(imdbTitle)&type=movie"
//returns a list of movies that contains the title searched
//------------------------------------------------
Alamofire.request(urlByName).responseJSON {
response in
switch response.result {
case .success(let value):
let moviesJSON = value
completionHandler(moviesJSON as? Dictionary<String, Any>)
case .failure(_):
completionHandler(nil)
}
}
//------------------------------------------------
}
That gives me this response from api (e.g.: imdbTitle = "arq"):
{
Response = True;
Search = (
{
Poster = "https://images-na.ssl-images-amazon.com/images/M/MV5BMjAxODQ2MzkyMV5BMl5BanBnXkFtZTgwNjU3MTE5OTE#._V1_SX300.jpg";
Title = ARQ;
Type = movie;
Year = 2016;
imdbID = tt5640450;
},
{
Poster = "N/A";
Title = Arq;
Type = movie;
Year = 2011;
imdbID = tt2141601;
},
{
Poster = "N/A";
Title = "A.R.Q.";
Type = movie;
Year = 2015;
imdbID = tt3829612;
}
);
totalResults = 3;
}
So I created a this class MovieByTitle
class MovieByTitle {
var poster : String?
var title : String?
var year : Int?
var imdbID : String?
init(poster: String?, title: String?, year: Int?, imdbID: String?) {
//validation
if let isPoster = poster { self.poster = isPoster }
else { self.poster = nil }
if let isTitle = title { self.title = isTitle }
else { self.title = nil }
if let isYear = year { self.year = isYear }
else { self.year = nil }
if let isImdbID = imdbID { self.imdbID = isImdbID }
else { self.imdbID = nil }
}
}
And now my doubt, I also create this MovieDAO:
class MovieDao {
func getMovies(imdbTitle: String, completionHandler: #escaping (([MovieByTitle]) -> ())) {
//function that conects to the api
searchMoviesOnJson(imdbTitle: imdbTitle, completionHandler: {
moviesJSON in
//array to keep the attributes received by the dictionary
var moviesArray = [MovieByTitle]()
//searchResults is the response from my request as an array
if let searchResults = moviesJSON?["Search"] as? NSArray{
for searchResult in searchResults {
let movieResult = searchResult as! Dictionary<String,Any>
let movieDetail = MovieByTitle()
movieDetail.poster = movieResult["Poster"] as? String
movieDetail.title = movieResult["Title"] as? String
movieDetail.year = movieResult["Year"] as? Int
movieDetail.imdbID = movieResult["imdbID"] as? String
moviesArray.append(movieDetail)
}
}
})
}
}
But the xcode returns an error in line:
let movieDetail = MovieByTitle()
Error message: missing argument for parameter 'poster' in call (and so on with the others)
What is the right sintax for that? What is the better way to cast my dictionary response as an object?
You MovieByTitle init function requires 4 parameters that are missing.
Solution 1: Add a secondary init:
init() {}
Solution 2: Define existing init parameters as optional by giving them default values:
init(poster: String? = nil, title: String? = nil, year: Int? = nil, imdbID: String? = nil)
Solution 3: Call the existing init with the parameters it needs:
let movieDetail = MovieByTitle(poster: movieResult["Poster"] as? String, title: movieResult["Title"] as? String, year: movieResult["Year"] as? Int, imdbID: movieResult["imdbID"] as? String)
Your init function requires 4 parameters. You haven't included any. Try the following
let poster = movieResult["Poster"] as? String
let title = movieResult["Title"] as? String
let year = movieResult["Year"] as? Int
let imdbID = movieResult["imdbID"] as? String
let movieDetail = MovieByTitle(poster:poster, title:title, year:year, imdbID:imdbDB)
I'm new to iOS development and I understand that allowing optional values when an object is initialized is not a 'good citizen' technique. That being said, I've read that it is good practice to always have values set, like this:
class Item{
var name: String
var color: String
init(name: String, color: String) {
self.name = name
self.color = color
}
}
This looks nice and tidy but how can I do something like that working with Firebase? Look what I've got so far:
private func loadPosts(){
databaseHandle = ref.child("users/\(self.user.uid)/posts").observe(.value, with:{(snapshot) in
var newPosts = [Post]()
for itemSnapShot in snapshot.children {
let post = Post(snapshot: itemSnapShot as! FIRDataSnapshot)
newPosts.append(post!)
}
self.posts = newPosts
self.tableView.reloadData()
})
}
This guy is placed in my PostsViewController where I have my table view. This is my model:
class Post {
var ref: FIRDatabaseReference?
var title: String?
var answer: String?
var contentUrl: String?
var photoUrl: String?
var createdAt: String?
var feeling: String?
var kind: String?
var text: String?
var uid: String?
var measurements: Dictionary<String, String>?
//MARK: Initialization
init?(snapshot: FIRDataSnapshot){
ref = snapshot.ref
let data = snapshot.value as! Dictionary<String, Any>
title = data["title"]! as? String
answer = data["answer"] as? String
contentUrl = data["content_url"] as? String
photoUrl = data["photo_url"] as? String
createdAt = data["created_at"] as? String
feeling = data["feeling"] as? String
kind = data["kind"] as? String
text = data["text"] as? String
uid = data["uid"] as? String
measurements = data["measurements"] as? Dictionary<String, String>
}
}
I don't know exactly why but those question marks doesn't feel quite right and now and then I get some nil pointer error, which I think I should be able to avoid by using the 'good citizen' technique.
So, does anybody know how can I use Firebase following Swift best practices?
Either you wish to allow the properties of your Post class to be nil or you don't.
If you do, that's fine. The code you posted allows any of them to be nil. You just need to safely access each property every time you need it.
If you don't, then don't make them optional. Then in your init you need to ensure none of the properties are set to nil by giving each a default if there is no value in the snapshot.
class Post {
var ref: FIRDatabaseReference
var title: String
var answer: String
var contentUrl: String
var photoUrl: String
var createdAt: String
var feeling: String
var kind: String
var text: String
var uid: String
var measurements: [String : String]
//MARK: Initialization
init?(snapshot: FIRDataSnapshot) {
if let data = snapshot.value as? [String : Any] {
self.ref = snapshot.ref
title = data["title"] as? String ?? ""
answer = data["answer"] as? String ?? ""
contentUrl = data["content_url"] as? String ?? ""
photoUrl = data["photo_url"] as? String ?? ""
createdAt = data["created_at"] as? String ?? ""
feeling = data["feeling"] as? String ?? ""
kind = data["kind"] as? String ?? ""
text = data["text"] as? String ?? ""
uid = data["uid"] as? String ?? ""
measurements = data["measurements"] as? [String : String] ?? [:]
} else {
return nil
}
}
}
Note how this ensures there is a proper snapshot. Note how a default value is set to each property if there is no value in the snapshot. Obviously you can assign any default you wish. I use the empty string as an example.
Even if you want to allow the properties to be nil, you should at least update your code to check for a valid snapshot like in the code above.
Of course you can have a combination where some properties can't be nil and some can. That's up to your needs.
First it is fine for you to have optionals in your data model, as long as you assign value to it later on in the future.
I would recommend to use ObserveSingleEvent() and you should make use of completion handler to make it easy. If you don't know completion handler: Link
I recommend:
• not to put database ref in your class model, and instead of using Dictionary<String, String>? just use [String: AnyObject]?
• make your post array public so that it can be accessed into the tableview.
Here's example:
class func getPosts(uid: String, _ completion: #escaping (_ posts: [Post]?, _ error: Error?) -> Void) {
//update inside users node
var posts = [Post]()
Firebase.databaseRef.child("users").child(uid).child("posts").observeSingleEvent(of: FIRDataEventType.value, with: { (dataSnapshot) in
guard let postsDictionary = dataSnapshot.value as? [String: AnyObject] else {
completion(nil, nil)
return
}
let n = postsDictionary.count
for postDictionary in postsDictionary {
let post = Post()
post.userID = uid
if let content = postDictionary.value["content"] as? String {
post.content = content
}
if let imageURL = postDictionary.value["imageURL"] as? String {
post.imageURL = imageURL
}
if let timeStamp = postDictionary.key as String! {
if let date = timeStamp.convertToDate() {
post.timeStamp = date
}
post.postIdentifier = timeStamp
}
posts.append(post)
if posts.count == n {
// Sort the array by the newest post
let sortedPosts = posts.sorted(by: { $0.timeStamp.compare($1.timeStamp) == .orderedDescending })
completion(sortedPosts, nil)
}
}
}) { (error) in
completion(nil, error)
}
}
Assigning to tableview be like:
getPosts(uid: Current.user.userID!) { (posts, error) in
guard error == nil else {
print(error.debugDescription)
return
}
cell.label.text = posts[indexPath.item].content
I am working on a Firebase Swift project using CocoaPods.
Every time after I log in the main ViewController, automatically I get EXC_BREAKPOINT error:
fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
Here are some of my code lines where I got errors:
All codes from Joke.swift:
import Foundation
import Firebase
class Joke {
private var _jokeRef: Firebase!
private var _jokeKey: String!
private var _jokeText: String!
private var _jokeVotes: Int!
private var _username: String!
var jokeKey: String {
return _jokeKey
}
var jokeText: String {
return _jokeText
}
var jokeVotes: Int {
return _jokeVotes //1
}
var username: String {
return _username
}
// Initialize the new Joke
init(key: String, dictionary: Dictionary<String, AnyObject>) {
self._jokeKey = key
// Within the Joke, or Key, the following properties are children
if let votes = dictionary["votes"] as? Int {
self._jokeVotes = votes
}
if let joke = dictionary["jokeText"] as? String {
self._jokeText = joke
}
if let user = dictionary["author"] as? String {
self._username = user
} else {
self._username = ""
}
// The above properties are assigned to their key.
self._jokeRef = DataService.dataService.JOKE_REF.childByAppendingPath(self._jokeKey)
}
// Add or Subtract a Vote from the Joke.
func addSubtractVote(addVote: Bool) {
if addVote {
_jokeVotes = _jokeVotes + 1
} else {
_jokeVotes = _jokeVotes - 1
}
// Save the new vote total.
_jokeRef.childByAppendingPath("votes").setValue(_jokeVotes)
}
}
In JokeCellTableViewCell.swift:
var joke: Joke!
...............
func configureCell(joke: Joke) {
self.joke = joke
// Set the labels and textView.
self.jokeText.text = joke.jokeText
self.totalVotesLabel.text = "Total Votes: \(joke.jokeVotes)" // 2
self.usernameLabel.text = joke.username
// Set "votes" as a child of the current user in Firebase and save the joke's key in votes as a boolean.
.........
}
And in the main ViewController, JokesFeedTableViewController.swift:
var jokes = [Joke]()
....................
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let joke = jokes[indexPath.row]
// We are using a custom cell.
if let cell = tableView.dequeueReusableCellWithIdentifier("JokeCellTableViewCell") as? JokeCellTableViewCell {
// Send the single joke to configureCell() in JokeCellTableViewCell.
cell.configureCell(joke) // 3
return cell
} else {
return JokeCellTableViewCell()
}
...........
// 1, // 2, // 3 are code lines where errors appear.
I hope you could help me to fix this!
Thank you in advance!
Your problem is that you have not clearly defined the expectations of the Joke class.
Your initializer suggests that the properties on Joke should be optional, however, you are using them as though they are not. You must decide on which way you want to take it.
If the properties can be optional, I would suggest something like this:
class Joke {
private let jokeReference: Firebase
let jokeKey: String
private(set) var jokeText: String?
private(set) var jokeVotes: Int?
let username: String
// Initialize the new Joke
init(key: String, dictionary: Dictionary<String, AnyObject>) {
jokeKey = key
// Within the Joke, or Key, the following properties are children
if let votes = dictionary["votes"] as? Int {
jokeVotes = votes
}
if let joke = dictionary["jokeText"] as? String {
jokeText = joke
}
if let user = dictionary["author"] as? String {
username = user
} else {
username = ""
}
// The above properties are assigned to their key.
jokeReference = DataService.dataService.JOKE_REF.childByAppendingPath(jokeKey)
}
}
However, if the properties should never be nil, you need something like this:
class Joke {
private let jokeReference: Firebase
let jokeKey: String
let jokeText: String
let jokeVotes: Int?
let username: String
// Initialize the new Joke
init?(key: String, dictionary: Dictionary<String, AnyObject>) {
jokeKey = key
guard let votes = dictionary["votes"] as? Int,
joke = dictionary["jokeText"] as? String else {
return nil
}
jokeText = joke
jokeVotes = votes
if let user = dictionary["author"] as? String {
username = user
} else {
username = ""
}
// The above properties are assigned to their key.
jokeReference = DataService.dataService.JOKE_REF.childByAppendingPath(jokeKey)
}
}
EDIT 1: Added my revision cellForRowAtIndexPath code at the bottom of the post
EDIT 2: Added my new EditorialElement code
I am having difficulty properly unwrapping my UILabel text inputs properly, so all of my text says "Optional(Author name)" (for example, this is an app for a newspaper). I have tried to force unwrap my variables in different ways but was not able to make it work.
The text input for my UILabels are created in the following way. The corresponding class is "EditorialElement", which has the following property definitions:
class EditorialElement: NSObject {
var title: String! // title
var nodeID: Int? // nid
var timeStamp: Int // revision_timestamp
var imageURL: String? // image_url
var author: String? // author
var issueNumber: String! // issue_int
var volumeNumber: String! // volume_int
var articleContent: String! // html_content
/* To get an NSDate objec from Unix timestamp
var date = NSDate(timeIntervalSince1970: timeStamp) */
init(title: String, nodeID: Int, timeStamp: Int, imageURL: String, author: String, issueNumber: String, volumeNumber: String, articleContent: String) {
self.title = title
self.nodeID = nodeID
self.timeStamp = timeStamp
self.imageURL = imageURL
self.author = author
self.issueNumber = issueNumber
self.volumeNumber = volumeNumber
self.articleContent = articleContent
}
override func isEqual(object: AnyObject!) -> Bool {
return (object as! EditorialElement).nodeID == self.nodeID
}
override var hash: Int {
return (self as EditorialElement).nodeID!
}
}
Then, I use this class to retrieve data from my JSON file and parse it into an "editorialObjects" array (sorry about all the commenting and bad spacing):
func populateEditorials() {
if populatingEditorials {
return
}
populatingEditorials = true
Alamofire.request(GWNetworking.Router.Editorials(self.currentPage)).responseJSON() { response in
if let JSON = response.result.value {
/*
if response.result.error == nil {
*/
/* Creating objects for every single editorial is long running work, so we put that work on a background queue, to keep the app very responsive. */
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
/* Making an array of all the node IDs from the JSON file */
var nodeIDArray : [Int]
if (JSON .isKindOfClass(NSDictionary)) {
for node in JSON as! Dictionary<String, AnyObject> {
let nodeIDValue = node.0
var lastItem : Int = 0
self.nodeIDArray.addObject(nodeIDValue)
if let editorialElement : EditorialElement = EditorialElement(title: "init", nodeID: 0, timeStamp: 0, imageURL: "init", author: "init", issueNumber: "init", volumeNumber: "init", articleContent: "init") {
editorialElement.title = node.1["title"] as! String
editorialElement.nodeID = Int(nodeIDValue)
let timeStampString = node.1["revision_timestamp"] as! String
editorialElement.timeStamp = Int(timeStampString)!
editorialElement.imageURL = String(node.1["image_url"])
editorialElement.author = String(node.1["author"])
editorialElement.issueNumber = String(node.1["issue_int"])
editorialElement.volumeNumber = String(node.1["volume_int"])
editorialElement.articleContent = String(node.1["html_content"])
lastItem = self.editorialObjects.count
print (editorialElement.nodeID)
self.editorialObjects.addObject(editorialElement)
/* Sorting the elements in order of newest to oldest (as the array index increases] */
let timestampSortDescriptor = NSSortDescriptor(key: "timeStamp", ascending: false)
self.editorialObjects.sortUsingDescriptors([timestampSortDescriptor])
let indexPaths = (lastItem..<self.editorialObjects.count).map { NSIndexPath(forItem: $0, inSection: 0) }
/*
nodeIDArray[nodeCounter] = jsonValue{nodeCounter}.string
let editorialInfos : EditorialElement = ((jsonValue as! NSDictionary].1["\(nodeIDArray[nodeCounter]]"] as! [NSDictionary]].map { EditorialElement(title: $0["title"] as! String, nodeID: $0["nid"] as! Int, timeStamp: $0["revision_timestamp"] as! Int, imageURL: $0["image_url"] as! String, author: $0["author"], issueNumber: $0["issue_int"] as! Int, volumeNumber: $0["volume_int"] as! Int, articleContent: $0["html_content"] as! String] // I am going to try to break this line down to simplify it and fix the build errors
*/
}
print(self.editorialObjects.count)
}
}
dispatch_async(dispatch_get_main_queue()) {
self.editorialsTableView.reloadData()
}
self.currentPage++
}
}
self.populatingEditorials = false
}
}
And then I just use those objects for my labels in my cellForRowAtIndexPath method:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = indexPath.row
let cell = tableView.dequeueReusableCellWithIdentifier(EditorialTableCellIdentifier, forIndexPath: indexPath) as! EditorialsTableViewCell
let title = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).title
let timeStamp = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).timeStamp
let timeStampDateObject = NSDate(timeIntervalSince1970: NSTimeInterval(Int((editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).timeStamp)))
timeStampDateString = dateFormatter.stringFromDate(timeStampDateObject)
let imageURL = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).imageURL
let author : String! = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).author!
let issueNumber = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).issueNumber
let volumeNumber = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).volumeNumber
let articleContent = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).articleContent
let nodeID = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).nodeID
/* Unlike the Pictures Collection View, there is no need to create another Alamofire request here, since we already have all the content we want from the JSON we downloaded. There is no URL that we wish to place a request to to get extra content. */
cell.editorialHeadlineLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.editorialHeadlineLabel.text = title
cell.editorialAuthorLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.editorialAuthorLabel.text = author
cell.editorialPublishDateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.editorialPublishDateLabel.text = timeStampDateString
return cell
}
Where should I be force-unwrapping my variables ?
EDIT 1: Revised code
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = indexPath.row
guard let cell = tableView.dequeueReusableCellWithIdentifier(EditorialTableCellIdentifier, forIndexPath: indexPath) as? EditorialsTableViewCell else {
print ("error: editorialsTableView cell is not of class EditorialsTableViewCell, we will use RandomTableViewCell instead")
return tableView.dequeueReusableCellWithIdentifier(EditorialTableCellIdentifier, forIndexPath: indexPath) as! RandomTableViewCell
}
if let editorialObject = editorialObjects.objectAtIndex(indexPath.row) as? EditorialElement {
// we just unwrapped editorialObject
let title = editorialObject.title ?? "" // if editorialObject.title == nil, then we return an empty string.
let timeStampDateObject = NSDate(timeIntervalSince1970: NSTimeInterval(editorialObject.timeStamp))
let timeStampDateString = dateFormatter.stringFromDate(timeStampDateObject)
let author = editorialObject.author ?? ""
let issueNumber = editorialObject.issueNumber ?? ""
let volumeNumber = editorialObject.volumeNumber ?? ""
let articleContent = editorialObject.articleContent ?? ""
let nodeID = editorialObject.nodeID ?? 0
cell.editorialHeadlineLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.editorialHeadlineLabel.text = title
cell.editorialAuthorLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.editorialAuthorLabel.text = String(author)
cell.editorialPublishDateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.editorialPublishDateLabel.text = timeStampDateString
} else {
}
return cell
}
EDIT 2: new EditorialElement code
class EditorialElement: NSObject {
var title: String // title
var nodeID: Int // nid
var timeStamp: Int // revision_timestamp
var imageURL: String // image_url
var author: String // author
var issueNumber: String // issue_int
var volumeNumber: String // volume_int
var articleContent: String // html_content
/* To get an NSDate objec from Unix timestamp
var date = NSDate(timeIntervalSince1970: timeStamp) */
init(title: String, nodeID: Int, timeStamp: Int, imageURL: String, author: String, issueNumber: String, volumeNumber: String, articleContent: String) {
self.title = title
self.nodeID = nodeID
self.timeStamp = timeStamp
self.imageURL = imageURL
self.author = author
self.issueNumber = issueNumber
self.volumeNumber = volumeNumber
self.articleContent = articleContent
}
override func isEqual(object: AnyObject!) -> Bool {
return (object as! EditorialElement).nodeID == self.nodeID
}
override var hash: Int {
return (self as EditorialElement).nodeID
}
}
Couple things. 1. You should only be force unwrapping them if you know that there is going to be something there. But if you're super confident in that (or you don't expect/want the app to work without them) then you should just have them be forced unwrapped from the get go. Theres no point in doing var imageURL: String? only to write imageURL! everywhere in the code. The point of optionals is to allow you to gracefully handle situations wherein the object might be nil.
Anyway, assuming this is the correct structure for you the first thing I would do is if let the return of object at index. So writing:
if let element = editorialObjects.objectAtIndex(indexPath.row) as? EditorialElement
Now you can use element as an EditorialElement throughout cell for row. From there you can decide how/when to unwrap everything else.
So let author : String! = (editorialObjects.objectAtIndex(indexPath.row) as! EditorialElement).author! would become either
let author = element.author! or you could if let to avoid a crash and handle the case where it is nil. if let author = element.author { // do something }
My opinion, change your variable declaration to not nil variable, example: var author: String!
Then, when you set value to your variable, set it is empty character (or default value) if it's nil:
editorialElement.author = String(node.1["author"]) ?? ""
After that, you can use your variable without unwrap everywhere.
In most of the cases forced unwrapping is a code smell. Don't use it unless you're linking IBOutlets or in some other exceptional cases. Try to properly unwrap your variables.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = indexPath.row
guard let cell = tableView.dequeueReusableCellWithIdentifier(EditorialTableCellIdentifier, forIndexPath: indexPath) as? EditorialsTableViewCell else {
// if we fall here cell isn't a EditorialsTableViewCell
// handle that properly
}
if let editorialObject = editorialObjects.objectAtIndex(indexPath.row) as? EditorialElement {
// editorialObject here is unwrapped
let title = editorialObject.title ?? "" // if editorialObject.title == nil we store empty string
let timeStampDateObject = NSDate(timeIntervalSince1970: NSTimeInterval(editorialObject.timeStamp))
let timeStampDateString = dateFormatter.stringFromDate(timeStampDateObject)
cell.editorialHeadlineLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
cell.editorialHeadlineLabel.text = title
cell.editorialPublishDateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleSubheadline)
cell.editorialPublishDateLabel.text = timeStampDateString
} else {
// no such editorial object - log error.
// return empty cell or do more sofisticated error handling
}
return cell
}
I'm using Alamofire and am parsing the returned JSON into an object as shown below:
final class User: NSObject, ResponseObjectSerializable {
var id: Int
var facebookUID: String?
var email: String
var firstName: String
var lastName: String
var phone: String?
var position: String?
var timeCreated: CVDate
init?(response: NSHTTPURLResponse, var representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
representation = dataRepresentation
}
if let id = representation.valueForKeyPath("id") as? Int {
self.id = id
} else {
self.id = 0
}
if let facebookUID = representation.valueForKeyPath("facebook_UID") as? String {
self.facebookUID = facebookUID
}
if let email = representation.valueForKeyPath("email") as? String {
self.email = email
} else {
self.email = ""
}
if let firstName = representation.valueForKeyPath("first_name") as? String {
self.firstName = firstName
} else {
self.firstName = ""
}
if let lastName = representation.valueForKeyPath("last_name") as? String {
self.lastName = lastName
} else {
self.lastName = ""
}
if let phone = representation.valueForKeyPath("phone") as? String {
self.phone = phone
}
if let position = representation.valueForKeyPath("position_name") as? String {
self.position = position
}
if let timeCreated = representation.valueForKeyPath("time_created") as? String {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
if let date = formatter.dateFromString(timeCreated) {
self.timeCreated = CVDate(date: date)
} else {
self.timeCreated = CVDate(date: NSDate())
}
} else {
self.timeCreated = CVDate(date: NSDate())
}
}
}
My question is, is this style the best way to decode JSON and set the non-optional instance variables? For example, in this statement:
if let id = representation.valueForKeyPath("id") as? Int {
self.id = id
}
I am required by the compiler to add an else clause and set the id to something otherwise xCode throws an error saying: self.id is not initialized at implicitly generated super.init call.
But at the same time, intializing self.id with a value of 0 is wrong and doesn't help me at all.
But at the same time, intializing self.id with a value of 0 is wrong and doesn't help me at all.
If having a default value for self.id feels wrong, then you should make this property an Optional. That way you wouldn't have to add an else clause:
final class User: NSObject, ResponseObjectSerializable {
var id: Int?
var facebookUID: String?
var email: String
var firstName: String
var lastName: String
var phone: String?
var position: String?
var timeCreated: CVDate
init?(response: NSHTTPURLResponse, var representation: AnyObject) {
if let dataRepresentation = ((representation as! NSDictionary).valueForKey("data") as? [String: AnyObject]) {
representation = dataRepresentation
}
if let id = representation.valueForKeyPath("id") as? Int {
self.id = id
}
...
Update
You said in the comments:
I always need to have an id for the user object though.
If you have to have this id property then the question is moot, you just have to do
let id = representation.valueForKeyPath("id") as! Int
and guarantee earlier that this value will exist.
Because if your object needs an ID, then you can't initialize it anyway if this value doesn't exist and if you don't want a default value.
You could use ?? to provide default values like this:
self.id = (representation.valueForKeyPath("id") as? Int) ?? 0
While the ResponseObjectSerializable code is a great example from the Alamofire project, it's really a better idea to use a dedicated JSON parsing library that has actual error states. This is far better than using optionals to represent error states, or having to provide a default value for every field just in case the response isn't correctly formed.
Although it has a bit of learning curve, I prefer to use Argo for my JSON parsing. Once you get the hang of it it makes JSON parsing practically bulletproof. Better yet, it's easy to integrate with Alamofire, especially version 3 that was released today.
To address your concern about not having an ID being an error condition, you could use a failable initializer. I did that in a recent project. Looks something like this:
let id: Int!
init? (inputJson: NSDictionary) {
if let id = inputJson["id"] as? Int {
self.id = id
} else {
// if we are initing from JSON, there MUST be an id
id = nil
cry(inputJson) // this logs the error
return nil
}
}
Of course, this means your code will need to accept that the initialization of your entire object may fail ..