How to call function from another ViewController - ios

I am trying to call a function reloadTable in my HomeViewController from my Task class. But I keep being thrown an
Use of instance member 'reloadTable' on type 'HomeViewController'; did you mean to use a value of type 'HomeViewController' instead?
This is my HomeViewController code:
import UIKit
import Alamofire
import SwiftyJSON
class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
func reloadTable() {
self.displayTask.reloadData()
}
}
This is my Tasks class:
import Foundation
import Alamofire
import SwiftyJSON
class Tasks {
static let sharedInstance = Tasks()
var datas: [JSON] = []
func getTaskDetails(){
Alamofire.request(.GET, Data.weeklyEndpoint).validate().responseJSON { response in
switch response.result {
case .Success(let data):
let json = JSON(data)
if let buildings = json.array {
for building in buildings {
if let startDate = building["start_date"].string{
print(startDate)
}
if let tasks = building["tasks"].array{
Tasks.sharedInstance.datas = tasks
HomeViewController.reloadTable()
for task in tasks {
if let taskName = task["task_name"].string {
print(taskName)
}
}
}
}
}
case .Failure(let error):
print("Request failed with error: \(error)")
}
}
}
// for prevent from creating this class object
private init() { }
}

In this case reloadTable() is an instance method. You can't call it by class name, you have to create object for HomeViewController and then you have to call that method by using that object.
But in this situation no need to call the method directly by using HomeViewController object. You can do this in another way by using NSNotification
Using NSNotification :
Add a notification observer for your HomeViewController
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(HomeViewController.reloadTable), name:"ReloadHomeTable", object: nil)
Add the above lines in your viewDidLoad method of HomeViewController
Now replace this line HomeViewController.reloadTable() by NSNotificationCenter.defaultCenter().postNotificationName("ReloadHomeTable", object: nil) in your Task class

As many of the other answers states, you are trying to access a non-static function on a static reference.
I would suggest the use of closures, consider the following;
Create a closure for .Success and the .Failure, this enables you to act accordingly to the data request result. The onError closure is defined as an optional, this means you don't have to implement the onError in the ´getTaskDetails` function call, implement it where you feel you need the error information.
func getTaskDetails(onCompletion: ([Task]) -> (), onError: ((NSError) -> ())? = nil) {
Alamofire.request(.GET, Data.weeklyEndpoint).validate().responseJSON {
response in
switch response.result {
case .Success(let data):
let json = JSON(data)
if let buildings = json.array {
for building in buildings {
if let startDate = building["start_date"].string{
print(startDate)
}
if let tasks = building["tasks"].array{
Tasks.sharedInstance.datas = tasks
onCompletion(tasks)
}
}
}
case .Failure(let error):
print("Request failed with error: \(error)")
onError?(error)
}
}
}
From your HomeViewController:
// Call from wherever you want to reload data.
func loadTasks(){
// With error handling.
// Fetch the tasks and reload data.
Tasks.sharedInstance.getTaskDetails({
tasks in
self.displayTask.reloadData()
}, onError: {
error in
// Handle error, display a message or something.
print(error)
})
// Without error handling.
// Fetch the tasks and reload data.
Tasks.sharedInstance.getTaskDetails({
tasks in
self.displayTask.reloadData()
})
}
Basics
Closures

Make class Task as Struct, make function getTaskDetails as static and try to call function getTaskDetails() in HomeViewController. In result this of this function use realoadTable()

In HomeViewController you have defined reloadTable() as instance method not the class function.You should call reloadTable() by making instance of HomeViewController as HomeViewController(). Replace
HomeViewController.showLeaderboard()
HomeViewController().showLeaderboard()
Hope it helps. Happy Coding.

Related

Unit test API Call with internal switch case

