1

I am using Alamofire to scrape web pages for some data, let’s say News. News is a generic object with something like title, content, picture, date, author etc. However for each web site, I use different method. For some I use json for others I use hpple to extract the data. How can I create a some kind of service for each website. Should I create different Services for each web site or is there a better way to use some kind of generic function templates for each web site. Like

Login()
Fetch()
Populate()
return News(…..)

Then after I create the news and populate the tableview, how can I refresh the News object? Since News is generic, it can’t know who created it with which method.

Meanteacher
  • 2,031
  • 3
  • 17
  • 48

1 Answers1

2

There are many ways to design this type of abstraction. I tend to lean towards simplicity as much as possible in my architectural designs if possible. A great pattern here is to use a Service object with class methods to handle calling your different services, parsing the result and calling a success or failure closure.

You can also use a completion handler that doesn't split the success and failure into two things, but then you need to handle the failure or success in your caller objects which I don't really like. Here's an example of the Service design in action.

FirstNewsService

import Alamofire

struct News {
    let title: String
    let content: String
    let date: NSDate
    let author: String
}

class FirstNewsService {

    typealias NewsSuccessHandler = ([News]) -> Void
    typealias NewsFailureHandler = (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void

    // MARK: - Fetching News Methods

    class func getNews(#success: NewsSuccessHandler, failure: NewsFailureHandler) {
        login(
            success: { apiKey in
                FirstNewsService.fetch(
                    apiKey: apiKey,
                    success: { news in
                        success(news)
                    },
                    failure: { response, json, error in
                        failure(response, json, error)
                    }
                )
            },
            failure: { response, json, error in
                failure(response, json, error)
            }
        )
    }

    // MARK: - Private - Helper Methods

    private class func login(#success: (String) -> Void, failure: (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) {
        let request = Alamofire.request(.GET, "login/url")
        request.responseJSON { _, response, json, error in
            if let error = error {
                failure(response, json, error)
            } else {
                // NOTE: You'll need to parse here...I would suggest using SwiftyJSON
                let apiKey = "12345678"
                success(apiKey)
            }
        }
    }

    private class func fetch(
        #apiKey: String,
        success: ([News]) -> Void,
        failure: (NSHTTPURLResponse?, AnyObject?, NSError?) -> Void)
    {
        let request = Alamofire.request(.GET, "fetch/url")
        request.responseJSON { _, _, json, error in
            if let error = error {
                failure(response, json, error)
            } else {
                // NOTE: You'll need to parse here...I would suggest using SwiftyJSON
                let news = [News]()
                success(news)
            }
        }
    }
}

Inside a View Controller

override func viewDidLoad() {
    super.viewDidLoad()

    FirstNewsService.getNews(
        success: { news in
            // Do something awesome with that news
            self.tableView.reloadData()
        },
        failure: { response, json, error in
            // Be flexible here...do you want to retry, pull to refresh, does it matter what the response status code was?
            println("Response: \(response)")
            println("Error: \(error)")
        }
    )
}

Feel free to mod the design however you like to tailor it to your use cases. None of this pattern is set in stone. It just gives you a common way to construct different services. @mattt also has some really cool patterns (Router and CRUD) in the Alamofire README which I would highly recommend reading through. They are definitely more complicated though and still require a Service type of object to maximize code reuse.

Hopefully that helps shed some light.

cnoon
  • 16,575
  • 7
  • 58
  • 66
  • After we get the news, how can I refresh the content of the news from the table ? Since news is a struct it doesn't know who created it. So it can't ask FirstNewsService to getNews again. I am also very puzzled how fetch() will wait till the end of login function. Wouldn't fetch immediately fire before login does its job? In your previous answer, http://stackoverflow.com/questions/28972847/ you used completion blocks. I also tried to copy/paste that boiler code but it gave me error. Since you are trying to return 4 variables but isn't those closure is void and doesn't allow parameters ? – Meanteacher Mar 11 '15 at 16:45
  • I'm terribly sorry about that...my previous answer was very off the mark (too much code too early in the morning I'm afraid). I have edited the answer to handle the async behavior correctly. – cnoon Mar 11 '15 at 17:50
  • As to the refreshing the content, you called the service in your view controller so you know which service the data came from. If you need to figure out which service to also query to get other content, then you'd want to track that state in the View Controller directly, or consider another object to track this type of information. That does not seem like the type of logic though that should live inside the Service. – cnoon Mar 11 '15 at 17:51
  • First of all thanks for your edited answer. It will definitely help a lot. Is something below possible. Less say I have enum with possible NewsType and inside News Struct I have member called var id:NewsType So maybe enum can have that function as property? I am not sure how can I do that. What I am trying to achieve is, I have a table with possible news sources, when I select the entry, it will use designated fetcher with some topic to fetch the news. Something like fetch(NewsType.CNN, "sports") or fetch (NewsType.BBC, "science") so that each one will use their own different fetch function – Meanteacher Mar 11 '15 at 20:59
  • It's definitely possible. I would suggest asking it as a separate question though so we don't highjack this one. Plus it is much easier to write out a sample chunk of code with your question when you're not in comments. – cnoon Mar 12 '15 at 04:34