generic type from string - json

EDIT: (initial example was too simplified, so I rewrote the code to be more specific)
based on http://holko.pl/2016/01/05/typed-table-view-controller/
I am trying to see if it is possible to set the generic parameter of a type from a string..
Say we have this code
protocol Updatable
{
associatedtype ViewModel
func updateWith(viewModel: ViewModel)
}
class ToasterCell: UITableViewCell
{
var toast: String?
func updateWith(viewModel: String) {
toast = viewModel
//Additional config...
}
}
extension ToasterCell: Updatable
{
typealias ViewModel = String
}
class PriceCell: UITableViewCell
{
var tagPrice: Float?
func updateWith(viewModel: Float) {
tagPrice = viewModel
//Additional config
}
}
extension PriceCell: Updatable
{
typealias ViewModel = Float
}
protocol CellConfiguratorType {
var reuseIdentifier: String { get }
var cellClass: AnyClass { get }
func updateCell(_ cell: UITableViewCell)
}
class MyTypeTest<Cell> where Cell: Updatable , Cell: UITableViewCell
{
let viewModel: Cell.ViewModel
let reuseIdentifier: String = String(describing: Cell.self)
let cellClass: AnyClass = Cell.self
init(viewModel: Cell.ViewModel) {
self.viewModel = viewModel
}
func updateCell(_ cell: UITableViewCell)
{
if let c = cell as? Cell
{
c.updateWith(viewModel: viewModel)
}
}
}
extension MyTypeTest: CellConfiguratorType{
}
let myTT1 = MyTypeTest<PriceCell>(viewModel: 3.76)
let myTT2 = MyTypeTest<ToasterCell>(viewModel: "Carpe Diem")
let data = [myTT1, myTT2] as [CellConfiguratorType] // data for the tableView
//register Cell calss ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cellConf = data[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: cellConf.reuseIdentifier)
cellConf.updateCell(cell)
return cell
}
And we want to make it so that the type T is set from a string we get from a JSON response.
//Some JSON {"list":[{"k":"Price","v":368.0},"{"k":"ToasterCell","v":"YOLO"},"{"k":"Toaster","v":"Space"},{"k":"PriceCell","v":1999}]}
the JSON value does not map directly to any object/class, So I need to use that key "k" to know witch class to use.
I tried using the string from the "k" value to setup the cell configurator.
(short example)
//for now let skip any logic in decoding the value / viewModel.
let myTT1 = MyTypeTest<NSClassFromString(list.first.k + "Cell")>(viewModel: list.first.v as Any)
All I got was following errors:
Cannot assign value of type 'T' to type 'AnyClass' (aka 'AnyObject.Type')
Use of undeclared type 'myTypeOBJ'
Is there a way to do this via a string, or do I really need to create a huge "if-else" structure for whatever type I could get from my JSON responses?
EDIT:
I tried to add an init to the CellConfigurator with a param of type Cell so it can infer the Type from the param it self.
init(viewModel: Cell.ViewModel, inferUsing: Cell){....}
where I could attempt to use this (but is does not work as the PATs is getting in the way)
func getSafeBundelName() -> String
{
if let namespace = Bundle.main.infoDictionary!["CFBundleExecutable"] as? String
{
return namespace
}
return ""
}
let cellClass = NSClassFromString("\(getSafeBundelName()).PriceCell") as? UITableViewCell.Type
let cell = cellClass?.init()
let myTT1 = MyTypeTest(viewModel: list.first.v as Any, inferUsing: cell)
I get the error that the Cell type can not be infered. If I try to use the cellClass in the <>
ex: MyTypeTest<cellType>(viewModel: 3.76) all it gives me is that "cellClass" is not declared. Looks to me I am hitting a dead end where PATs become impossible to infer in any way that I can see. I find this limitation very very sad.

You can do this if you downcast the result from NSClassFromString to a type with a known initializer that you can call. If it will always be Toaster or a subclass then you can do:
if let myTypeOBJ = NSClassFromString("Toaster") as? Toaster.Type {
let test = myTypeTest(someOBJ: myTypeOBJ.init())
// test.someThing will be of type Toaster
}
In order for NSClassFromString to work you will also need to specify how you want the type name to be represented in objc, otherwise there will be some other stuff prepended to the type name:
#objc(Toaster)
class Toaster: NSObject

