¿Cómo se activa un bloqueo después de un retraso, como -performSelector: withObject: afterDelay :?

734

¿Hay alguna manera de llamar a un bloque con un parámetro primitivo después de un retraso, como usar performSelector:withObject:afterDelay:pero con un argumento como int/ double/ float?

Egil
fuente
Este es un punto raro en el que GCD puede hacer algo que NSOperation no puede, ¿no?
Anónimo White

Respuestas:

1175

Creo que lo estás buscando dispatch_after(). Requiere que su bloque no acepte parámetros, pero puede dejar que el bloque capture esas variables de su ámbito local.

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);
});

Más: https://developer.apple.com/documentation/dispatch/1452876-dispatch_after

Ryan
fuente
88
En realidad, eso no es cierto. Los objetos capturados por un bloque que no están marcados como __bloque de almacenamiento son retenidos por el bloque, y el bloque los libera cuando se destruye (cuando su recuento de retención va a 0). Aquí está la documentación sobre eso: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Ryan
99
Este dispatch_time(DISPATCH_TIME_NOW, 10ull * NSEC_PER_SEC)fragmento es desagradable. ¿No hay una forma más limpia para esto?
samvermette
77
Sí, dispatch_get_current_queue()siempre devuelve la cola desde la que se ejecuta el código. Entonces, cuando este código se ejecuta desde el hilo principal, el bloque también se ejecutará en el hilo principal.
Ryan
20
dispatch_get_current_queue()ahora está en desuso
Matej
99
Además de NSEC_PER_SEC, NSEC_PER_MSEC también existe, en caso de que desee especificar milisegundos;)
cprcrack
504

Puede usar dispatch_afterpara llamar a un bloque más tarde. En Xcode, comience a escribir dispatch_aftery Enterpresione para autocompletar lo siguiente:

ingrese la descripción de la imagen aquí

Aquí hay un ejemplo con dos flotantes como "argumentos". No tiene que confiar en ningún tipo de macro, y la intención del código es bastante clara:

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)")
}

C objetivo

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
fuente
45
Tenga cuidado, el tiempo de retraso no es el doble. Así que no intentes NSEC_PER_SEC * 0.5 durante medio segundo, ¡no funcionará! Debe bajar a milisegundos y usar NSEC_PER_MSEC * 500. Por lo tanto, debe cambiar su muestra de código a: int delayInSeconds = 2 para mostrar que las personas no pueden usar fracciones de NSEC_PER_SEC.
malhal
11
@malhal En realidad, NSEC_PER_SEC * 0.5funcionaría igual que NSEC_PER_MSEC * 500. Si bien tiene razón al notar que dispatch_timeespera un número entero de 64 bits, el valor que espera está en nanosegundos. NSEC_PER_SECse define como 1000000000ull, y multiplicar eso con una constante de punto flotante 0.5implícitamente realizaría una aritmética de punto flotante, produciendo 500000000.0, antes de que se convierta explícitamente en un entero de 64 bits. Por lo tanto, es perfectamente aceptable usar una fracción de NSEC_PER_SEC.
junjie
202

¿Qué hay de usar la biblioteca de fragmentos de código incorporada de Xcode?

ingrese la descripción de la imagen aquí

Actualización para Swift:

Muchos votos positivos me inspiraron para actualizar esta respuesta.

La biblioteca de fragmentos de código Xcode incorporada dispatch_aftersolo tiene un objective-cidioma. Las personas también pueden crear su propio fragmento de código personalizado para Swift.

Escribe esto en 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#>
    })

Arrastre este código y suéltelo en el área de la biblioteca de fragmentos de código. ingrese la descripción de la imagen aquí

Al final de la lista de fragmentos de código, habrá una nueva entidad llamada My Code Snippet. Edite esto para un título. Para sugerencias mientras escribe el código X, complete el Completion Shortcut.

Para obtener más información, consulte Creación de un código de código personalizado .

Actualizar Swift 3

Arrastre este código y suéltelo en el área de la biblioteca de fragmentos de código.

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(<#delayInSeconds#>)) {
    <#code to be executed after a specified delay#>
}
Warif Akhand Rishi
fuente
19
¿Alguien realmente usa esta función en Xcode? Prefiero escribirlo como la ventana emergente de sugerencias de código y son igual de fáciles de usar.
Supertecnoboff
66
Hasta ahora, pensé que copiar y pegar era la forma más fácil de codificar. Ahora solo arrastro y
suelto
58

Ampliando la respuesta de Jaime Cham, creé una categoría NSObject + Blocks como se muestra a continuación. Sentí que estos métodos coincidían mejor con los performSelector:métodos existentes de NSObject

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

y usar así:

