Como você aciona um bloco após um atraso, como -performSelector: withObject: afterDelay :?

735

Existe uma maneira de chamar um bloco com um parâmetro primitivo após um atraso, como usar, performSelector:withObject:afterDelay:mas com um argumento como int/ double/ float?

Egil
fonte
Este é um ponto raro em que o GCD pode fazer algo que a NSOperation não pode, não é?
Anonymous White

Respostas:

1175

Eu acho que você está procurando dispatch_after(). Ele exige que seu bloco não aceite parâmetros, mas você pode permitir que o bloco capture essas variáveis ​​do seu escopo 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);
});

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

Ryan
fonte
88
Na verdade, isso não é verdade. Objetos capturados por um bloco que não estão marcados como estando no armazenamento __block são retidos pelo bloco e são liberados pelo bloco quando são destruídos (quando sua contagem de retenções chega a 0). Aqui está a documentação sobre isso: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Ryan
9
esse dispatch_time(DISPATCH_TIME_NOW, 10ull * NSEC_PER_SEC)trecho é desagradável. Não existe uma maneira mais limpa para isso?
samvermette
7
Sim, dispatch_get_current_queue()sempre retorna a fila na qual o código está sendo executado. Portanto, quando esse código for executado no thread principal, o bloco também será executado no thread principal.
Ryan
20
dispatch_get_current_queue()está obsoleto agora
Matej
9
Além NSEC_PER_SEC, NSEC_PER_MSEC também existe, no caso de você querer especificar milissegundos;)
cprcrack
504

Você pode usar dispatch_afterpara ligar para um bloco mais tarde. No Xcode, comece a digitar dispatch_aftere pressione Enterpara concluir automaticamente o seguinte:

insira a descrição da imagem aqui

Aqui está um exemplo com dois carros alegóricos como "argumentos". Você não precisa confiar em nenhum tipo de macro, e a intenção do código é 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)")
}

Objetivo 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
fonte
45
Tenha cuidado, o tempo de atraso não é duplo. Portanto, não tente o NSEC_PER_SEC * 0.5 por meio segundo, não funcionará! Você precisa cair para milissegundos e usar NSEC_PER_MSEC * 500. Portanto, você deve alterar seu exemplo de código para: int delayInSeconds = 2 para mostrar que as pessoas não podem usar frações de NSEC_PER_SEC.
malhal
11
@malhal Na verdade, NSEC_PER_SEC * 0.5funcionaria da mesma forma que NSEC_PER_MSEC * 500. Enquanto você está correto ao observar que dispatch_timeespera um número inteiro de 64 bits, o valor esperado é em nanossegundos. NSEC_PER_SECé definido como 1000000000ulle multiplicar isso por uma constante de ponto flutuante 0.5implicitamente executaria uma aritmética de ponto flutuante, produzindo 500000000.0, antes que seja explicitamente convertida de volta para um número inteiro de 64 bits. Portanto, é perfeitamente aceitável usar uma fração de NSEC_PER_SEC.
Junjie
202

Que tal usar a biblioteca de trechos de código internos do Xcode?

insira a descrição da imagem aqui

Atualização para Swift:

Muitos votos me inspiraram a atualizar esta resposta.

A biblioteca de trechos de código Xcode incorporada possui dispatch_afterapenas o objective-cidioma. As pessoas também podem criar seu próprio código personalizado de trechos para Swift.

Escreva isso no 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#>
    })

Arraste esse código e solte-o na área da biblioteca de trechos de código. insira a descrição da imagem aqui

No final da lista de trechos de código, haverá uma nova entidade chamada My Code Snippet. Edite isso para um título. Para sugestões ao digitar o Xcode, preencha o Completion Shortcut.

Para mais informações, consulte CreatingaCustomCodeSnippet .

Atualização Swift 3

Arraste esse código e solte-o na área da biblioteca de trechos de código.

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(<#delayInSeconds#>)) {
    <#code to be executed after a specified delay#>
}
Warif Akhand Rishi
fonte
19
Alguém realmente usa esse recurso no Xcode? Prefiro digitá-lo como o pop-up de sugestões de código e são fáceis de usar.
Supertecnoboff
6
Até saber que eu pensei que copiar e colar era a maneira mais fácil de codificar. Agora eu só drag & drop .... hahaha
arshu
58

Expandindo a resposta de Jaime Cham, criei uma categoria NSObject + Blocks como abaixo. Eu senti que esses métodos correspondiam melhor aos performSelector:métodos NSObject existentes

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

e use assim:

[anyObject performBlock:^{
    [anotherObject doYourThings:stuff];
} afterDelay:0.15];
Oliver Pearmain
fonte
5
O delaydeve ser de NSTimeInterval(que é a double). #import <UIKit/UIKit.h>Não é necessário. E não vejo por que - (void)performBlock:(void (^)())block;poderia ser útil, portanto pode ser removido do cabeçalho.
significado-importa
@ assuntos de significado, ambos os pontos válidos +1, atualizei minha resposta de acordo.
Oliver Pearmain 7/03/13
isso não é correto em tudo, o performSelector tem que ser removido explicitamente dealloc, ou então você vai correr em um comportamento muito estranho e cai, mais correto é usar a dispatch_after
Peter Lapisu
21