Related

Swift - Missing argument label 'where:' in call but fixing causes a "cannot convert value of type Bool to expected" error

I am trying to access parsed JSON data in an if statement and it outputs the following error:
Missing argument label 'where:' in call
When I resolve the error by clicking fix it gives me the following error:
Cannot convert value of type 'Bool' to expected argument type '(Status) throws -> Bool'
Here is the code:
import UIKit
import Foundation
// JSON parsing
struct Welcome: Decodable {
let greeting: String
let instructions: [Instruction]
}
struct Instruction: Decodable {
let statuses: [Status]
let message, messageURL: String
let status: Bool
let load, time: Int
enum CodingKeys: String, CodingKey {
case statuses, message
case messageURL = "message_url"
case status, load, time
}
}
struct Status: Decodable {
let title: String
let code: Int
let status: Bool
let time: Int
}
//main class
class ViewController: UIViewController {
//Hides the status bar
override var prefersStatusBarHidden: Bool {
return true
}
//Storyboard controllers
#IBOutlet weak var downImage: UIImageView!
#IBOutlet weak var upImage: UIImageView!
//The meat of the program
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let url = "https://issteamdown.com/status.json"
let urlObj = URL(string: url)
URLSession.shared.dataTask(with: urlObj!) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONDecoder().decode(Instruction.self, from: data)
json.statuses.forEach { status in
if !json.statuses.contains(where: false) {
print("all true")
// show the image view
self.upImage.isHidden = false
}
}
} catch {
print(error)
}
}.resume()
}
}
If someone can please assist me with this error, I would be very grateful. I have tried searching but the solutions offered by similar questions don't seem to be very helpful or I am not implementing them correctly.
If someone could help me understand what is going on here, that would be fantastic of them.
Thanks in advance!
contains(where:) expects a closure that returns a bool:
if !json.statuses.contains(where: {$0.status == false}) {
print("all true")
self.upImage.isHidden = false
}
json.statuses is an array of type [Status], and you want to check if no element of that array has a status == false .
As noted by #MartinR in the comments, you can write the above condition more elegantly like so:
if json.statuses.allSatisfy ({ $0.status }) {
print("all true")
self.upImage.isHidden = false
}
To learn more about closures, have a look here.

Swift - Why fetching data from JSON have a blank TableView delay before data get displayed?