[anyObject performBlock:^{
    [anotherObject doYourThings:stuff];
} afterDelay:0.15];
Oliver Pearmain
fuente
55
El delaydebería ser de NSTimeInterval(que es a double). #import <UIKit/UIKit.h>no es necesario. Y, no veo por qué - (void)performBlock:(void (^)())block;podría ser útil, por lo que se puede eliminar del encabezado.
significado-importa
@ significado-importa, ambos puntos válidos +1, he actualizado mi respuesta en consecuencia.
Oliver Pearmain
esto no es correcto en absoluto, el performSelector debe eliminarse explícitamente en dealloc, o de lo contrario se encontrará con un comportamiento realmente extraño y se bloquea, lo más correcto es usar el dispatch_after
Peter Lapisu
21

Quizás más simple que ir a través de GCD, en una clase en algún lugar (por ejemplo, "Util"), o una Categoría en Objeto:

+ (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];
}

Para usar:

[Util runAfterDelay:2 block:^{
    NSLog(@"two seconds later!");
}];
Jaime Cham
fuente
3
@Jaimie Cham ¿Por qué crees que pasar por GCD es difícil?
Besi
2
Pasar por GCD tiene un comportamiento ligeramente diferente al de PerformSelector: afterDelay :, por lo que puede haber razones para no usar GCD. Consulte, por ejemplo, la siguiente pregunta: stackoverflow.com/questions/10440412/…
fishinear
¿Por qué copia el bloque antes de pasarlo a performSelector?
c roald
1
Perdón por el retraso. @croald: Creo que necesitas la copia para mover el bloque de la pila al montón.
Jaime Cham
@Besi: más prolijo y oculta la intención.
Jaime Cham
21

Para Swift, he creado una función global, nada especial, utilizando el dispatch_aftermétodo. Me gusta más, ya que es legible y fácil de usar:

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 puedes usar de la siguiente manera:

performBlock({ () -> Void in
    // Perform actions
}, afterDelay: 0.3)
Antoine
fuente
1
Sugiero intercambiar argumentos y renombrarlo a after. Entonces puedes escribir:after(2.0){ print("do somthing") }
Lars Blumberg
16

Aquí están mis 2 centavos = 5 métodos;)

Me gusta encapsular estos detalles y hacer que AppCode me diga cómo terminar mis oraciones.

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
fuente
8

PerformSelector: WithObject siempre toma un objeto, por lo que para pasar argumentos como int / double / float, etc. ... puede usar algo como esto.

// NSNumber es un objeto ...

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



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

     [txtUsername setAlpha: [number floatValue] ];

}

De la misma manera puede usar [NSNumber numberWithInt:] etc .... y en el método de recepción puede convertir el número a su formato como [número int] o [número doble].

Agustín
fuente
8

La función dispatch_after despacha un objeto de bloque a una cola de despacho después de un período de tiempo determinado. Use el siguiente código para realizar algunas tareas relacionadas con la interfaz de usuario después de 2.0 segundos.

            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")
            })

En 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
fuente
hay un error tipográfico: en mainQueue, lugar demainQueue)
Bastian
5

Aquí está la forma de Swift 3 de hacer cola después de un retraso.

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

Aquí hay un útil ayudante para evitar hacer la molesta llamada GCD una y otra vez:

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)
        }
    }
}

Ahora simplemente retrasa su código en el hilo principal de esta manera:

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

Si desea retrasar su código a un hilo diferente :

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

Si prefiere un Framework que también tenga algunas características más útiles, entonces consulte HandySwift . Puede agregarlo a su proyecto a través de Carthage y luego usarlo exactamente como en los ejemplos anteriores:

import HandySwift    

delay(bySeconds: 1.5) { 
    // delayed code
}
Jeehut
fuente
Esto está implícito en que su función de retraso ejecuta el código desde el hilo de fondo. Alguien que use su ejemplo puede tener dificultades para depurar la aplicación que se bloquea, si pone algún código relacionado con la interfaz de usuario dentro de la sección // código retrasado .
nalexn
Por defecto, mi método usa el hilo principal para que eso no suceda. Vea el nivel de despacho predeterminado en .Main?
Jeehut
4

En swift 3, simplemente podemos usar la función DispatchQueue.main.asyncAfter para activar cualquier función o acción después del retraso de 'n' segundos. Aquí en el código hemos establecido el retraso después de 1 segundo. Llama a cualquier función dentro del cuerpo de esta función que se activará después del retraso de 1 segundo.

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

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

}
Rouny
fuente
4

Xcode 10.2 y Swift 5 y superior

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

Puede ajustar el argumento en su propia clase o ajustar la llamada al método en un método que no necesita pasarse en el tipo primitivo. Luego llame a ese método después de su retraso, y dentro de ese método realice el selector que desea realizar.

GendoIkari
fuente
1

Así es como puede activar un bloqueo después de un retraso en 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)
}

Se incluye como una función estándar en mi repositorio .

Esqarrouth
fuente
1

Swift 3 y Xcode 8.3.2

Este código te ayudará, también agrego una explicación

// 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
fuente
0

Creo que el autor no pregunta cómo esperar un tiempo fraccional (retraso), sino cómo pasar un escalar como argumento del selector (con Objeto :) y la forma más rápida en el objetivo moderno C es:

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

su selector tiene que cambiar su parámetro a NSNumber y recuperar el valor utilizando un selector como floatValue o doubleValue

André
fuente