Talvez seja mais simples do que passar pelo GCD, em uma classe em algum lugar (por exemplo, "Util") ou em uma categoria no 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];
}

Então, para usar:

[Util runAfterDelay:2 block:^{
    NSLog(@"two seconds later!");
}];
Jaime Cham
fonte
3
@Jaimie Cham Por que você acha difícil passar pela GCD?
Besi
2
Passar pelo GCD tem um comportamento ligeiramente diferente do PerformSelector: afterDelay :, portanto, pode haver razões para não usar o GCD. Veja, por exemplo, a seguinte pergunta: stackoverflow.com/questions/10440412/…
fishinear
Por que você copia o bloco antes de passá-lo para o performSelector?
c Roald
1
Desculpe o atraso. @ Croald: Eu acho que você precisa da cópia para mover o bloco da pilha para a pilha.
Jaime Cham
@Besi: mais prolixo e esconde a intenção.
Jaime Cham
21

Para Swift, criei uma função global, nada de especial, usando o dispatch_aftermétodo Eu gosto mais disso, pois é legível e 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 você pode usar da seguinte maneira:

performBlock({ () -> Void in
    // Perform actions
}, afterDelay: 0.3)
Antoine
fonte
1
Sugiro trocar argumentos e renomeá-lo para after. Então você pode escrever:after(2.0){ print("do somthing") }
Lars Blumberg
16

Aqui estão os meus 2 centavos = 5 métodos;)

Eu gosto de encapsular esses detalhes e fazer com que o AppCode me diga como terminar minhas frases.

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

PerformSelector: WithObject sempre pega um objeto, portanto, para passar argumentos como int / double / float, etc ..... Você pode usar algo como isto.

// NSNumber é um objeto ..

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



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

     [txtUsername setAlpha: [number floatValue] ];

}

Da mesma forma que você pode usar [NSNumber numberWithInt:] etc .... e no método de recebimento, você pode converter o número em seu formato como [number int] ou [number double].

Agostinho
fonte
8

A função dispatch_after despacha um objeto de bloco para uma fila de despacho após um determinado período de tempo. Use o código abaixo para executar algumas tarefas relacionadas à interface do usuário após 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")
            })

No 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
fonte
existe um erro de digitação: em mainQueue, vez demainQueue)
Bastian
5

Aqui está a maneira do Swift 3 de enfileirar o trabalho após um atraso.

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

Aqui está um ajudante útil para evitar fazer a chamada irritante do GCD repetidamente:

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

Agora você simplesmente adia seu código no thread principal assim:

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

Se você deseja atrasar seu código para outro segmento :

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

Se você preferir um Framework que também tenha alguns recursos mais úteis, consulte o HandySwift . Você pode adicioná-lo ao seu projeto via Carthage e usá-lo exatamente como nos exemplos acima:

import HandySwift    

delay(bySeconds: 1.5) { 
    // delayed code
}
Jeehut
fonte
Isso está implícito que sua função de atraso executa o código do thread em segundo plano. Alguém que usa o seu exemplo pode ter dificuldades para depurar o aplicativo que está sendo travado, se colocar algum código relacionado à interface do usuário dentro da seção // código atrasado .
Nalexn 5/09/16
Por padrão, meu método usa o thread principal para que isso não aconteça. Veja o dispatchLevel padrão para .Main?
Jeehut 5/09
4

Há uma boa na estrutura do BlocksKit.

BlocksKit

(e a classe)

BBlocksKit.m

psy
fonte
4

No swift 3, podemos simplesmente usar a função DispatchQueue.main.asyncAfter para disparar qualquer função ou ação após o atraso de 'n' segundos. Aqui no código, definimos o atraso após 1 segundo. Você chama qualquer função dentro do corpo dessa função que será acionada após o atraso de 1 segundo.

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

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

}
Rouny
fonte
4

Xcode 10.2 e Swift 5 e acima

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

Você pode agrupar o argumento em sua própria classe ou agrupar a chamada do método em um método que não precise ser passado no tipo primitivo. Em seguida, chame esse método após o seu atraso e, dentro desse método, execute o seletor que deseja executar.

GendoIkari
fonte
1

Aqui está como você pode acionar um bloco após um atraso no 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)
}

Está incluído como uma função padrão no meu repositório .

Esqarrouth
fonte
1

Swift 3 e Xcode 8.3.2

Este código irá ajudá-lo, eu adiciono uma explicação também

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

Acredito que o autor não está perguntando como esperar um tempo fracionário (atraso), mas como passar um escalar como argumento do seletor (withObject :) e a maneira mais rápida no objetivo moderno C é:

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

seu seletor precisa alterar seu parâmetro para NSNumber e recuperar o valor usando um seletor como floatValue ou doubleValue

André
fonte