Edit: Please ignore the old answers. I can't create a new question my accunt was blocked. So I edited this one with a new question in the title.
How can i have my data result as soon as i viewDidLoad my tableView ?
Here is a snippet of my code. Can anyone help me to understand the asynchronous part that make my code fetch data and bring me back the result late.
`
let apiUrl = "https://shopicruit.myshopify.com/admin/products.json?
page=1&access_token=c32313df0d0ef512ca64d5b336a0d7c6"
var myRowProduct : String = ""
var productArrayName : [String] = []
var productsPassedOver : [String] = []
override func viewDidLoad() {
super.viewDidLoad()
getFullProductsJson(url: apiUrl, tag1: myRowProduct){array, err in
if err != nil {
print("ERROR 2")
}else{
self.productsPassedOver = array
self.tableView.reloadData()
print("productsPassedOver \(self.productsPassedOver)")
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return productsPassedOver.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath)
cell.selectionStyle = .none
cell.textLabel?.text = "\(indexPath.row + 1). " + productsPassedOver[indexPath.row] + "\n" //label are in every single cell. The current text that is displayed by the label
cell.textLabel?.numberOfLines = 0; //removes any limitations on the number of lines displayed
return cell
}
//Networking JSON
func getFullProductsJson(url: String, tag1: String, completion: #escaping ([String], Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
//response.result.value comes back as an optional so use ! to use it. Also value have datatype of Any? thats why we JSON casting
if response.result.isSuccess{
let fullJSON : JSON = JSON(response.result.value!)
let finalArrayOfProductS = self.updateTagProductData(json: fullJSON, tag2: tag1)
completion(finalArrayOfProductS, nil)
}
else{
print("Error JSON")
}
}
}
//JSON parsing, deal with formatting here
func updateTagProductData(json : JSON, tag2: String) -> [String]{
let tagClicked = tag2
var nbOfProducts : Int = 0
nbOfProducts = json["products"].count
var inventoryAvailableTotal : Int = 0
var nbOfVariants : Int = 0
for i in 0..<nbOfProducts {
let tagsGroup : String = json["products"][i]["tags"].stringValue
let productName : String = json["products"][i]["product_type"].stringValue
nbOfVariants = json["products"][i]["variants"].count
if tagsGroup.contains(tagClicked){
for j in 0..<nbOfVariants{
inventoryAvailableTotal += json["products"][i]["variants"][j]["inventory_quantity"].int!
}
print("\(tagClicked) tag exist in the product \(i): \(productName)")
print("inventorytotal \(inventoryAvailableTotal)")
productArrayName.append("\(productName): \(inventoryAvailableTotal) available")
}
}
return productArrayName
}
}
`
This is a function that does something in the background and return immediately, and when it finishes, it calls your completion handler.
So it is natural for this function to go directly to the return finaleNbOfRowsInProductsListPage.
Your solution is that the function shouldn't return the value, but it should
accept a completion handler, and call it when ended. This is the continuation passing style.
func getFullProductsJson(url: String, tag1: String, completion: #escaping (Int?, Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
...
// When you are done
completion(finaleNbOfRowsInProductsListPage, nil)
}
}
Also please note to try avoiding setting a lot of variables, try to make everything a parameter or a return value from a function, this is easier to debug. For example, try to make the list as a parameter that is passed to the completion handler, not as a member variable of your view controller, make this member variable only for the list that is ready to be displayed.
func getFullProductsJson(url: String, tag1: String, completion: #escaping (Products?, Error?) -> ()) {
Alamofire.request(url, method: .get).responseJSON {
response in
...
// When you are done
completion(products, nil)
}
}
var products: [Product] = []
func refreshData() {
getFullProductsJson(url: "YOUR_URL_HERE", tag1: "TAG") {
// Try to use [weak self], also, read about Automatic Reference Counting
productsNullable, error in
if let products = productsNullable {
// Alamofire calls your callback on a background thread
// So you must return to the main thread first when you want
// to pass the result to any UI component.
DispatchQueue.main.async {
self?.products = products
self?.tableView.reloadData()
}
}
}
}

unexpectedly found nil while unwrapping an Optional value for my UITextField