I am trying to unit test a class that has different modes of setup.
class Controller{
enum Mode{
case listing
case pages(String?)
}
private (set) var mode : Mode = .listing
private (set) var models = [Model]()
init() {
...
}
init(id : String) {
mode = .pages(id)
}
func fetchInfo(){
switch mode{
case .listing:
ApiManager.firstNetworkCall(){ (json, error) in ...
setupModel()
}
case .pages(let id):
ApiManager.secondNetworkCall(id : id){ (json, error) in ...
setupModel()
}
}
}
}
Both of these will update the models array with different quantity of data.
What I have right now:
var controller : Controller!
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
try super.setUpWithError()
controller = Controller()
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
controller = nil
try super.tearDownWithError()
}
func testDefaultListingMode() throws {
switch controller.mode{
case .listing:
XCTAssertTrue(true)
default:
XCTAssertFalse(false)
}
}
func testAPISetup() throws {
controller.fetchInfo()
//now what?
}
This checks if the mode is correct but I am trying to go one step further and check if the correct number of items is setup depending on the mode. And want to call the fetchInfo() method directly from the XCTestCase and just validate the model count.
All the tutorials and guides I have seen just talk about faking the behaviour with a URLSession. But the API call is dependent on the mode that happens as an internal check inside the fetchInfo method and is the only method exposed to other classes. I would simply like to test the method (in case something breaks inside that method causing a bug).
How do I go about doing that? I can't figure out how to complete the testAPISetup() method.
What I had for networking:
class NetworkingManager{
static var alamoFireManager = Session.default
static func POST(...., completion : ()->()) {
sendRequest(....., completion : completion)
}
private static func sendRequest(...., completion : ()->()) {
let request = alamoFireManager.request(.....)
request.responseJSON{
completion()
}
}
}
class APIManager{
static func firstNetworkCall(completion : ()->()){
NetworkingManager.POST(..., completion : completion)
}
}
I had to change the above and removed all mentions of static and singletons. I decided to go ahead with using class inheritance. I tried to avoid it and use protocols but it frankly was quite easier to use classes!
class NetworkingManager{
private (set) var sessionManager: Session
init(config : URLSessionConfiguration = .default){
config.timeoutIntervalForResource = 8.0
config.timeoutIntervalForRequest = 8.0
self.sessionManager = Session(configuration: config)
}
func request(...) {
//hit alamofire
}
}
class APIManager : NetworkingManager{
override init(config: URLSessionConfiguration = .default) {
super.init(config: config)
}
//other methods
...
}
class Controller{
private let apiManager : APIManager
init(manager : APIManager = APIManager()){
self.apiManager = manager
}
}
And in my test class:
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
try super.setUpWithError()
let config = URLSessionConfiguration.ephemeral
apiManager = APIManager(config : config)
controller = Controller(manager : apiManager)
}
func testApiCalled() throws{
controller.fetchNecessaryInfo()
//had to add one second delay as alamofire adds the request on another queue. Wasn't able to put it on main queue.
sleep(1)
let promise = expectation(description: "Check request called")
apiManager.sessionManager.session.getAllTasks { (taskArray) in
if taskArray.count > 1{
XCTFail("Multiple requests when there should be only one")
}
if let task = taskArray.first, let request = task.currentRequest{
if let string = request.url?.absoluteString{
XCTAssert(...)
}else{
XCTFail("Incorrect URL")
}
}else{
XCTFail("Somehow no task exists. So this is an error")
}
promise.fulfill()
}
wait(for: [promise], timeout: 1.0)
}
I couldn't figure out any other way without having to instantiate an object for APIManager, so had to refactor!

Memory leak during JSON Initialisation using SwiftyJSON

