I was just curious as to how I would approach this. If I had a function, and I wanted something to happen when it was fully executed, how would I add this into the function? Thanks
Say you have a download function to download a file from network, and want to be notified when download task has finished.
typealias CompletionHandler = (success:Bool) -> Void
func downloadFileFromURL(url: NSURL,completionHandler: CompletionHandler) {
// download code.
let flag = true // true if download succeed,false otherwise
completionHandler(success: flag)
}
// How to use it.
downloadFileFromURL(NSURL(string: "url_str")!, { (success) -> Void in
// When download completes,control flow goes here.
if success {
// download success
} else {
// download fail
}
})
I had trouble understanding the answers so I'm assuming any other beginner like myself might have the same problem as me.
My solution does the same as the top answer but hopefully a little more clear and easy to understand for beginners or people just having trouble understanding in general.
To create a function with a completion handler
func yourFunctionName(finished: () -> Void) {
print("Doing something!")
finished()
}
to use the function
override func viewDidLoad() {
yourFunctionName {
//do something here after running your function
print("Tada!!!!")
}
}
Your output will be
Doing something
Tada!!!
Simple Example:
func method(arg: Bool, completion: (Bool) -> ()) {
print("First line of code executed")
// do stuff here to determine what you want to "send back".
// we are just sending the Boolean value that was sent in "back"
completion(arg)
}
How to use it:
method(arg: true, completion: { (success) -> Void in
print("Second line of code executed")
if success { // this will be equal to whatever value is set in this method call
print("true")
} else {
print("false")
}
})
Swift 5.0 + , Simple and Short
example:
Style 1
func methodName(completionBlock: () -> Void) {
print("block_Completion")
completionBlock()
}
Style 2
func methodName(completionBlock: () -> ()) {
print("block_Completion")
completionBlock()
}
Use:
override func viewDidLoad() {
super.viewDidLoad()
methodName {
print("Doing something after Block_Completion!!")
}
}
Output
block_Completion
Doing something after Block_Completion!!
We can use Closures for this purpose. Try the following
func loadHealthCareList(completionClosure: (indexes: NSMutableArray)-> ()) {
//some code here
completionClosure(indexes: list)
}
At some point we can call this function as given below.
healthIndexManager.loadHealthCareList { (indexes) -> () in
print(indexes)
}
Please refer the following link for more information regarding Closures.
https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html
In addition to above : Trailing closure can be used .
downloadFileFromURL(NSURL(string: "url_str")!) { (success) -> Void in
// When download completes,control flow goes here.
if success {
// download success
} else {
// download fail
}
}
In case you need result values in your completion handler, it's a good idea to include labels proceeded with underscores, like this...
func getAccountID(account: String, completionHandler: (_ id: String?, _ error: Error?) -> ()) {
// Do something and return values in the completion handler
completionHandler("123", nil)
}
...because when you type this function, Xcode will automatically fill in the result value labels, like this:
getAccountID(account: inputField.stringValue) { id, error in
}
I'm a little confused about custom made completion handlers. In your example:
Say you have a download function to download a file from network,and want to be notified when download task has finished.
typealias CompletionHandler = (success:Bool) -> Void
func downloadFileFromURL(url: NSURL,completionHandler: CompletionHandler) {
// download code.
let flag = true // true if download succeed,false otherwise
completionHandler(success: flag)
}
Your // download code will still be ran asynchronously. Why wouldn't the code go straight to your let flag = true and completion Handler(success: flag) without waiting for your download code to be finished?
//MARK: - Define
typealias Completion = (_ success:Bool) -> Void
//MARK: - Create
func Call(url: NSURL, Completion: Completion) {
Completion(true)
}
//MARK: - Use
Call(url: NSURL(string: "http://")!, Completion: { (success) -> Void in
if success {
//TRUE
} else {
//FALSE
}
})
Related
In my SwiftUI app, I currently have a PageViewController implemented using UIKit. It follows the traditional SwiftUI - UIKit implementation outlined in Apple's SwiftUI UIKit integration tutorials.
I have the data that populates a UIViewController, within the controllers array that is passed to the PageViewController, provided by an #Environment variable. In a different screen in the app, you can issue an action that causes the Environment object to update, triggering a re-render of a ViewController that lives within PageViewController.
This re-render, however, causes an issue as the ViewController is re-made with a new identifier and so the index of the viewController cannot be found in the parent.controller array within the Class Coordinator pageViewController function. This causes the index to default to nil and disables any swiping on the updated viewController. I am still able to navigate between viewControllers using the page control dots but I would like to identify how I can update the parent.controller array to include the new ViewController and to discard the old one.
After hours of searching, debugging and tinkering around I've not been able to find a way how I can reset the parents controller array with the new ViewController replacing the old view which has been discarded. Below is the code for the PageViewController.
import SwiftUI
import UIKit
struct PageViewController: UIViewControllerRepresentable {
var controllers: [UIViewController]
#Binding var currentPage: Int
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> UIPageViewController {
let pageViewController = UIPageViewController(
transitionStyle: .scroll,
navigationOrientation: .horizontal)
pageViewController.view.backgroundColor = UIColor.clear
pageViewController.dataSource = context.coordinator
pageViewController.delegate = context.coordinator
return pageViewController
}
func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
pageViewController.setViewControllers(
[controllers[currentPage]], direction: .forward, animated: false)
}
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var parent: PageViewController
init(_ pageViewController: PageViewController) {
self.parent = pageViewController
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(parent.controllers)
print(viewController)
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index + 1 == parent.controllers.count {
return parent.controllers.first
}
return parent.controllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed,
let visibleViewController = pageViewController.viewControllers?.first,
let index = parent.controllers.firstIndex(of: visibleViewController) {
parent.currentPage = index
}
}
}
}
After updating the environment state that populates the data for a given ViewController, causing a re-render, the coordinator class can print an array of controllers that include the old ViewController and also the new ViewController in the annotated code below but I have not yet been able to find a reliable way to ensure that the new ViewController effectively replaces the old one.
struct PageViewController: UIViewControllerRepresentable {
...
class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
...
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(parent.controllers)
// prints out array of ViewControllers including old ViewController that has now been
// discarded
// [<_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c93de0>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c94be0>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c96830>,
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd595c976b0>]
print(viewController)
// prints out new ViewController that does not exist within the parent.controllers array
// and hence nil is returned from the guard
// <_TtGC7SwiftUI19UIHostingControllerVS_7AnyView_: 0x7fd593721710>
guard let index = parent.controllers.firstIndex(of: viewController) else {
return nil
}
if index == 0 {
return parent.controllers.last
}
return parent.controllers[index - 1]
}
...
}
Any help or guidance with this issue would be greatly appreciated!
Having the same issue, for few last hours, I found a work around.
It seems that it is a bug. If you look at the UIViewControllerRepresentable cycle, it should go from the Init > Coordinator > makeUIViewController > updateUIViewController.
But if the pages in the PageViewController get updated (our case). The pages get re-created by SwiftUI, great, but the PageViewController get an internal wrong creation cycle going from Init > updateUIViewController. Without makeUIViewController neither Coordinator. A new UIPageViewController is somehow created (How?) Leading to the discrepancy you noticed.
To solve the issue and force a proper recreation of the PageViewController, add .id(UUID) in your page view code, such as :
struct SGPageView<Page: View>: View {
var viewControllers: [UIHostingController<Page>]
#Binding var currentPage:Int
init(_ views: [Page], currentPage:Binding<Int>) {
self.viewControllers = views.map { UIHostingController(rootView: $0) }
self._currentPage = currentPage
}
var body: some View {
PageViewController(controllers: viewControllers, currentPage: $currentPage).id(UUID())
}
}
It will work, but you will notice that the scroll position of the pages are reset. Another bug ;)
think the problem stays in the Coordinator:
when you update controllers in pageviewcontroller, the parent property of Coordinator remains the same (and because it is a struct, all his properties). So you can simply add this line of code in your updateUIViewController method:
context.coordinator.parent = self
also, remember that animation of pageViewController.setViewControllers will occur, so you have to set animated to false, or handle it properly.
There are many other ways to solve this (I wrote the most intuitive solution),
the important thing is to know where the error comes from.
I am trying to get learn how to use AlamoFire and I am having trouble.
My method so far is as follows:
func siteInfo()->String?{
var info:NSDictionary!
var str:String!
Alamofire.request(.GET, MY_API_END_POINT).responseJSON {(request, response, JSON, error) in
info = JSON as NSDictionary
str = info["access_key"] as String
//return str
}
return str
}
This returns nil which is a problem. From what I have read here, this is because the request can take a while so the closure doesn't execute till after the return. The suggested solution of moving the return into the closure does not work for me and the compiler just yells (adding ->String after (request,response,JSON,error) which gives "'String' is not a subtype of void"). Same goes for the other solution provided.
Any ideas? Even some source code that is not related to this problem, that uses AlamoFire, would be helpful.
Thanks!
One way to handle this is to pass a closure (I usually call it a completionHandler) to your siteInfo function and call that inside Alamofire.request's closure:
func siteInfo(completionHandler: (String?, NSError?) -> ()) -> () {
Alamofire.request(.GET, MY_API_END_POINT).responseJSON {
(request, response, JSON, error) in
let info = JSON as? NSDictionary // info will be nil if it's not an NSDictionary
let str = info?["access_key"] as? String // str will be nil if info is nil or the value for "access_key" is not a String
completionHandler(str, error)
}
}
Then call it like this (don't forget error handling):
siteInfo { (str, error) in
if str != nil {
// Use str value
} else {
// Handle error / nil value
}
}
In the comments you asked:
So how would you save the info you collect from the get request if you
can only do stuff inside the closure and not effect objects outside of
the closure? Also, how to keep track to know when the request has
finished?
You can save the result of the get request to an instance variable in your class from inside the closure; there's nothing about the closure stopping you from doing that. What you do from there really depends on, well, what you want to do with that data.
How about an example?
Since it looks like you're getting an access key form that get request, maybe you need that for future requests made in other functions.
In that case, you can do something like this:
Note: Asynchronous programming is a huge topic; way too much to cover here. This is just one example of how you might handle the data you get back from your asynchronous request.
public class Site {
private var _accessKey: String?
private func getAccessKey(completionHandler: (String?, NSError?) -> ()) -> () {
// If we already have an access key, call the completion handler with it immediately
if let accessKey = self._accessKey {
completionHandler(accessKey, nil)
} else { // Otherwise request one
Alamofire.request(.GET, MY_API_END_POINT).responseJSON {
(request, response, JSON, error) in
let info = JSON as? NSDictionary // info will be nil if it's not an NSDictionary
let accessKey = info?["access_key"] as? String // accessKey will be nil if info is nil or the value for "access_key" is not a String
self._accessKey = accessKey
completionHandler(accessKey, error)
}
}
}
public func somethingNeedingAccessKey() {
getAccessKey { (accessKey, error) in
if accessKey != nil {
// Use accessKey however you'd like here
println(accessKey)
} else {
// Handle error / nil accessKey here
}
}
}
}
With that setup, calling somethingNeedingAccessKey() the first time will trigger a request to get the access key. Any calls to somethingNeedingAccessKey() after that will use the value already stored in self._accessKey. If you do the rest of somethingNeedingAccessKey's work inside the closure being passed to getAccessKey, you can be sure that your accessKey will always be valid. If you need another function that needs accessKey, just write it the same way somethingNeedingAccessKey is written.
public func somethingElse() {
getAccessKey { (accessKey, error) in
if accessKey != nil {
// Do something else with accessKey
} else {
// Handle nil accessKey / error here
}
}
}
I would like to add the quick actions of iOS 9 to my app.
I put this code in my app delegate:
import UIKit
enum ShortcutType: String {
case NewScan = "QuickAction.NewScan"
case Settings = "QuickAction.Settings"
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let applicationShortcutUserInfoIconKey = "applicationShortcutUserInfoIconKey"
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
UIViewController.prepareInterstitialAds()
if(UIApplication.instancesRespondToSelector(Selector("registerUserNotificationSettings:"))) {
UIApplication.sharedApplication().registerUserNotificationSettings(UIUserNotificationSettings(forTypes: [.Alert, .Badge, .Sound], categories: nil))
}
// QUICK ACTIONS
var launchedFromShortCut = false
if #available(iOS 9.0, *) {
if let shortcutItem = launchOptions?[UIApplicationLaunchOptionsShortcutItemKey] as? UIApplicationShortcutItem {
launchedFromShortCut = true
handleShortCutItem(shortcutItem)
}
} else {
return true
}
return !launchedFromShortCut
}
/**************** QUICK ACTIONS ****************/
#available(iOS 9.0, *)
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {
let handledShortCutItem = handleShortCutItem(shortcutItem)
completionHandler(handledShortCutItem)
}
#available(iOS 9.0, *)
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
var handled = false
if let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) {
let rootNavigationViewController = window!.rootViewController as? UINavigationController
let rootViewController = rootNavigationViewController?.viewControllers.first as UIViewController?
rootNavigationViewController?.popToRootViewControllerAnimated(false)
switch shortcutType {
case .NewScan:
rootViewController?.performSegueWithIdentifier("goToCamera", sender: nil)
handled = true
case.Settings:
rootViewController?.performSegueWithIdentifier("goToSettings", sender: nil)
handled = true
}
}
return handled
}
}
Now I can make a force touch on my app icon > quick actions will be shown > I select the Quick Action "New Scan" > the app will open and show me the last view, which I have leave.
But the segue will not be execute.
Here is a part of my storyboard:
Explanation:
A: Navigation Controller and initiale Controller
B: ViewController, after a check this will make a segue to navigation Controller C
C: Navigation Controller
D: Table View Controller
E: ViewController
If I select New Scan with quick actions - I would like to show ViewController E.
It appears that you're doing things correctly based on the example code in the documentation. However, you've got a lot of optional chaining in your handleShortCutItem: implementation. Have you used the debugger to verify none of those expression have nil values? Also, from what I can see (although the image is blurry), the root view controller of the first nav controller in that storyboard does not have a segue to E. So I'm not sure how you intend to get there.
I would suggest that you set a breakpoint in your handleShortCutItem: implementation to first validate that the values you're working with are not nil and the code is actually executing. Once you've done this, you can use your storyboard to instantiate the view controls you want and just create an array of them as you want your view controller hierarchy to be in your navigation controller and set the navigation controller's viewControllers property to this array. Again, it's hard to tell exactly what you want from your image, but perhaps something like this:
func handleShortCutItem(shortcutItem: UIApplicationShortcutItem) -> Bool {
guard let shortcutType = ShortcutType.init(rawValue: shortcutItem.type) else {
return false
}
guard let rootNavigationController = window?.rootViewController as? UINavigationController else {
return false
}
guard let rootViewController = rootNavigationController?.viewControllers.first else {
return false
}
guard let storyboard = rootNavigationController.storyboard else {
return false
}
var viewControllers = [rootViewController]
switch shortcutType {
case .NewScan:
// Instantiate the necessary view controllers for this case
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some view controller#>")]
...
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some other view controller#>")]
case.Settings:
// Instantiate the necessary view controllers for this case
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some view controller#>")]
...
viewControllers += [storyboard.instantiateViewControllerWithIdentifier("<#Identifier for some other view controller#>")]
}
// Set the new view controllers array
rootNavigationController.setViewControllers(viewControllers, animated: false)
return true
}
Note: Since you tagged this question with Swift2, I've taken the liberty of adjusting the code to use guard statements.
i tried to save data from an API in my app.
But somehow it fails:
func getApiData() -> NSArray {
let ApiManager = RestApiManager(apiUrl:"http://localhost/api/", apiUsername:"user", apiPassword:"password", apiRequestedResource:"resource")
ApiManager.collectDataFromApi() { responseObject, error in
return responseObject! // Asynchronous data!! :/
}
}
My manager:
func collectDataFromApi(completionHandler: (responseObject: NSDictionary?, error: NSError?) -> ()) {
prepareHttpRequest(completionHandler)
}
func prepareHttpRequest(completionHandler: (responseObject: NSDictionary?, error: NSError?) -> ()) {
Alamofire.request(.GET, "\(self.apiUrl + self.apiRequestedResource)")
.authenticate(user: self.apiUsername, password: self.apiPassword)
.responseJSON { request, response, responseObject, error in
completionHandler(responseObject: responseObject as? NSDictionary, error: error)
}
}
Retrieving data works great!
When i print "responseObject" instead of returning it, it works.
But how to save it...
Anybody knows how i could save my asyncronous retrieved data, so i can work with it?
Greetings and thanks
Your getApiData function cannot return the API data, because when the function returns, the data is not ready yet. This is a concept at the heart of asynchronous programming. Your function starts the network operation and then returns so that program execution can continue while your network operation is working on a different thread.
Instead of returning a value from your getApiData function, you could consider passing a completion handler, like the other asynchronous functions in your example are doing. It would look something like this:
func getApiData(completion: (responseObject: NSDictionary?, error: NSError?) -> ()) {
let ApiManager = RestApiManager(apiUrl:"http://localhost/api/", apiUsername:"user", apiPassword:"password", apiRequestedResource:"resource")
ApiManager.collectDataFromApi() { responseObject, error in
completion(responseObject, error)
}
}
Whatever work you want to do to the data when it is ready, that goes in the completion handler. Instead of using it like this (like I think you might be trying to do):
let data = getApiData()
processData(data)
updateUserInterfaceWithData(data)
you would use it like this:
getApiData() { responseObject, error in
let data = responseObject!
processData(data)
updateUserInterfaceWithData(data)
}
Of course you would want to check for errors and unwrap the optional data in a safe way, but this is the general structure that you want, I think.
i'm trying to create a vary simple game on Swift based on Balloons.playground (that Apple showed on WWDC). There is code that Apple provides:
func fireCannon(cannon: SKNode) {
let balloon = createRandomBalloon()
displayBalloon(balloon, cannon)
fireBalloon(balloon, cannon)
}
let leftBalloonCannon = scene.childNodeWithName("//left_cannon")!
let left = SKAction.runBlock { fireCannon(leftBalloonCannon) }
I do the same (i suppose):
func displayFruit(xPos: CGFloat) -> Void {
let fruit = SKSpriteNode(imageNamed: "Banana")
fruit.size = CGSize(width: 100, height: 100)
fruit.position = CGPoint(x:xPos, y: 800)
fruit.physicsBody = SKPhysicsBody(texture: fruit.texture, size: fruit.size)
fruit.physicsBody?.affectedByGravity = true
self.addChild(fruit)
}
let start = SKAction.runBlock { displayFruit(100) }
This code above i put at override func didMoveToView(view: SKView)
I get an error: Cannot reference a local function with capture from another function
I know there was very the same questions, but solutions there don't seemed helpful for me.
What would be the right way to do this?
Thank you!
This is a local function:
override func didMoveToView(view: SKView) {
func displayFruit() {
// ...
}
}
This is not a local function:
override func didMoveToView(view: SKView) {
// ...
}
func displayFruit() {
// ...
}
Make it look like the second one.