Comment déclencher un bloc après un délai, comme -performSelector: withObject: afterDelay :?

734

Existe-t-il un moyen d'appeler un bloc avec un paramètre primitif après un délai, comme en utilisant performSelector:withObject:afterDelay:mais avec un argument comme int/ double/ float?

Egil
la source
C'est un point rare où GCD peut faire quelque chose que NSOperation ne peut pas, n'est-ce pas?
Anonymous White

Réponses:

1175

Je pense que vous cherchez dispatch_after(). Il nécessite que votre bloc n'accepte aucun paramètre, mais vous pouvez simplement laisser le bloc capturer ces variables à partir de votre portée locale à la place.

int parameter1 = 12;
float parameter2 = 144.1;

// Delay execution of my block for 10 seconds.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    NSLog(@"parameter1: %d parameter2: %f", parameter1, parameter2);
});

Plus: https://developer.apple.com/documentation/dispatch/1452876-dispatch_after

Ryan
la source
88
En fait, ce n'est pas vrai. Les objets capturés par un bloc qui ne sont pas marqués comme étant dans le stockage __block sont conservés par le bloc et sont libérés par le bloc lorsqu'il est détruit (lorsque son nombre de retenues passe à 0). Voici la documentation à ce sujet: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Ryan
9
cet dispatch_time(DISPATCH_TIME_NOW, 10ull * NSEC_PER_SEC)extrait est méchant. N'y a-t-il pas un moyen plus propre pour cela?
samvermette
7
Oui, dispatch_get_current_queue()renvoie toujours la file d'attente à partir de laquelle le code est exécuté. Ainsi, lorsque ce code est exécuté à partir du thread principal, le bloc sera également exécuté sur le thread principal.
Ryan
20
dispatch_get_current_queue()est désormais obsolète
Matej
9
Outre NSEC_PER_SEC, NSEC_PER_MSEC existe également, au cas où vous voudriez spécifier des millisecondes;)
cprcrack
504

Vous pouvez utiliser dispatch_afterpour appeler un bloc ultérieurement. Dans Xcode, commencez à taper dispatch_afteret appuyez sur Enterpour compléter automatiquement les éléments suivants:

entrez la description de l'image ici

Voici un exemple avec deux flottants comme «arguments». Vous n'avez pas à vous fier à n'importe quel type de macro, et l'intention du code est assez claire:

Swift 3, Swift 4

let time1 = 8.23
let time2 = 3.42

// Delay 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
    print("Sum of times: \(time1 + time2)")
}

Swift 2

let time1 = 8.23
let time2 = 3.42

// Delay 2 seconds
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in
        println("Sum of times: \(time1 + time2)")
}

Objectif c

CGFloat time1 = 3.49;
CGFloat time2 = 8.13;

// Delay 2 seconds
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    CGFloat newTime = time1 + time2;
    NSLog(@"New time: %f", newTime);
});
Steven Hepting
la source
45
Attention le temps de retard n'est pas un double. N'essayez donc pas NSEC_PER_SEC * 0.5 pendant une demi-seconde, cela ne fonctionnera pas! Vous devez passer en millisecondes et utiliser NSEC_PER_MSEC * 500. Vous devez donc changer votre exemple de code en: int delayInSeconds = 2 pour montrer que les gens ne peuvent pas utiliser des fractions de NSEC_PER_SEC.
malhal
11
@malhal En fait, NSEC_PER_SEC * 0.5fonctionnerait de la même manière que NSEC_PER_MSEC * 500. Bien que vous ayez raison de noter qu'il dispatch_timeattend un entier 64 bits, la valeur attendue est en nanosecondes. NSEC_PER_SECest défini comme 1000000000ull, et multiplier cela avec une constante à 0.5virgule flottante effectuerait implicitement une arithmétique à virgule flottante, produisant 500000000.0, avant qu'elle ne soit explicitement convertie en un entier 64 bits. Il est donc parfaitement acceptable d'utiliser une fraction de NSEC_PER_SEC.
junjie
202

Que diriez-vous d'utiliser la bibliothèque d'extraits de code Xcode intégrée?

entrez la description de l'image ici

Mise à jour pour Swift:

Beaucoup de votes positifs m'ont inspiré pour mettre à jour cette réponse.

La bibliothèque d'extraits de code Xcode intégrée a dispatch_afterpour seule objective-clangue. Les utilisateurs peuvent également créer leur propre extrait de code personnalisé pour Swift.