First of all, I am new to instrument and memory graph. Was trying to check whether my app is having memory leak then I bump into this issue. It is a collection of class having MVVM architecture that does the functions below:
View Controller trigger a function in the View Model
View Model ask the networking class to retrieve JSON from web api
Networking class successfully retrieve data from the web api, it use SwiftyJson to convert the dictionary into JSON object and memory leak occur
The leak happen in the init function of SwiftyJSON aka JSON()/JSON.init()
The code base is a mixture of objective-c and swift but the classes that has memory leak are written in swift
I have done a little bit of research, below are my finding:
Create a new project and copy the classes and attempt to recreate the issue but no memory leak occur
This issue only occur when closure is called and return the result back to the caller
This leak also happen on other places that having similar code structure; Calling closure -> closure make an api call -> receive result -> convert into JSON -> Memory leak. Another case is getting remote config using Firebase SDK. In more detail, the function is FIRRemoteConfig.fetch
Enough of introduction, code and screenshot of memory graph and instrument as below, (The code class is pretty big so I only attach the most important part):
View Controller Class
#objc final class HomeVC: UIViewController {
internal lazy var viewModel = { return HomeVM() }()
override func viewDidLoad() {
initVM()
}
private func initVM() {
viewModel.reloadPersonalization.addObserver(onChange: {[weak self] (reload) in
if reload {
self?.collectionView.reloadSections(IndexSet(integer: HomeSection.personalization.rawValue))
}
})
}
}
View Model Class
import SwiftyJSON
class HomeVM {
//MARK:- Observable
let reloadPersonalization = Observable<Bool>(value: false)
let isLoading = Observable<Bool>(value: false)
//MARK:- Properties
var personalizations = Personalization()
//MARK:- Initialization
init() {
}
//MARK:- Functions
public func fetchPersonalization() {
BaseApi.requestAPIService(apiService: MainMallService.showPersonalization, resultReceive: {
}, success: {[weak self] (json) in
self?.personalizations.updateProductList(json: json)
self?.reloadPersonalization.value = true
}, successWithError: {[weak self] (errorMsg) in
self?.reloadPersonalization.value = false
}, fail: {[weak self] (errorMsg) in
self?.reloadPersonalization.value = false
})
}
}
Networking Class
import Moya
import SwiftyJSON
import Alamofire
class BaseApi {
//MARK: Properties
//MARK: Functions
open class func requestAPIService(apiService: TargetType, resultReceive: #escaping () -> Void ,success: #escaping (_ jsonResponse: JSON) -> (), successWithError: #escaping (_ error: String) -> (), fail: #escaping (_ errorMsg: String) -> ()) {
let provider = MoyaProvider<MultiTarget>(manager:NetworkManager.shared ,plugins: [NetworkLoggerPlugin(verbose: true)])
let target = MultiTarget(apiService)
provider.request(target) { result in
resultReceive()
switch result {
case let .success(moyaResponse):
let data = moyaResponse.data
let json = JSON(data)
let apiStatus = ApiStatus.initWithJson(json: json["status"])
switch apiStatus.status {
case .Success:
if json["response"].exists() {
let jsonResponse = json["response"]
success(jsonResponse)
}
case .Failed:
successWithError(StringConst.Error.internalError)
case .Unkown:
fail(StringConst.Error.unknownError)
}
case let .failure(error):
var errorMsg = ""
if error._code == NSURLErrorTimedOut {
errorMsg = StringConst.Connection.timeout
} else if error._code == NSURLErrorNotConnectedToInternet {
errorMsg = StringConst.Connection.noConnection
} else {
errorMsg = StringConst.Error.unknownError
}
fail(errorMsg)
}
}
}
}
Memory leak occur on this line code, both memory graph and instrument is pointing to this line, again; Leak only occur when success block is called, other block like error and successWithError don't trigger the memory leak.
let json = JSON(data)
Appreciate your help !

Swift URLSession completion Handler

I am trying to make an API call in my Swift project. I just started implementing it and i am trying to return a Swift Dictionary from the call.
But I think i am doing something wrong with the completion handler!
I am not able to get the returning values out of my API call.
import UIKit
import WebKit
import SafariServices
import Foundation
var backendURLs = [String : String]()
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
func getBackendURLs(completion: #escaping (NSArray) -> ()) {
let backend = URL(string: "http://example.com")
var json: NSArray!
let task = URLSession.shared.dataTask(with: backend! as URL) { data, response, error in
guard let data = data, error == nil else { return }
do {
json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? NSArray
completion(json)
} catch {
#if DEBUG
print("Backend API call failed")
#endif
}
}
task.resume()
}
func extractJSON(JSON : NSArray) -> [String : String] {
var URLs = [String : String]()
for i in (0...JSON.count-1) {
if let item = JSON[i] as? [String: String] {
URLs[item["Name"]! ] = item["URL"]!
}
}
return URLs
}
}
The first print() statements gives me the correct value, but the second is "nil".
Does anyone have a suggestion on what i am doing wrong?
Technically #lubilis has answered but I couldn't fit this inside a comment so please bear with me.
Here's your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
}
print(backendURLs)
}
What will happen is the following:
viewDidLoad is called, backendURLs is nil
you call getBackendURLs, which starts on another thread in the background somewhere.
immediately after that your code continues to the outer print(backendURLs), which prints nil as backendURLs is still nil because your callback has not been called yet as getBackendURLs is still working on another thread.
At some later point your getBackendURLs finishes retrieving data and parsing and executes this line completion(json)
now your callback is executed with the array and your inner print(backendURLs) is called...and backendURLs now has a value.
To solve your problem you need to refresh your data inside your callback method.
If it is a UITableView you could do a reloadData() call, or maybe write a method that handles updating the UI for you. The important part is that you update the UI inside your callback, because you don't have valid values until then.
Update
In your comments to this answer you say:
i need to access the variable backendURLs right after the completionHandler
To do that you could make a new method:
func performWhateverYouNeedToDoAfterCallbackHasCompleted() {
//Now you know that backendURLs has been updated and can work with them
print(backendURLs)
//do what you must
}
In the callback you then send to your self.getBackendURLs, you invoke that method, and if you want to be sure that it happens on the main thread you do as you have figured out already:
self.getBackendURLs { json in
backendURLs = self.extractJSON(JSON: json)
print(backendURLs)
DispatchQueue.main.async {
self.performWhateverYouNeedToDoAfterCallbackHasCompleted()
}
}
Now your method is called after the callback has completed.
As your getBackendURLs is an asynchronous method you can not know when it has completed and therefore you cannot expect values you get from getBackedURLs to be ready straight after calling getBackendURLs, they are not ready until getBackendURLs has actually finished and is ready to call its callback method.
Hope that makes sense.