I'm having trouble passing the JSON values (I'm reading successfully) into my textfield on the next viewcontroller because of this unwrapping error, stating my text field is nil.
I'm very stuck. Here's my class that reads the JSON:
class DoOAuth
{
func doOAuthFitbit() -> String{
var name = ""
let oauthswift = OAuth1Swift(
consumerKey: "eabf603efe9e45168d057b60b03f8e94",
consumerSecret: "46b4dfa8c9d59666769e03f887d531a8",
requestTokenUrl: "https://api.fitbit.com/oauth/request_token",
authorizeUrl: "https://www.fitbit.com/oauth/authorize?display=touch",
accessTokenUrl: "https://api.fitbit.com/oauth/access_token")
oauthswift.authorizeWithCallbackURL( NSURL(string: "fitbit://oauth")!,
success:{
credential, response in
let vc: ViewController = ViewController()
let user: OAuthSwiftClient = OAuthSwiftClient(consumerKey: oauthswift.consumer_key, consumerSecret: oauthswift.consumer_secret, accessToken: credential.oauth_token, accessTokenSecret: credential.oauth_token_secret)
let object:[String : AnyObject] = ["oauth_token": credential.oauth_token, "oauth_token_secret" : credential.oauth_token_secret]
user.get("https://api.fitbit.com/1/user/-/profile.json", parameters: object,
success: {
(data: NSData, response: NSHTTPURLResponse) -> Void in
let jsonValues = JSON(data: data, options: NSJSONReadingOptions.AllowFragments, error: nil)
println(jsonValues)
/*public var dictionary: [Swift.String: JSON]?
{
switch self
{
case .Dictionary(let d):
var jsonObject: [Swift.String: JSON] = [:]
for(k,v) in d
{
jsonObject[k] = JSON.wrap(v)
}
return jsonObject
default:
return nil
}
}*/
for(key, subJson) in jsonValues
{
if let nm = subJson["fullName"].string
{
println("\(nm)")
name = nm
}
}
/*for(index: String, subJson: JSON) in jsonValues
{
let name = subJson.dictionary?["fullName"]?.string
println("\(name!)")
//vc.nm.text = name!
main.acceptJson(name!)
}*/
},
failure: {
(error:NSError!) -> Void in
println(error.localizedDescription)
println("error")
})
},
failure: {
(error:NSError!) -> Void in
println(error.localizedDescription)
})
return name
}
}
I call a function that is supposed to receive the JSON strings (acceptJson) located in the next view controller:
class mainMenu: UIViewController
{
var oauthfitbit: DoOAuth = DoOAuth()
var name = ""
//let vc: ViewController = ViewController()
#IBOutlet weak var lbl: UILabel!
#IBOutlet weak var nameField: UITextField!{
didSet{
nameField.text = name
}
}
override func viewWillAppear(animated: Bool)
{
//name = oauthfitbit.doOAuthFitbit()
//self.nameField.text = "Working"
//self.nameField.text = name
}
func acceptJson(info: String!)
{
println("\(info)")
self.nameField.text = info
//name = info
}
}
I get the excepting thrown on the setting nameField.text line stating nameField is nil. How do I get the textfield to store the JSON string?
And here's the initial View Controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBOutlet weak var nm: UITextField!
//let main: mainMenu = mainMenu()
var name = ""
#IBAction func connectPressed(sender: UIButton)
{
var oauthFitbit: DoOAuth = DoOAuth()
name = oauthFitbit.doOAuthFitbit()
self.performSegueWithIdentifier("loginSuccess", sender: nil)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "loginSuccess")
{
let controller = segue.destinationViewController as! mainMenu
controller.name = name
//vc.nameField.text = "Hello"
}
}
}
How did you create your textfield? Was it through Interface Builder? There have been plenty of times when I've run into these type of problems when using Interface Builder and IBOutlets.
The first step is to make sure your text field is connected to your view controller from the .xib file correctly. Delete the connection and reconnect by control (command?) dragging from IB to your view controller code.
If you're not using IB and still having problems, post the code where you create the textfield. You have to set your view controller as the text field delegate if you're creating it programmatically, I believe. It's been awhile since I've done it that way.
Let us know!
The easiest way to get the new view controller the value of nm is in prepareForSegue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "YourIdentifier" {
let controller = segue.destinationViewController as! mainMenu
controller.name = name
}
}
So, to get this to work, you will need to add a name instance variable (var name = "") to your first view controller, and change main.acceptJson(nm) to name = nm.
Once name is set in your first view controller, you can segue to the second view controller.
In the second view controller, you can change your text field outlet to this:
#IBOutlet weak var nameField: UITextField! {
didSet {
nameField.text = name
}
}
The didSet is a property observer. You can't set the nameField text field directly from the first view controller's prepareForSegue because the text field isn't set up yet when prepareForSegue is called in the first view controller. That's why you're storing it in an instance variable. Then, because of the didSet, your text field's text property will be set as soon as it comes into existence.
UPDATE:
The following is in a class of its own. Let's call that class DoOAuth (looks like that's what you called it):
class DoOAuth {
func doOAuthFitbit() -> String { // Now it's returning a string
var name = "" // Create local variable to return
let oauthswift = OAuth1Swift(
consumerKey: "eabf603efe9e45168d057b60b03f8e94",
consumerSecret: "46b4dfa8c9d59666769e03f887d531a8",
requestTokenUrl: "https://api.fitbit.com/oauth/request_token",
authorizeUrl: "https://www.fitbit.com/oauth/authorize?display=touch",
accessTokenUrl: "https://api.fitbit.com/oauth/access_token")
oauthswift.authorizeWithCallbackURL( NSURL(string: "fitbit://oauth")!,
success:{
credential, response in
//let vc: ViewController = ViewController() // Get rid of this
let user: OAuthSwiftClient = OAuthSwiftClient(consumerKey: oauthswift.consumer_key, consumerSecret: oauthswift.consumer_secret, accessToken: credential.oauth_token, accessTokenSecret: credential.oauth_token_secret)
let object:[String : AnyObject] = ["oauth_token": credential.oauth_token, "oauth_token_secret" : credential.oauth_token_secret]
user.get("https://api.fitbit.com/1/user/-/profile.json", parameters: object,
success: {
(data: NSData, response: NSHTTPURLResponse) -> Void in
let jsonValues = JSON(data: data, options: NSJSONReadingOptions.AllowFragments, error: nil)
println(jsonValues)
/*public var dictionary: [Swift.String: JSON]?
{
switch self
{
case .Dictionary(let d):
var jsonObject: [Swift.String: JSON] = [:]
for(k,v) in d
{
jsonObject[k] = JSON.wrap(v)
}
return jsonObject
default:
return nil
}
}*/
for(key, subJson) in jsonValues
{
if let nm = subJson["fullName"].string
{
println("\(nm)")
name = nm // Store 'nm' in local variable declared above
}
}
}
return name
} // end doOAuthFitbit()
} // end class
Now change your connectPressed() method in ViewController to this:
#IBAction func connectPressed(sender: UIButton)
{
var oauthFitbit: DoOAuth = DoOAuth()
name = oauthFitbit.doOAuthFitbit() // doOAuthFitbit() now returns a String
self.performSegueWithIdentifier("loginSuccess", sender: nil)
}
Now it should work.

Paragraph Tags After using JSON in Swift (for Arrays and not Single strings)

I have been struggling with this for some time and I have finally resorted to stackoverflow as I cannot seem to find an answer anywhere else. I am new to programming (I only used design with HTML and CSS) and I am really enjoying learning - but i have run into a problem.
So my problem is that I am making a call to an endpoint which gives me data in a JSON format. I am able to get the data the I wan out, but when I use the data, It has HTML paragraph tags, which show in the app. I am using SWIFTYJSON to unwrap the optionals.
I am going to add my ViewController's code as well as add println() function which will show what the problem is. If you need a little more information from please let me know, I just don't know where to start??
class NECViewController: UITableViewController {
var names = [String] ()
var quickGlance = [String] ()
var cat = [String] ()
var refreshControll = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
DataManager.getWordpressData {
(wordpressData) -> Void in
let json = JSON(data: wordpressData)
self.tableView.reloadData()
// Creating Loop
if let dataArray = json.arrayValue {
//2
var information = [DataModel]()
//3
for Details in dataArray {
var title: String? = Details["title"].stringValue
var excerpt: String? = Details["excerpt"].stringValue
var image: String? = Details["featured_image"]["guid"].stringValue
var catName: String? = Details["terms"]["category"][0]["name"].stringValue
var content: String? = Details["title"].stringValue
self.names.append(title!)
self.quickGlance.append(excerpt!)
self.cat.append(catName!)
}
//4
println(self.names)
println(self.quickGlance) //THIS IS WHERE THE PROBLEM LIES
println(self.cat)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
}
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Potentially incomplete method implementation.
// Return the number of sections.
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete method implementation.
// Return the number of rows in the section.
return names.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("messageCell", forIndexPath: indexPath) as NECCellView
cell.messageLabel.text = names[indexPath.row] as String
cell.postCategory.text = cat[indexPath.row] as String
cell.quickGlance.text = quickGlance[indexPath.row] as String
return cell
}
}
and then I have this printed out in the console.
[Beginner Post, Intermediate Post, Advanced Post]
[<p>This is an example of a beginner post. The content found in this post is HTML and not a string. This is a problem!</p>
, <p>This is an example of an intermediate post. You can come back when the website is ready :)!! You need to login or register to bookmark/favorite this content.</p>
, <p>This is an example of an advanced post. You can come back when the website is ready :)! You need to login or register to bookmark/favorite this content.</p>
]
[Beginner, Intermediate, Advanced]
As you can see, It is full of HTML which is what I need to get rid of. If someone could help - That would be AMAZING!

Best way to convert JSON or other untyped data into typed classes in Swift?

I'm trying to parse out JSON into typed classes for safety/convenience, but it's proving very clunky. I wasn't able to find a library or even a post for Swift (Jastor is as close as I got). Here's a fabricated little snippet to illustrate:
// From NSJSONSerialization or similar and casted to an appropriate toplevel type (e.g. Dictionary).
var parsedJson: Dictionary<String, AnyObject> = [ "int" : 1, "nested" : [ "bool" : true ] ]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(fromParsedJson json: NSDictionary) {
if let parsedStringValue = json["string"] as? String {
self.stringValueWithDefault = parsedStringValue
}
if let parsedIntValue = json["int"] as? Int {
self.intValueRequired = parsedIntValue
} else {
// Raise an exception...?
}
// Optional-chaining is actually pretty nice for this; it keeps the blocks from nesting absurdly.
if let parsedBool = json["nested"]?["bool"] as? Bool {
self.nestedBoolBroughtToTopLevel = parsedBool
}
if let parsedFirstInt = json["firstInt"] as? Int {
if let parsedSecondInt = json["secondInt"] as? Int {
self.combinedIntRequired = parsedFirstInt * parsedSecondInt
}
}
// Most succinct way to error if we weren't able to construct self.combinedIntRequired?
}
}
TypedObject(fromParsedJson: parsedJson)
There's a number of issues here that I'm hoping to work around:
It's extremely verbose, since I need to wrap every single property in a copy-pasted if-let for safety.
I'm not sure how to communicate errors when required properties are missing (as noted above). Swift seems to prefer (?) using exceptions for show-stopping problems (rather than pedestrian malformed data as here).
I don't know a nice way to deal with properties that exist but are the wrong type (given that the as? casting will fail and simply skip the block, it's not very informative to the user).
If I want to translate a few properties into a single one, I need to nest the let blocks proportional to the number of properties I'm combining. (This is probably more generally a problem with combining multiple optionals into one value safely).
In general, I'm writing imperative parsing logic when I feel like I ought to be able to do something a little more declarative (either with some stated JSON schema or at least inferring the schema from the class definition).
I do this using the Jastor framework:
1) Implement a Protocol that has a single function that returns an NSDictionary response:
protocol APIProtocol {
func didReceiveResponse(results: NSDictionary)
}
2) Create an API class that defines an NSURLConnection object that can be used as a Request URL for iOS's networking API. This class is created to simply return a payload from the itunes.apple.com API.
class API: NSObject {
var data: NSMutableData = NSMutableData()
var delegate: APIProtocol?
func searchItunesFor(searchTerm: String) {
// Clean up the search terms by replacing spaces with +
var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+",
options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music"
var url: NSURL = NSURL(string: urlPath)
var request: NSURLRequest = NSURLRequest(URL: url)
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
println("Search iTunes API at URL \(url)")
connection.start()
}
// NSURLConnection Connection failed.
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
println("Failed with error:\(error.localizedDescription)")
}
// New request so we need to clear the data object.
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
self.data = NSMutableData()
}
// Append incoming data.
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
self.data.appendData(data)
}
// NSURLConnection delegate function.
func connectionDidFinishLoading(connection: NSURLConnection!) {
// Finished receiving data and convert it to a JSON object.
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
delegate?.didReceiveResponse(jsonResult)
}
}
3) Create a class with associated properties that inherits from Jastor
NSDictionary response:
{
"resultCount" : 50,
"results" : [
{
"collectionExplicitness" : "notExplicit",
"discCount" : 1,
"artworkUrl60" : "http:\/\/a4.mzstatic.com\/us\/r30\/Features\/2a\/b7\/da\/dj.kkirmfzh.60x60-50.jpg",
"collectionCensoredName" : "Changes in Latitudes, Changes in Attitudes (Ultmate Master Disk Gold CD Reissue)"
}
]
}
Music.swift
class Music : Jastor {
var resultCount: NSNumber = 0
}
4) Then in your ViewController be sure to set the delegate to self and then make a call to the API's searchITunesFor() method.
var api: API = API()
override func viewDidLoad() {
api.delegate = self;
api.searchItunesFor("Led Zeppelin")
}
5) Implement the Delegate method for didReceiveResponse(). Jastor extends your class to set a NSDictionary of the results returned from the iTunes API.
// #pragma - API Delegates
func didReceiveResponse(results: NSDictionary) {
let music = Music(dictionary: results)
println(music)
}
Short version: Since init isn't allowed to fail, validation has to happen outside of it. Optionals seem to be the intended tool for flow control in these cases. My solution is to use a factory method that returns an optional of the class, and use option chaining inside it to extract and validate the fields.
Note also that Int and Bool aren't children of AnyObject; data coming from an NSDictionary will have them stored as NSNumbers, which can't be cast directly to Swift types. Thus the calls to .integerValue and .boolValue.
Long version:
// Start with NSDictionary since that's what NSJSONSerialization will give us
var invalidJson: NSDictionary = [ "int" : 1, "nested" : [ "bool" : true ] ]
var validJson: NSDictionary = [
"int" : 1,
"nested" : [ "bool" : true ],
"firstInt" : 3,
"secondInt" : 5
]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(intValue: Int, combinedInt: Int, stringValue: String?, nestedBool: Bool?) {
self.intValueRequired = intValue
self.combinedIntRequired = combinedInt
// Use Optionals for the non-required parameters so
// we know whether to leave the default values in place
if let s = stringValue {
self.stringValueWithDefault = s
}
if let n = nestedBool {
self.nestedBoolBroughtToTopLevel = n
}
}
class func createFromDictionary(json: Dictionary<String, AnyObject>) -> TypedObject? {
// Validate required fields
var intValue: Int
if let x = (json["int"]? as? NSNumber)?.integerValue {
intValue = x
} else {
return nil
}
var combinedInt: Int
let firstInt = (json["firstInt"]? as? NSNumber)?.integerValue
let secondInt = (json["secondInt"]? as? NSNumber)?.integerValue
switch (firstInt, secondInt) {
case (.Some(let first), .Some(let second)):
combinedInt = first * second
default:
return nil
}
// Extract optional fields
// For some reason the compiler didn't like casting from AnyObject to String directly
let stringValue = json["string"]? as? NSString as? String
let nestedBool = (json["nested"]?["bool"]? as? NSNumber)?.boolValue
return TypedObject(intValue: intValue, combinedInt: combinedInt, stringValue: stringValue, nestedBool: nestedBool)
}
class func createFromDictionary(json: NSDictionary) -> TypedObject? {
// Manually doing this cast since it works, and the only thing Apple's docs
// currently say about bridging Cocoa and Dictionaries is "Information forthcoming"
return TypedObject.createFromDictionary(json as Dictionary<String, AnyObject>)
}
}
TypedObject.createFromDictionary(invalidJson) // nil
TypedObject.createFromDictionary(validJson) // it works!
I've also done the following to convert to/from:
class Image {
var _id = String()
var title = String()
var subTitle = String()
var imageId = String()
func toDictionary(dict dictionary: NSDictionary) {
self._id = dictionary["_id"] as String
self.title = dictionary["title"] as String
self.subTitle = dictionary["subTitle"] as String
self.imageId = dictionary["imageId"] as String
}
func safeSet(d: NSMutableDictionary, k: String, v: String) {
if (v != nil) {
d[k] = v
}
}
func toDictionary() -> NSDictionary {
let jsonable = NSMutableDictionary()
self.safeSet(jsonable, k: "title", v: self.title);
self.safeSet(jsonable, k: "subTitle", v: self.subTitle);
self.safeSet(jsonable, k: "imageId", v: self.imageId);
return jsonable
}
}
Then I simply do the following:
// data (from service)
let responseArray = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSArray
self.objects = NSMutableArray()
for item: AnyObject in responseArray {
var image = Image()
image.toDictionary(dict: item as NSDictionary)
self.objects.addObject(image)
}
If you want to POST the data:
var image = Image()
image.title = "title"
image.subTitle = "subTitle"
image.imageId = "imageId"
let data = NSJSONSerialization.dataWithJSONObject(image.toDictionary(), options: .PrettyPrinted, error: nil) as NSData
// data (to service)
request.HTTPBody = data;