0

Hey I'm trying to use Yelp's Review API and am having trouble structuring/writing the code necessary to display the different Yelp Star Ratings. I have no problem getting the response (it's successful). Yelp has provided image assets of all their different star ratings (5, 4.5, 4 etc. stars). Because the rating response is as a Double, I converted that into a String value. As for knowing which to call, I created an enum class so that it knows which image name to use. Using that name, I can then use it to find the image asset I need.

Now that I structure the code this way, my app crashes. Xcode will build it but upon opening the app, it crashes.

Enum class:

import Foundation
import UIKit

enum Rating: String {

case five = "regular_5"
case fourAndAHalf = "regular_4_half"
case four = "regular_4"
case threeAndAHalf = "regular_3_half"
case three = "regular_3"
case twoAndAHalf =  "regular_2_half"
case two = "regular_2"
case oneAndAHalf = "regular_1_half"
case one = "regular_1"
case zero = "regular_0"

}

Yelp Client Service class:

import Foundation
import Alamofire
import SwiftyJSON

class YelpClientService {

    static func getReviews(url: String, completionHandler: @escaping ([Review]?)-> Void)
{
    let httpHeaders: HTTPHeaders = ["Authorization": "Bearer \(UserDefaults.standard.string(forKey: "token") ?? "")"]

    //removing diacritics from the URL
    if let requestUrl = URL(string: url.folding(options: .diacriticInsensitive, locale: .current))
    {
        Alamofire.request(requestUrl, encoding: URLEncoding.default, headers: httpHeaders).responseJSON { (returnedResponse) in
            let returnedJson = JSON(with: returnedResponse.data as Any)
            let reviewArray = returnedJson["reviews"].array
            print(reviewArray as Any)

            var reviews = [Review]()

            for review in reviewArray! {

                let userName = review["user"]["name"].stringValue

                let ratingDouble = review["rating"].doubleValue
                let rating = String(ratingDouble)

                let text = review["text"].stringValue

                let formatter = DateFormatter()
                formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

                let timeCreated =  formatter.date(from: review["time_created"].stringValue)


                let url = review["url"].stringValue

                let review = Review(rating: Rating(rawValue: rating)!, userName: userName, text: text, timeCreated: timeCreated!, url: url)
                reviews.append(review)

            }

            completionHandler(reviews)

        }
    }
    else
    {
        print("invalid url")
        completionHandler(nil)
    }
}

}

Func in View Controller thats displaying the Star:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: "reviewCell", for: indexPath) as! ReviewCell

    let review = reviewList[indexPath.row]

    print(review.userName)

    cell.userName.text = review.userName
    cell.reviewText.text = review.text

    cell.yelpStars.image = UIImage(named: review.rating.rawValue)




    //cell.date.text = review.timeCreated



    return cell

}

The error when I build is: fatal error: unexpectedly found nil while unwrapping an Optional value.

I'm not sure what went wrong. Is it correct of me to instantiate rating as a Rating type? Should I keep it String?

I realize this is long code but I hope someone can help! Thank you!

Sam
  • 495
  • 3
  • 11
  • 19

2 Answers2

0

I am sure it would crash. The way you have written it. let ratingDouble = review["rating"].doubleValue you are expecting double. It would be 0, 4.5, 3.0 etc. Which would get converted to string "0","4.5" "3.0" etc.Then you try to initialise rating with Rating(rawValue : rating), Rating enum does not have these raw values as "0", "4.5" etc, so nil will be returned. You are force unwrapping it with '!", no doubt its crashing. You will need to format your enum like this

    enum Rating: String {

    case five = "5.0"
    case fourAndAHalf = "4.5"
    case four = "4.0"
    case threeAndAHalf = "3.5"
    case three = "3.0"
    case twoAndAHalf =  "2.5"
    case two = "2.0"
    case oneAndAHalf = "1.5"
    case one = "1.0"
    case zero = "0.0"

getImageName()-> String {
switch self {
case five:
    return "ImageNameForFive"
case fourAndHalf:
    return "ImageNameForFourAndHalf.
......

}
}

    }

and change

let rating = String(ratingDouble)

to

let rating = String.init(format: "%.1f", ratingDouble)
Mohammad Sadiq
  • 5,070
  • 28
  • 29
  • Because I'm trying to populate an imageView cell (cell.yelpStars.image = UIImage(named: review.rating.rawValue)) does that mean I would have to change the asset names? – Sam Jul 22 '17 at 20:40
  • See the updated answer. You can get the image like cell.yelpStars.image = UIImage(named: review.rating.getImagName) – Mohammad Sadiq Jul 22 '17 at 20:45
0

The error fatal error: unexpectedly found nil while unwrapping an Optional value. is thrown when Swift is unable to do an action usually when the targets are nil, empty, non-existent or undefined.

If a value could potentially be nil, empty, non-existent or undefined; it is known as an optional value. In order to use these values in our code, we must unwrap them. If the value is either nil, empty, non-existent or undefined, our app would most likely crash if it was not unwrapped safely.

To unwrap an object safely in swift we can either use if let or a guard statement. The code is written inside of this only runs if the object is not null.

It's good practice to safely unwrap all your objects in swift to prevent crashes. An example can be found below:

Safe Unwrapping

// initalize a string that may be nil (an optional)
var string: String? = nil

// create a random number between 0-2
let randomNum:UInt32 = arc4random_uniform(3)

// create a switch to set the string value based on our number
switch (randomNum) {

case 0: 
   string = "Some String"

default:
  break

}

// attempt to print out our string

// using a guard statement
guard let string = string else {
   // handle if string is null
   return
}

// print the string from the guard
print(string)

// using if let
if let string = string {
   print(string)
} else {
  // handle string is null
}

Unsafe Unwrapping

// initalize a string that may be nil (an optional) var string: String? = nil

// create a random number between 0-2
let randomNum:UInt32 = arc4random_uniform(3)

// create a switch to set the string value based on our number
switch (randomNum) {

case 0: 
   string = "Some String"

default:
  break

}

// attempt to print our string by forcefully unwrapping it even if it is null causing a crash if it is null
print(string!)

So you can see the difference, in the second one the app would crash with the same error you are getting as it failed to unwrap an optional value in the event the random number is not 0.

It is possible to safely unwrap an object without using if let or guard. This is known as an inline conditional which is basically an if/else clause but quicker.

Inline Conditionals

// if the string is null, it will print the value beside the two `??`
print(string ?? "This is what is printed if the string is nil")

So now that you have all this knowledge, you can go ahead and take a look at your code to see if you are forcefully unwrapping any of your values. A hint is that you use the ! to do this.

Also, the enum that you made takes string values like "half" not double values even if it is a string like '0.5". So it could also crash

Some examples I picked out that may cause the crash are:

for review in reviewArray! review["rating"].doubleValue Rating(rawValue: rating)! timeCreated!

Jesse Onolemen
  • 1,277
  • 1
  • 15
  • 32