Writing API requests with completion blocks using Swift generics

I am experimenting with generics in Swift and I am attempting to push it to its limits.
In my application I have a super simple API wrapper around Alamofire. The structure is like so:
API -> Request -> Alamofire request
Here is some generic code that I threw into a playground to test some concepts. Here is what I have so far:
protocol SomeProtocol {
var cheese: String { get }
init()
}
class Something: SomeProtocol {
required init() { }
var cheese: String {
return "wiz"
}
}
class API {
class func performRequest<T: SomeProtocol>(completion: (T?, NSError) -> Void) {
// This code is irrelevant, just satisfying the completion param
let test = T()
let error = NSError(domain: "Pizza", code: 1, userInfo: nil)
completion(test, error)
}
}
func test() {
API.performRequest<Something> { item, error in
}
}
Calling the function gives the error:
"Cannot explicitly specialize a generic function"
****** UPDATE ******
As per the answer below, removing the typical <> generic type specifier and instead adding the expected type to the completion params solves the issue. Just a quick example:
func test() {
API.performRequest { (item: Something?, error) in
}
}
Additionally, I have discovered that making the API wrapper class a generic class solves the issue like so:
protocol SomeProtocol {
var pizza: String { get }
}
class SomeObject: SomeProtocol {
var pizza: String { return "pie" }
}
class API<T: SomeProtocol> {
class func performRequest(completion: (T?, NSError?) -> Void) {
}
}
func test() {
API<SomeObject>.performRequest { item, error in
// Do something with item, which has a type of SomeObject
}
}
Either way, the end goal is accomplished. We have a single generic method that will perform a set of tasks and return, via completion closure, the object based on the type passed in with each use.
The way generics work is they allow a function to use unspecialized variables inside of its implementation. One can add functionality to these variables by specifying that the variables must conform to a given protocol (this is done within the declaration). The result is a function that can be used as a template for many types. However, when the function is called in the code itself, the compiler must be able to specialize and apply types to the generics.
In your code above, try replacing
func test() {
API.performRequest<Something> { item, error in
}
}
with
func test() {
API.performRequest { (item: Something?, error) in
}
}
this lets the compiler know which type it must apply to the function without explicitly specifying. The error message you received should now make more sense.
Here is what i did using alamofire and alamofire object mapper:
Step 1: Create modal classes that conforms to Mappable protocols.
class StoreListingModal: Mappable {
var store: [StoreModal]?
var status: String?
required init?(_ map: Map){
}
func mapping(map: Map) {
store <- map["result"]
status <- map["status"]
}
}
Step 2: Create a fetch request using the generic types:
func getDataFromNetwork<T:Mappable>(urlString: String, completion: (T?, NSError?) -> Void) {
Alamofire.request(.GET, urlString).responseObject { (response: Response<T, NSError>) in
guard response.result.isSuccess else{
print("Error while fetching: \(response.result.error)")
completion(nil, response.result.error)
return
}
if let responseObject = response.result.value{
print(responseObject)
completion(responseObject, nil)
}
}
}
Step 3: Now all you need is to call this fetch function. This can be done like this:
self.getDataFromNetwork("your url string") { (userResponse:StoreListingModal?, error) in
}
You will not only get your response object but it will also be mapped to your modal class.