Écrivez ceci dans Xcode.

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(<#delayInSeconds#> * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), {
        <#code to be executed after a specified delay#>
    })

Faites glisser ce code et déposez-le dans la zone de bibliothèque d'extraits de code. entrez la description de l'image ici

En bas de la liste d'extraits de code, il y aura une nouvelle entité nommée My Code Snippet. Modifiez ceci pour un titre. Pour des suggestions en tapant le Xcode, remplissez le Completion Shortcut.

Pour plus d'informations, voir Création d'un SnapCodeCustomCode .

Mettre à jour Swift 3

Faites glisser ce code et déposez-le dans la zone de bibliothèque d'extraits de code.

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(<#delayInSeconds#>)) {
    <#code to be executed after a specified delay#>
}
Warif Akhand Rishi
la source
19
Quelqu'un utilise-t-il réellement cette fonctionnalité dans Xcode? Je préfère simplement le taper comme popup de suggestions de code et il est tout aussi facile à utiliser.
Supertecnoboff
6
Jusqu'à ce que je sache, je pensais simplement que copier-coller était le moyen le plus simple de coder. Maintenant je viens de glisser-déposer .... hahaha
arshu
58

En développant la réponse de Jaime Cham, j'ai créé une catégorie NSObject + Blocks comme ci-dessous. Je pensais que ces méthodes correspondaient mieux aux performSelector:méthodes NSObject existantes

NSObject + Blocks.h

#import <Foundation/Foundation.h>

@interface NSObject (Blocks)

- (void)performBlock:(void (^)())block afterDelay:(NSTimeInterval)delay;

@end

NSObject + Blocks.m

#import "NSObject+Blocks.h"

@implementation NSObject (Blocks)

- (void)performBlock:(void (^)())block
{
    block();
}

- (void)performBlock:(void (^)())block afterDelay:(NSTimeInterval)delay
{
    void (^block_)() = [block copy]; // autorelease this if you're not using ARC
    [self performSelector:@selector(performBlock:) withObject:block_ afterDelay:delay];
}

@end

et utiliser comme ça:

[anyObject performBlock:^{
    [anotherObject doYourThings:stuff];
} afterDelay:0.15];
Oliver Pearmain
la source
5
Le delaydevrait être de NSTimeInterval(qui est un double). #import <UIKit/UIKit.h>n'est pas nécessaire. Et, je ne vois pas pourquoi cela - (void)performBlock:(void (^)())block;pourrait être utile, donc peut être supprimé de l'en-tête.
signification compte
@ sens-questions, les deux points valides +1, j'ai mis à jour ma réponse en conséquence.
Oliver Pearmain
ce n'est pas correct du tout, le performSelector doit être supprimé explicitement sur dealloc, sinon vous rencontrerez un comportement vraiment étrange et des plantages, le plus correct est d'utiliser le dispatch_after
Peter Lapisu
21

Peut-être plus simple que de passer par GCD, dans une classe quelque part (par exemple "Util"), ou une catégorie sur un objet:

+ (void)runBlock:(void (^)())block
{
    block();
}
+ (void)runAfterDelay:(CGFloat)delay block:(void (^)())block 
{
    void (^block_)() = [[block copy] autorelease];
    [self performSelector:@selector(runBlock:) withObject:block_ afterDelay:delay];
}

Donc à utiliser:

[Util runAfterDelay:2 block:^{
    NSLog(@"two seconds later!");
}];
Jaime Cham
la source
3
@Jaimie Cham Pourquoi pensez-vous que passer par GCD est difficile?
Besi
2
Passer par GCD a un comportement légèrement différent de PerformSelector: afterDelay :, il peut donc y avoir des raisons de ne pas utiliser GCD. Voir, par exemple, la question suivante: stackoverflow.com/questions/10440412/…
fishinear
Pourquoi copiez-vous le bloc avant de le transmettre à performSelector?
c roald
1
Désolé pour le retard. @croald: Je pense que vous avez besoin de la copie pour déplacer le bloc de la pile vers le tas.
Jaime Cham
@Besi: plus verbeux et cache l'intention.
Jaime Cham
21

Pour Swift, j'ai créé une fonction globale, rien de spécial, en utilisant la dispatch_afterméthode. J'aime plus cela car il est lisible et facile à utiliser:

func performBlock(block:() -> Void, afterDelay delay:NSTimeInterval){
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), block)
}

