I'd like to implement protocol methods in the other method parameters.
First, I defined a protocol containing a method,
protocol MyProtocol {
func myProtocolFunc();
}
and I made a method taking the protocol as a parameter.
func myFunc(myProtocol : MyProtocol) {
. . .
}
and when I using this method, I want to override protocolFunc().
myFunc( . . . )
Where should I override protocolFunc() in my myFunc() method?
p.s. In Kotlin, I made that by doing like this.
interface MyProtocol {
fun myProtocolFunc();
}
fun myFunc(myProtocol : MyProtocol) {
. . .
}
myFunc(object : MyProtocol {
override fun myProtocolFunc() {
. . .
}
})
I want to do same thing in swift code.
========================================
Edit:
Actually, I'm planning to make Http Request class.
After getting some data from web server, I'd like to do some work in ViewController class.
Because Http Request runs on thread, while fetching some data from web server, next code regarding the data should wait.
Here is my Http Request class,
class HttpConnector {
static let basicURL = "http://******"
static func getData(url : String, parameters : String, listener : UIModifyAvailableListener) {
if let fullURL = URL(string : "\(basicURL)\(url)") {
var request = URLRequest(url : fullURL)
request.httpMethod = "POST"
request.httpBody = parameters.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("error = \(error!)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response!)")
}
if let result = String(data: data, encoding: .utf8) {
listener.taskCompleted(result: result)
}
})
task.resume()
}
}
}
protocol UIModifyAvailableListener {
func taskCompleted(result : String)
}
and this class might be called in ViewController like this
HttpConnector.getData("my_url", "my_parameter", [[some codes regarding UIModifyAvailableListener protocol]])
If it can't be done in swift, I want to get some alternatives.
In your protocol, change the function to a variable of a function.
protocol UIModifyAvailableListener {
var taskCompleted : ((result : String) -> Void)? {get set}
}
Then in your HttpConnector class that implements UIModifyAvailableListener, add this:
var taskCompleted : ((result : String) -> Void)?
Within HttpConnector class's method(s), you can call taskCompleted like so:
self.taskCompleted?(result)
Then in the calling code that wants to be called back with taskCompleted(), simply set the var:
myHttpConnector.taskCompleted =
{
print("Done!") // note if you want to reference self here, you'll probably want to use weakSelf/strongSelf to avoid a memory leak.
}
BTW, this is an important pattern if you're doing MVVM, so that your ViewModel can call back into the ViewController. Since the ViewModel would never have a reference to its ViewController, all it can do is have callback properties that the ViewController can set with the closure blocks of code it wants called. And by using a protocol the ViewModel can be mocked when doing unit tests against the ViewController. :)
Related
Here is what I am trying to do:
var usernameCheckerResponse : String = ""
//This IBAction is a UITextfield that sends post request when editing is finshed.
#IBAction func usernameChecker(_ sender: Any) {
// perform post request with URLSession
// post request returns url response from URLSession
// the value of this response is either 'usernameExists' or 'usernameAvailable'
// usernameCheckerResponse = String(describing : response)
}
//use modified usernameCheckerResponse variable outside the IBAction function.
//For example like this:
func UsernameExists () -> Bool {
if(usernameCheckerResponse == "usernameExists"){
return true
} else { return false }
}
I am aware that an IBAction will only return a void, so is there anyway around this problem?
Any help and/or advice will be greatly appreciated.
Yes absolutely. Here is an example,
var usernameCheckerResponse : String = ""
//This IBAction is a UITextfield that sends post request when editing is finshed.
#IBAction func usernameChecker(_ sender: Any) {
//post request
// post request returns url response
// usernameCheckerResponse = String(describing : response)
}
//use modified usernameCheckerResponse variable outside the IBAction function.
func accessVariable() {
print("\(usernameCheckerResponse")
}
Keep in mind that the trick here is to access the variable when it has changed. To do that you need to pick some sort of way to keep track of that. Delegation is probably the most standard way to do that. See this. You would have to be more specific as to why you want the variable changed, because I would need to know what is using it (delegation required that you have are very specific on who is participating).
I would like to also be more specific with how delegation works. You would specify when the 'accessVariable()' function is called, in the place where you want the modified variable (this would always be between two different classes or structures). Keep in mind that you do not need to use delegation if you are just trying to share the variable in the same class. Calling the function 'accessVariable()' will suffice. However if this is the case where you want something to happen in the same class, but you really want to control in what order the functions finish then you need to use callbacks.
BTW Leo, doing it that way will make the app crash...
In general, you should think of IBAction functions as
connection points for controls like buttons etc.
You would never call it yourself.
If you need to do that, make another function
and have the IBAction function call that.
Since you are using URLSession to fetch the data from an external
source, you will need to be aware that this does not happen synchronously.
Send the call to your API and have the completion handler get called
when data is returned.
All of this code goes into your ViewController
// Set up a reusable session with appropriate timeouts
internal static var session: URLSession {
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 6.0
sessionConfig.timeoutIntervalForResource = 18.0
return URLSession( configuration: sessionConfig )
}
// Create an httpPost function with a completion handler
// Completion handler takes :
// success: Bool true/false if things worked or did not work
// value: String string value returned or "" for failures
// error: Error? the error object if there was one else nil
func httpPost(_ apiPath: String, params: [String: String], completion:#escaping (Bool, String, Error?) -> Void) {
// Create POST request
if let requestURL = URL( string: apiPath ) {
print("requestUrl \(apiPath)")
// Create POST request
var request = URLRequest( url: requestURL )
request.httpMethod = "POST"
var postVars : [String : String ] = params
var postString = postVars.toHttpArgString()
request.httpBody = postString.data( using: String.Encoding.utf8, allowLossyConversion: true )
let sendTask = ViewController.session.dataTask( with: request) {
(data, response, error) in
if let nserror = error as NSError? {
// There was an error
// Log it or whatever
completion(false, "", error)
return
}
// Here you handle getting data into a suitable format
let resultString = "whatever you got from api call"
// Send it back to the completion block
completion(true, resultString, nil)
}
sendTask.resume()
}
}
// I assume you have a text field with the user name you want to try
#IBOutlet weak var usernameToCheck : UITextField!
#IBAction func usernameChecker(_ sender: Any) {
guard let username = usernameToCheck.text else {
// This is unlikely to happen but just in case.
return
}
httpPost("https://someapicall", params: ["username" : username] ) {
(success, value, error) in
// This code gets called when the http request returns data.
// This does not happen on the main thread.
if success {
if value == "usernameExists" {
// User name already exists. Choose a different one.
DispatchQueue.main.async {
// put code here if you need to do anything to the UI, like alerts, screen transitions etc.
}
}
else if value == "usernameAvailable" {
// You can use this user name
DispatchQueue.main.async {
// put code here if you need to do anything to the UI, like alerts, screen transitions etc.
}
}
else {
// Unexpected response from server
}
}
else {
// Something did not work
// alert "Unable to connect to server"
}
}
}
To make this code work you will need this:
// Syntatic sugar to convert [String:String] to http arg string
protocol ArgType {}
extension String: ArgType {}
extension Dictionary where Key: ArgType, Value: ArgType {
// Implement using a loop
func toHttpArgString() -> String {
var r = String()
for (n, v) in self {
if !r.isEmpty { r += "&" }
r += "\(n)=\(v)"
}
return r
}
}
I want to use my custom delegate methods in Alamofire's response callback, like below:
func startDownloadSegment() {
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let filePath = self.generateFilePath()
return (filePath, [.createIntermediateDirectories])
}
// 1
print(self.delegate)
Alamofire.download(downloadURL, to: destination).response { response in
// 2
print(self.delegate)
if response.error == nil {
self.delegate?.segmentDownloadSucceeded(with: self)
} else {
self.delegate?.segmentDownloadFailed(with: self)
}
}
}
As you can see, No.1 print(self.delegate) returns the delegator I set. But No.2 always returns nil so delegate method like downloadSucceeded(with:) cannot be called.
Thank you.
I find the problem. The problem is I set the delegate as
weak var delegate
But as in Alamofire response callback, I should omit 'weak' keyword to get it done.
I have the following function in a class in my program:
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
In the init() for the class, I attempt to call it like this:
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
self.XMLString = xmlData
}
This compiles without errors, but after init() is executed, my class's self.XMLString is still nil (shown both by the Xcode debugger and by my program crashing due to the nil value later on). I see no reason why this shouldn't work. Can anyone see what I am missing?
You shouldn't be making internet calls in the initializer of a class. You will reach the return of the init method before you go into the completion of your internet call, which means it is possible that the class will be initialized with a nil value for the variable you are trying to set.
Preferably, you would have another class such as an API Client or Data Source or View Controller with those methods in it. I am not sure what your class with the init() method is called, but lets say it is called Trips.
class Trips: NSObject {
var xmlString: String
init(withString xml: String) {
xmlString = xml
}
}
Then one option is to put the other code in whatever class you are referencing this object in.
I'm gonna use a view controller as an example because I don't really know what you are working with since you only showed two methods.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//setting some fake variables as an example
let stop = "Stop"
let route = 3
//just going to put that method call here for now
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
//initialize Trip object with our response string
let trip = Trip(withString: xmlData)
}
}
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
}
If you want to be able to initialize the class without requiring setting the xmlString variable, you can still do the same thing.
Change the Trips class init() method to whatever you need it to be and set var xmlString = "" or make it optional: var xmlString: String?.
Initialize the class wherever you need it initialized, then in the completion of getXMLForTrips, do trip.xmlString = xmlData.
In app delegate, after to get the coordinates of the user with core location, I want to make two api calls. One is to my server, to get a slug of the city name in which we are. The call is async so I want to load all the content into a global variable array before make the second call to google maps api, to get the city name from google, also with an async call. And finally after I have loaded all the google data, I want to compare the two arrays, with the city names to find a coincidence. To do that, I need the first two operation to have ended. For this I'm using closures, to ensure all the data is loaded before the next operation start. But when I launch my program, it doesn't find any coincidence between the two arrays and when I set breakpoints, I see the second array (google) is loaded after the comparison is made, which is very frustrating because I've set a lot of closures, and at this stage I'm not able to find the source of my issue. Any help would be appreciated.
this is app delegate:
let locationManager = CLLocationManager()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
//Language detection
let pre = NSLocale.preferredLanguages()[0]
print("language= \(pre)")
//Core Location
// Ask for Authorisation from the User.
self.locationManager.requestAlwaysAuthorization()
//Clore Location
// For use in foreground
self.locationManager.requestWhenInUseAuthorization()
if CLLocationManager.locationServicesEnabled() {
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
locationManager.startUpdatingLocation()
//Load cities slug via api call
let apiCall : webApi = webApi()
apiCall.loadCitySlugs(){(success) in
//Slug loaded in background
//Call google api to compare the slug
apiCall.loadGoogleContent(){(success) in //this function is called after compareGoogleAndApiSlugs()
apiCall.compareGoogleAndApiSlugs() //this one called before
}
}
}
return true
}
This is my global variables swift file:
import Foundation
import CoreLocation
let weatherApiKey : String = "" //weather api key
var globWeatherTemp : String = ""
var globWeatherIcon : String = ""
var globCity : String = ""
var globCountry : String = ""
let googleMapsApiKey : String = ""
let googlePlacesApiKey : String = ""
var TableData:Array< String > = Array < String >()
var nsDict = []
var locValue : CLLocationCoordinate2D = CLLocationCoordinate2D()
typealias SuccessClosure = (data: String?) -> (Void)
typealias FinishedDownload = () -> ()
typealias complHandlerAsyncCall = (success : Bool) -> Void
typealias complHandlerCitySlug = (success:Bool) -> Void
typealias complHandlerAllShops = (success:Bool) -> Void
typealias googleCompareSlugs = (success:Bool) -> Void
var flagCitySlug : Bool?
var flagAsyncCall : Bool?
var flagAllShops : Bool?
var values : [JsonArrayValues] = []
var citySlug : [SlugArrayValues] = []
var asyncJson : NSMutableArray = []
let googleJson : GoogleApiJson = GoogleApiJson()
this is the first function called in app delegate, which make a call to my server to load the city slug:
func loadCitySlugs(completed: complHandlerCitySlug){
//Configure Url
self.setApiUrlToGetAllSlugs()
//Do Async call
asyncCall(userApiCallUrl){(success)in
//Reset Url Async call and Params
self.resetUrlApi()
//parse json
self.parseSlugJson(asyncJson)
flagCitySlug = true
completed(success: flagCitySlug!)
}
}
This is the second function, it load google content but it's called after compareGoogleAndApiSlugs() and it's supposed to be called before...
/*
Parse a returned Json value from an Async call with google maps api Url
*/
func loadGoogleContent(completed : complHandlerAsyncCall){
//Url api
setGoogleApiUrl()
//Load google content
googleAsyncCall(userApiCallUrl){(success) in
//Reset API URL
self.resetUrlApi()
}
flagAsyncCall = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
And finally the async calls, there are two but they are almost the same code:
/**
Simple async call.
*/
func asyncCall(url : String, completed : complHandlerAsyncCall)/* -> AnyObject*/{
//Set async call params
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = "POST"
request.HTTPBody = postParam.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
guard error == nil && data != nil else {
// check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200 {
// check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
let responseString = NSString(data: data!, encoding: NSUTF8StringEncoding)
asyncJson = responseString!.parseJSONString! as! NSMutableArray
flagAsyncCall = true // true if download succeed,false otherwise
completed(success: flagAsyncCall!)
}
task.resume()
}
If anyone can see the issue or throw some light it would be very appreciated.
The problem is this function:
func loadGoogleContent(completed : complHandlerAsyncCall){
setGoogleApiUrl()
googleAsyncCall(userApiCallUrl){(success) in
self.resetUrlApi()
}
flagAsyncCall = true
completed(success: flagAsyncCall!) //THIS LINE IS CALLED OUTSIDE THE googleAsyncCall..
}
The above completion block is called outside of the googleAsyncCall block.
The code should be:
func loadGoogleContent(completed : complHandlerAsyncCall){
setGoogleApiUrl()
googleAsyncCall(userApiCallUrl){(success) in
self.resetUrlApi()
flagAsyncCall = true
completed(success: flagAsyncCall!)
}
}
Btw.. your global variables are NOT atomic.. so be careful.
I'm building an app with MVC Model.
I use lazy load technical to fill up a variable. (Model)
And this variable is being by one UIViewController (Controller)
But i don't know how to reload or trigger the view controller when the model action is finished. Here is my code
Model (lazy load data)
class func allQuotes() -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
}
return quotes
}
And the part of view controller
class Index:
UIViewController,UICollectionViewDelegate,UICollectionViewDataSource {
var quotes = IndexQuotes.allQuotes()
var collectionView:UICollectionView!
override func viewDidLoad() {
This is really serious question, i'm confusing what technic will be used to full fill my purpose?
Since Alamofire works asynchronously you need a completion block to return the data after being received
class func allQuotes(completion: ([IndexQuotes]) -> Void)
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for photoDict in (response.result.value as! [NSDictionary]) {
let photo = IndexQuotes(dictionary: photoDict)
quotes.append(photo)
}
}
completion(quotes)
}
}
Or a bit "Swiftier"
... {
let allPhotos = response.result.value as! [NSDictionary]
quotes = allPhotos.map {IndexQuotes(dictionary: $0)}
}
I'd recommend also to use native Swift collection types rather than NSArray and NSDictionary
In viewDidLoad in your view controller call allQuotes and reload the table view in the completion block on the main thread.
The indexQuotes property starting with a lowercase letter is assumed to be the data source array of the table view
var indexQuotes = [IndexQuotes]()
override func viewDidLoad() {
super.viewDidLoad()
IndexQuotes.allQuotes { (quotes) in
self.indexQuotes = quotes
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
}
First of all call the function from inside the viewdidLoad. Secondly use blocks or delegation to pass the control back to ViewController. I would prefer the blocks approch. You can have completion and failure blocks. In completions block you can reload the views and on failure you can use alertcontroller or do nothing.
You can see AFNetworking as an example for blocks.
It's async action, just use a callback here:
class func allQuotes(callback: () -> Void) -> [IndexQuotes]
{
var quotes = [IndexQuotes]()
Alamofire.request(.GET, api_indexquotes).responseJSON { response in
if response.result.isSuccess && response.result.value != nil {
for i in (response.result.value as! [AnyObject]) {
let photo = IndexQuotes(dictionary: i as! NSDictionary)
quotes.append(photo)
}
}
callback()
}
return quotes
}
In your UIViewController:
var quotes = IndexQuotes.allQuotes() {
self.update()
}
var collectionView:UICollectionView!
override func viewDidLoad() {
update()
}
private func update() {
// Update collection view or whatever.
}
Actually, I strongly don't recommend to use class functions in this case (and many other cases too), it's not scalable and difficult to maintain after some time.