3
\$\begingroup\$

I am new in iOS development and Swift. I am going to develop a simple application which communicates with external REST services. This is my first experiment.

I appreciate feedback on best practices and improvements. Style comments are always considered as well.

import Foundation
import SwiftyJSON

class UserObject {
    var pictureURL: String!
    var username: String!

    required init(json: JSON) {
        pictureURL = json["picture"]["medium"].stringValue
        username = json["email"].stringValue
    }
}
import Foundation
import SwiftyJSON

typealias ServiceResponse = (JSON, Error?) -> Void

class RestApiManager: NSObject {

    static let sharedInstance = RestApiManager()
    let baseURL = "http://api.randomuser.me/"

    func getRandomUser(onCompletion: @escaping (JSON) -> Void) {

        let route = baseURL
        makeHTTPGetRequest(path: route, onCompletion: { json, err in
            onCompletion(json as JSON)
        })
    }

    private func makeHTTPGetRequest(path: String, onCompletion: @escaping ServiceResponse) {

        let request = URLRequest(url: URL(string: path)!)
        let session = URLSession.shared

        session.dataTask(with: request) {data, response, err in
            if(err != nil) {
                onCompletion(JSON.null, err)
            } else {
                let jsonData = data
                let json:JSON = JSON(data: jsonData!)
                onCompletion(json, nil)
            }
        }.resume()
    }
}
import UIKit
import SwiftyJSON

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    var tableView: UITableView!
    var items = [UserObject]()

    override func viewWillAppear(_ animated: Bool) {

        let frame:CGRect = CGRect(x: 0, y: 100, width: self.view.frame.width, height: self.view.frame.height - 100)
        self.tableView = UITableView(frame: frame)
        self.tableView.dataSource = self
        self.tableView.delegate = self
        self.view.addSubview(self.tableView)

        let btn = UIButton(frame: CGRect(x: 0, y: 25, width: self.view.frame.width, height: 50))
        btn.backgroundColor = UIColor.cyan
        btn.setTitle("Add new dummy", for: UIControlState.normal)
        btn.addTarget(self, action: #selector(addDummyData), for: UIControlEvents.touchUpInside)
        self.view.addSubview(btn)
    }

    func addDummyData() {
        RestApiManager.sharedInstance.getRandomUser { (json: JSON) in
            if let results = json["results"].array {
                for entry in results {
                    self.items.append(UserObject(json: entry))
                }

                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            }
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "CELL")

        if cell == nil {
            cell = UITableViewCell(style: UITableViewCellStyle.value1, reuseIdentifier: "CELL")
        }

        let user = self.items[indexPath.row]

        if let url = NSURL(string: user.pictureURL) {
            if let data = NSData(contentsOf: url as URL) {
                cell?.imageView?.image = UIImage(data: data as Data)
            }
        }

        cell!.textLabel?.text = user.username
        return cell!
    }

    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.
    }

}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

This is mostly a clean and well considered design. Some opportunities for improvement which stand out:

Make the baseURL property a parameter of the initialiser for RestApiManager. This will allow you to construct the api class with any URL, which is useful for testing, or initialising from a config file.

class RestApiManager: NSObject {

    static let sharedInstance = RestApiManager(baseURL: "http://api.randomuser.me/")
    let baseURL = "http://api.randomuser.me/"
    init(baseURL: URL) {
        self.baseURL = baseURL
    }

RestApiManager.getRandomUser should return a UserObject instance, instead of a dictionary. The view controller should not need to know the intricacies of parsing the JSON object.

func getRandomUser(onCompletion: @escaping ([UserObject]) -> Void) {

    let route = baseURL
    makeHTTPGetRequest(path: route, onCompletion: { json, err in
        var users = [UserObject]()
        if let results = json["results"].array {
            for entry in results {
                users.append(UserObject(json: entry))
            }
        }
        onCompletion(users)
    })
}

The view controller should not depend on a concrete class. The API class should conform to a protocol, which is the dependency used in the view controller. This allows the view controller to be tested with a mock API.

protocol APIProtocol {
    func getRandomUser(completion: @escaping ([UserObject]) -> Void)
}

class RestApiManager: NSObject, APIProtocol {

    func getRandomUser(onCompletion: @escaping ([UserObject]) -> Void) {

Use dependency inversion to pass in an instance of the api class to the view controller.

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    var api: APIProtocol!

    // ... more code

    func addDummyData() {
        api.getRandomUser { (users: [UserObject]) in
            DispatchQueue.main.async { 
                // Update on main queue to prevent race condition 
                // if multiple requests complete at the same time
                self.users.append(contentsOf: users)
                self.tableView.reloadData()
            }
        }
    }

Usage:

let viewController = // instantiate view controller
viewController.api = RestApiManager.sharedInstance
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.