Que vous pouvez utiliser comme suit:

performBlock({ () -> Void in
    // Perform actions
}, afterDelay: 0.3)
Antoine
la source
1
Je suggère d'échanger les arguments et de les renommer after. Ensuite, vous pouvez écrire:after(2.0){ print("do somthing") }
Lars Blumberg
16

Voici mes 2 cents = 5 méthodes;)

J'aime encapsuler ces détails et demander à AppCode de me dire comment terminer mes phrases.

void dispatch_after_delay(float delayInSeconds, dispatch_queue_t queue, dispatch_block_t block) {
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
    dispatch_after(popTime, queue, block);
}

void dispatch_after_delay_on_main_queue(float delayInSeconds, dispatch_block_t block) {
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_after_delay(delayInSeconds, queue, block);
}

void dispatch_async_on_high_priority_queue(dispatch_block_t block) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), block);
}

void dispatch_async_on_background_queue(dispatch_block_t block) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), block);
}

void dispatch_async_on_main_queue(dispatch_block_t block) {
    dispatch_async(dispatch_get_main_queue(), block);
}
Dan Rosenstark
la source
8

PerformSelector: WithObject prend toujours un objet, donc pour passer des arguments comme int / double / float etc ..... Vous pouvez utiliser quelque chose comme ça.

// NSNumber est un objet ..

[self performSelector:@selector(setUserAlphaNumber:)
     withObject: [NSNumber numberWithFloat: 1.0f]       
     afterDelay:1.5];



-(void) setUserAlphaNumber: (NSNumber*) number{

     [txtUsername setAlpha: [number floatValue] ];

}

De la même manière, vous pouvez utiliser [NSNumber numberWithInt:] etc .... et dans la méthode de réception, vous pouvez convertir le nombre dans votre format en tant que [nombre entier] ou [numéro double].

Augustin
la source
8

La fonction dispatch_after distribue un objet bloc dans une file d'attente de répartition après un laps de temps donné. Utilisez le code ci-dessous pour effectuer certaines tâches liées à l'interface utilisateur après 2,0 secondes.

            let delay = 2.0
            let delayInNanoSeconds = dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)))
            let mainQueue = dispatch_get_main_queue()

            dispatch_after(delayInNanoSeconds, mainQueue, {

                print("Some UI related task after delay")
            })

Dans swift 3.0:

            let dispatchTime: DispatchTime = DispatchTime.now() + Double(Int64(2.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
            DispatchQueue.main.asyncAfter(deadline: dispatchTime, execute: {

          })
Himanshu Mahajan
la source
il y a une faute de frappe: mainQueue, au lieu demainQueue)
Bastian
5

Voici la méthode Swift 3 pour mettre en file d'attente le travail après un délai.

DispatchQueue.main.asyncAfter(
  DispatchTime.now() + DispatchTimeInterval.seconds(2)) {
    // do work
}
cpimhoff
la source
5

Voici une aide pratique pour éviter de faire des appels GCD ennuyeux encore et encore:

public func delay(bySeconds seconds: Double, dispatchLevel: DispatchLevel = .main, closure: @escaping () -> Void) {
    let dispatchTime = DispatchTime.now() + seconds
    dispatchLevel.dispatchQueue.asyncAfter(deadline: dispatchTime, execute: closure)
}

public enum DispatchLevel {
    case main, userInteractive, userInitiated, utility, background
    var dispatchQueue: DispatchQueue {
        switch self {
        case .main:                 return DispatchQueue.main
        case .userInteractive:      return DispatchQueue.global(qos: .userInteractive)
        case .userInitiated:        return DispatchQueue.global(qos: .userInitiated)
        case .utility:              return DispatchQueue.global(qos: .utility)
        case .background:           return DispatchQueue.global(qos: .background)
        }
    }
}

Maintenant, vous retardez simplement votre code sur le fil principal comme ceci:

delay(bySeconds: 1.5) { 
    // delayed code
}

Si vous souhaitez retarder votre code sur un thread différent :

delay(bySeconds: 1.5, dispatchLevel: .background) { 
    // delayed code that will run on background thread
}

Si vous préférez un Framework qui possède également des fonctionnalités plus pratiques, passez à HandySwift . Vous pouvez l'ajouter à votre projet via Carthage puis l'utiliser exactement comme dans les exemples ci-dessus:

import HandySwift    

delay(bySeconds: 1.5) { 
    // delayed code
}
Jeehut
la source
Cela implique implicitement que votre fonction de délai exécute du code à partir du thread d'arrière-plan. Quelqu'un qui utilise votre exemple peut avoir du mal à déboguer l'application en cours de plantage, s'il met du code lié à l'interface utilisateur dans // la section de code différé .
nalexn
Par défaut, ma méthode utilise le thread principal afin que cela ne se produise pas. Voir le dispatchLevel par défaut à .Main?
Jeehut
4

Il y en a une belle dans le framework BlocksKit.

BlocksKit

(et la classe)

BBlocksKit.m

psy
la source
4

Dans swift 3, nous pouvons simplement utiliser la fonction DispatchQueue.main.asyncAfter pour déclencher toute fonction ou action après le délai de 'n' secondes. Ici, dans le code, nous avons défini le délai après 1 seconde. Vous appelez n'importe quelle fonction à l'intérieur du corps de cette fonction qui se déclenchera après le délai de 1 seconde.

let when = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: when) {

    // Trigger the function/action after the delay of 1Sec

}
Rouny
la source
4