Combining Alamofire and RxSwift

I have this custom implementation of Alamofire:
protocol HTTPProtocol: class {
typealias RequestType
typealias RespondType
func doRequest(requestData: RequestType) -> Self
func completionHandler(block:(Result<RespondType, NSError>) -> Void) -> Self
}
//example of a request:
locationInfo
//Make a request
.doRequest(HTTPLocationInfo.RequestType(coordinate: $0))
//Call back when request finished
.completionHandler { result in
switch result {
case .Success(let info): self.locationInfoRequestSuccess(info)
case .Failure(let error): self.locationInfoRequestFailed(error)
}
}
I want to apply MVVM and RxSwift into my project. However, I can't find a proper way to do this.
What I want to achieve is a ViewModel and a ViewController that can do these things:
class ViewController {
func googleMapDelegate(mapMoveToCoordinate: CLLocationCoordinate2D) {
// Step 1: set new value on `viewModel.newCoordinate` and make a request
}
func handleViewModelCallBack(resultParam: ...*something*) {
// Step 3: subscribeOn `viewModel.locationInfoResult` and do things.
}
}
class ViewModel {
//Result if a wrapper object of Alamofire.
typealias LocationInfoResult = (Result<LocationInfo.Respond, NSError>) -> Void
let newCoordinate = Variable<CLLocationCoordinate2D>(kInvalidCoordinate)
let locationInfoResult: Observable<LocationInfoResult>
init() {
// Step 2: on newCoordinate change, from step 1, request Location Info
// I could not find a solution at this step
// how to make a `completionHandler` set its result on `locationInfoResult`
}
}
Any help is deeply appreciated. Thank you.
You can use RxAlamofire as #Gus said in the comment. But if you are using any library that doesn't support Rx extensions by default you may need to do the conversion by hand.
So for the above code snippet, you can create an observable from the callback handler you had implemented
func getResultsObservable() -> Observable<Result> {
return Observable.create{ (observer) -> Disposable in
locationInfo
//Make a request
.doRequest( .... )
//Call back when request finished
.completionHandler { result in
switch result {
case .Success(let info): observer.on(Event.Next(info))
case .Failure(let error): observer.on(Event.Error(NetworkError()))
}
}
return Disposables.create {
// You can do some cleaning here
}
}
}
Callback handlers are implementation to observer pattern, so mapping it to a custom Observable is a straight forward operation.
A good practice is to cancel the network request in case of disposing, for example this is a complete disposable Post request:
return Observable<Result>.create { (observer) -> Disposable in
let requestReference = Alamofire.request("request url",
method: .post,
parameters: ["par1" : val1, "par2" : val2])
.validate()
.responseJSON { (response) in
switch response.result{
case .success:
observer.onNext(response.map{...})
observer.onCompleted()
case .failure:
observer.onError(NetworkError(message: response.error!.localizedDescription))
}
}
return Disposables.create(with: {
requestReference.cancel()
})
Note: before swift 3 Disposables.create() is replaced with NopDisposable.instance
It doesn't seem like you need to subscribe to newCoordinate so I would just make that a request func.
Then, using the info you get back from Alamofire, just set the value on the locationInfoResult and you will get the new result in the ViewController
class ViewController: UIViewController {
func viewDidLoad() {
super.viewDidLoad()
//subscribe to info changes
viewModel.locationInfoResult
.subscribeNext { info in
//do something with info...
}
}
func googleMapDelegate(mapMoveToCoordinate: CLLocationCoordinate2D) {
viewModel.requestLocationInfo(mapMoveToCoordinate)
}
}
class ViewModel {
let locationInfoResult: Variable<LocationInfoResult?>(nil)
init() {
}
func requestLocationInfo(location: CLLocationCoordinate2D) {
//do Alamofire stuff to get info
//update with the result
locationInfoResult.value = //value from Alamofire
}
}

Resources