Xcode 10.2 et Swift 5 et supérieur

DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: {
   // code to execute                 
})
midhun p
la source
1

Vous pouvez soit encapsuler l'argument dans votre propre classe, soit encapsuler l'appel de méthode dans une méthode qui n'a pas besoin d'être passée dans le type primitif. Appelez ensuite cette méthode après votre délai, et dans cette méthode, effectuez le sélecteur que vous souhaitez effectuer.

GendoIkari
la source
1

Voici comment déclencher un blocage après un retard dans Swift:

runThisAfterDelay(seconds: 2) { () -> () in
    print("Prints this 2 seconds later in main queue")
}

/// EZSwiftExtensions
func runThisAfterDelay(seconds seconds: Double, after: () -> ()) {
    let time = dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC)))
    dispatch_after(time, dispatch_get_main_queue(), after)
}

Il est inclus en tant que fonction standard dans mon référentiel .

Esqarrouth
la source
1

Swift 3 et Xcode 8.3.2

Ce code vous aidera, j'ajoute aussi une explication

// Create custom class, this will make your life easier
class CustomDelay {

    static let cd = CustomDelay()

    // This is your custom delay function
    func runAfterDelay(_ delay:Double, closure:@escaping ()->()) {
        let when = DispatchTime.now() + delay
        DispatchQueue.main.asyncAfter(deadline: when, execute: closure)
    }
}


// here how to use it (Example 1)
class YourViewController: UIViewController {

    // example delay time 2 second
    let delayTime = 2.0

    override func viewDidLoad() {
        super.viewDidLoad()

        CustomDelay.cd.runAfterDelay(delayTime) {
            // This func will run after 2 second
            // Update your UI here, u don't need to worry to bring this to the main thread because your CustomDelay already make this to main thread automatically :)
            self.runFunc()
        }
    }

    // example function 1
    func runFunc() {
        // do your method 1 here
    }
}

// here how to use it (Example 2)
class YourSecondViewController: UIViewController {

    // let say you want to user run function shoot after 3 second they tap a button

    // Create a button (This is programatically, you can create with storyboard too)
    let shootButton: UIButton = {
        let button = UIButton(type: .system)
        button.frame = CGRect(x: 15, y: 15, width: 40, height: 40) // Customize where do you want to put your button inside your ui
        button.setTitle("Shoot", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        // create an action selector when user tap shoot button
        shootButton.addTarget(self, action: #selector(shoot), for: .touchUpInside)   
    }

    // example shoot function
    func shoot() {
        // example delay time 3 second then shoot
        let delayTime = 3.0

        // delay a shoot after 3 second
        CustomDelay.cd.runAfterDelay(delayTime) {
            // your shoot method here
            // Update your UI here, u don't need to worry to bring this to the main thread because your CustomDelay already make this to main thread automatically :)
        }
    }   
}
Andrian Rahardja
la source
0

Je crois que l'auteur ne demande pas comment attendre un temps fractionnaire (retard), mais plutôt comment passer un scalaire comme argument du sélecteur (withObject :) et le moyen le plus rapide dans l'objectif moderne C est:

[obj performSelector:...  withObject:@(0.123123123) afterDelay:10]

votre sélecteur doit changer son paramètre en NSNumber et récupérer la valeur en utilisant un sélecteur comme floatValue ou doubleValue

André
la source