I'm trying to create a typewriter animation effect with a UILabel, but can't find any answers. Is the UILabel the correct object to use? I want the text to print to the screen an array of strings like, "Logging in... Opening Folder... Rebooting system.." etc. I should mention that I'm new to coding and I've tried searching the Documentation and API reference but no luck. I'm currently learning SWIFT if thats worth mentioning
Based on this Answer:
Letter by letter animation for UILabel?
I've updated it to Swift 4 and solved the CPU animation problem with DispatchWorkItem in order to create a queue.
Swift 4
extension UILabel {
func setTextWithTypeAnimation(typedText: String, characterDelay: TimeInterval = 5.0) {
text = ""
var writingTask: DispatchWorkItem?
writingTask = DispatchWorkItem { [weak weakSelf = self] in
for character in typedText {
DispatchQueue.main.async {
weakSelf?.text!.append(character)
}
Thread.sleep(forTimeInterval: characterDelay/100)
}
}
if let task = writingTask {
let queue = DispatchQueue(label: "typespeed", qos: DispatchQoS.userInteractive)
queue.asyncAfter(deadline: .now() + 0.05, execute: task)
}
}
}
Usage
label.setTextWithTypeAnimation(typedText: text, characterDelay: 10) //less delay is faster
Swift 5
func setTyping(text: String, characterDelay: TimeInterval = 5.0) {
self.text = ""
let writingTask = DispatchWorkItem { [weak self] in
text.forEach { char in
DispatchQueue.main.async {
self?.text?.append(char)
}
Thread.sleep(forTimeInterval: characterDelay/100)
}
}
let queue: DispatchQueue = .init(label: "typespeed", qos: .userInteractive)
queue.asyncAfter(deadline: .now() + 0.05, execute: writingTask)
}
Usage
label.setTyping(text: "Your text")
update: Xcode 7.0 GM • Swift 2.0
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myTypeWriter: UITextField!
let myText = Array("Hello World !!!".characters)
var myCounter = 0
var timer:NSTimer?
func fireTimer(){
timer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: "typeLetter", userInfo: nil, repeats: true)
}
func typeLetter(){
if myCounter < myText.count {
myTypeWriter.text = myTypeWriter.text! + String(myText[myCounter])
let randomInterval = Double((arc4random_uniform(8)+1))/20
timer?.invalidate()
timer = NSTimer.scheduledTimerWithTimeInterval(randomInterval, target: self, selector: "typeLetter", userInfo: nil, repeats: false)
} else {
timer?.invalidate()
}
myCounter++
}
override func viewDidLoad() {
super.viewDidLoad()
fireTimer()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I have written a subclass of UILabel called CLTypingLabel, available on GitHub. This should do what you want.
After installing CocoaPods, add the following like to your Podfile to use it:
pod 'CLTypingLabel'
Sample Code
Change the class of a label from UILabel to CLTypingLabel;
#IBOutlet weak var myTypeWriterLabel: CLTypingLabel!
At runtime, set text of the label will trigger animation automatically:
myTypeWriterLabel.text = "This is a demo of typing label animation..."
You can customize time interval between each character:
myTypeWriterLabel.charInterval = 0.08 //optional, default is 0.1
You can pause the typing animation at any time:
myTypeWriterLabel.pauseTyping() //this will pause the typing animation
myTypeWriterLabel.continueTyping() //this will continue paused typing animation
Also there is a sample project that comes with cocoapods
my version of the typewriter effect animation using a timer:
var text = "text"
_ = Timer.scheduledTimer(
withTimeInterval: 0.1,
repeats: true
) { [weak self] timer in
let char = text.removeFirst()
self?.yourLabel.text?.append(char.description)
if text.isEmpty {
timer.invalidate()
}
}
Related
How can I run a function every minute?
In JavaScript I can do something like setInterval, does something similar exist in Swift?
Wanted output:
Hello World once a minute...
var helloWorldTimer = NSTimer.scheduledTimerWithTimeInterval(60.0, target: self, selector: Selector("sayHello"), userInfo: nil, repeats: true)
func sayHello()
{
NSLog("hello World")
}
Remember to import Foundation.
Swift 4:
var helloWorldTimer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(ViewController.sayHello), userInfo: nil, repeats: true)
#objc func sayHello()
{
NSLog("hello World")
}
If targeting iOS version 10 and greater, you can use the block-based rendition of Timer, which simplifies the potential strong reference cycles, e.g.:
weak var timer: Timer?
func startTimer() {
timer?.invalidate() // just in case you had existing `Timer`, `invalidate` it before we lose our reference to it
timer = Timer.scheduledTimer(withTimeInterval: 60.0, repeats: true) { [weak self] _ in
// do something here
}
}
func stopTimer() {
timer?.invalidate()
}
// if appropriate, make sure to stop your timer in `deinit`
deinit {
stopTimer()
}
While Timer is generally best, for the sake of completeness, I should note that you can also use dispatch timer, which is useful for scheduling timers on background threads. With dispatch timers, since they're block-based, it avoids some of the strong reference cycle challenges with the old target/selector pattern of Timer, as long as you use weak references.
So:
var timer: DispatchSourceTimer?
func startTimer() {
let queue = DispatchQueue(label: "com.domain.app.timer") // you can also use `DispatchQueue.main`, if you want
timer = DispatchSource.makeTimerSource(queue: queue)
timer!.schedule(deadline: .now(), repeating: .seconds(60))
timer!.setEventHandler { [weak self] in
// do whatever you want here
}
timer!.resume()
}
func stopTimer() {
timer = nil
}
For more information, see the the Creating a Timer section of Dispatch Source Examples in the Dispatch Sources section of the Concurrency Programming Guide.
For Swift 2, see previous revision of this answer.
If you can allow for some time drift here's a simple solution executing some code every minute:
private func executeRepeatedly() {
// put your code here
DispatchQueue.main.asyncAfter(deadline: .now() + 60.0) { [weak self] in
self?.executeRepeatedly()
}
}
Just run executeRepeatedly() once and it'll be executed every minute. The execution stops when the owning object (self) is released. You also can use a flag to indicate that the execution must stop.
Here's an update to the NSTimer answer, for Swift 3 (in which NSTimer was renamed to Timer) using a closure rather than a named function:
var timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
(_) in
print("Hello world")
}
You can use Timer (swift 3)
var timer = Timer.scheduledTimerWithTimeInterval(60, target: self, selector: Selector("function"), userInfo: nil, repeats: true)
In selector() you put in your function name
In swift 3.0 the GCD got refactored:
let timer : DispatchSourceTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
timer.scheduleRepeating(deadline: .now(), interval: .seconds(60))
timer.setEventHandler
{
NSLog("Hello World")
}
timer.resume()
This is specially useful for when you need to dispatch on a particular Queue. Also, if you're planning on using this for user interface updating, I suggest looking into CADisplayLink as it's synchronized with the GPU refresh rate.
Here is another version algrid's answer with an easy way to stop it
#objc func executeRepeatedly() {
print("--Do something on repeat--")
perform(#selector(executeRepeatedly), with: nil, afterDelay: 60.0)
}
Here's an example of how to start it and stop it:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
executeRepeatedly() // start it
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NSObject.cancelPreviousPerformRequests(withTarget: self) // stop it
}
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: myMethod)
func myMethod(_:Timer) {
...
}
or
timer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { _ in
...
}
make sure to invalid the timer at some point like your time is no longer visible, or you object is deist
I have two views in my swift app. I am performing a segue as below.
ViewController.swift -----------------> GameViewController.swift
When loading the GameViewController an value array also passed to GameViewController.swift from ViewController.swift
A timer should be initialized in GameViewController.swift
I tried to initialize a timer and call a method through it, but it doesn't work.
Followings are my code snippets.
ViewController.swift
func signIn(difficultyLvl:String){
let username = usernameTxt.text
let password = passwordTxt.text
let url = URL(string: "http://192.168.1.106/speed/scoreBoardController.php?username="+username!+"&password="+password!+"&action=SIGNIN")
let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in
let isPassed = String(data: data!, encoding:.utf8)?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
var gameViewControllerParams = [Int: [String: String]]()
gameViewControllerParams[0] = ["userId" : isPassed!]
gameViewControllerParams[1] = ["difficultyLvl" : difficultyLvl]
if(isPassed != "null"){
self.performSegue(withIdentifier: "gotoGame", sender: gameViewControllerParams)
}
}
task.resume()
}
GameViewController.swift
class GameViewController: UIViewController {
var gameViewControllerParams = [Int: [String: String]]()
override func viewDidLoad() {
super.viewDidLoad()
let _ = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(self.setCalculationLs), userInfo:nil,repeats: true)
}
func setCalculationLs(){
print("Timing")
}
}
Timers don't work on background queues (without some sleight of hand involving creating run loops or manually scheduling it on an existing run loop). But you should never initiate any UI update from anything other than the main queue, anyway.
So, since you're calling performSegue from a URLSession completion closure (which runs on a background queue), it's actually running viewDidLoad from the background queue, too. Thus the attempt to schedule the timer is failing. To get around this, you have to manually dispatch the performSegue code to the main queue:
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
...
if isPassed != "null" {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "gotoGame", sender: ...)
}
}
}
If you're ever unsure whether some code is running on the main queue or not, refer to the documentation. Or you can use a dispatch precondition:
dispatchPrecondition(condition: .onQueue(.main))
That way it will (in debug builds) stop the app if you've accidentally invoked the code from a background queue.
Unrelated to your current problem, but as an aside, to avoid a strong reference cycle between the timer and the view controller, you generally want to keep a reference to the timer so that you can invalidate it when the view disappears (e.g. create timer in viewDidAppear and remove it in viewDidDisappear). Otherwise you can end up retaining the GameViewController after it was dismissed, e.g.:
class GameViewController: UIViewController {
weak var timer: Timer?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timer = Timer.scheduledTimer(timeInterval: 1.0, target:self, selector: #selector(setCalculationLs(_:)), userInfo: nil, repeats: true)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
timer?.invalidate()
}
#objc func setCalculationLs(_ timer: Timer) {
print("Tick")
}
}
Or in iOS 10 or later, you can use the block-based variant with weak reference to self, and invalidate in deinit:
class GameViewController: UIViewController {
weak var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
self?.setCalculationLs()
}
}
deinit {
timer?.invalidate()
}
func setCalculationLs() {
print("Tick")
}
}
Swift 5.5
I was having this issue as well while working in a segued view controller. Since segues are executed as a background thread to the main thread (main view controller), running timer within the segued view controller won't work. So, that being known and also knowing timers don't work in background threads, I did some digging and found this Apple Documentation gem.
I added the following line right after my scheduled timer. The timer now works beautifully. It is just sending the timer to the main thread for execution.
myTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(self.updateWithTimer), userInfo: nil, repeats: true)
RunLoop.main.add(myTimer, forMode: .common)
I have a sliderValueChange function which updates a UILabel's text. I want for it to have a time limit until it clears the label's text, but I also want this "timed clear" action to be cancelled & restarted or delayed whenever the UISlider is moved within the time limit before the "timed clear" action takes place.
So far this is what I have:
let task = DispatchWorkItem {
consoleLabel.text = ""
}
func volumeSliderValueChange(sender: UISlider) {
task.cancel()
let senderValue = String(format: "%.2f", sender.value)
consoleLabel.text = "Volume: \(senderValue)"
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3, execute: task)
}
Obviously, this approach does not work, since cancel() apparently cannot be reversed.. (or at least I don't know how). I also don't know how to start a new task at the end of this function which will be cancelled if the function is recalled..
Am I going about this the wrong way? Is there something I am overlooking to make this work?
Use a timer:
weak var clearTimer: Timer?
And:
override func viewDidLoad() {
super.viewDidLoad()
startClearTimer()
}
func startClearTimer() {
clearTimer = Timer.scheduledTimer(
timeInterval: 3.0,
target: self,
selector: #selector(clearLabel(_:)),
userInfo: nil,
repeats: false)
}
func clearLabel(_ timer: Timer) {
label.text = ""
}
func volumeSliderValueChange(sender: UISlider) {
clearTimer?.invalidate() //Kill the timer
//do whatever you need to do with the slider value
startClearTimer() //Start a new timer
}
The problem is that you are cancelling the wrong thing. You don't want to cancel the task; you want to cancel the countdown which you got going when you said asyncAfter.
So use a DispatchTimer or an NSTimer (now called a Timer in Swift). Those are counters-down that can be cancelled. And then you can start counting again.
How is Siri is able to determine when I'm finished speaking. The reason I would like to know is that I would like to implement similar functionality with Apple's Speech Recognition API with my app. Is this doable, or is the only way to know when the user has stopped speaking is via user input?
You can use a timer, i had the same problem and I could not solve it with an elegant method.
fileprivate var timer:Timer?
func startRecordingTimer() {
lastString = ""
createTimerTimer(4)
}
func stopRecordingTimer() {
timer?.invalidate()
timer = nil
}
fileprivate func whileRecordingTimer() {
createTimerTimer(2)
}
fileprivate var lastString = ""
func createTimerTimer(_ interval:Double) {
OperationQueue.main.addOperation({[unowned self] in
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { (_) in
self.timer?.invalidate()
if(self.lastString.characters.count > 0){
//DO SOMETHING
}else{
self.whileRecordingTimer()
}
}
})
}
and in SFSpeechRecognitionTaskDelegate
public func speechRecognitionTask(_ task: SFSpeechRecognitionTask, didHypothesizeTranscription transcription: SFTranscription) {
let result = transcription.formattedString
lastString = result
}
I search to make vibrate twice my iphone when I click on a button (like a sms alert vibration)
With AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
I obtain just one normal vibration but I want two shorts :/.
Update for iOS 10
With iOS 10, there are a few new ways to do this with minimal code.
Method 1 - UIImpactFeedbackGenerator:
let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
feedbackGenerator.impactOccurred()
Method 2 - UINotificationFeedbackGenerator:
let feedbackGenerator = UINotificationFeedbackGenerator()
feedbackGenerator.notificationOccurred(.error)
Method 3 - UISelectionFeedbackGenerator:
let feedbackGenerator = UISelectionFeedbackGenerator()
feedbackGenerator.selectionChanged()
#import <AudioToolbox/AudioServices.h>
AudioServicesPlayAlertSound(UInt32(kSystemSoundID_Vibrate))
This is the swift function...See this article for detailed description.
This is what I came up with:
import UIKit
import AudioToolbox
class ViewController: UIViewController {
var counter = 0
var timer : NSTimer?
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func vibratePhone() {
counter++
switch counter {
case 1, 2:
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
default:
timer?.invalidate()
}
}
#IBAction func vibrate(sender: UIButton) {
counter = 0
timer = NSTimer.scheduledTimerWithTimeInterval(0.6, target: self, selector: "vibratePhone", userInfo: nil, repeats: true)
}
}
When you press the button, the timer starts and repeats at desired time interval. The NSTimer calls the vibratePhone(Void) function and from there I can control how many times the phone will vibrate. I used a switch in this case, but you could use a if else, too. Simply set a counter to count each time the function is called.
If you want to vibrate only two times. You can just..
func vibrate() {
AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
}
}
And vibrating multiple times can be possible by using recursion and AudioServicesPlaySystemSoundWithCompletion.
You can pass a count number to vibrate function like vibrate(count: 10). Then it vibrates 10 times.
func vibrate(count: Int) {
if count == 0 {
return
}
AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate) { [weak self] in
self?.vibrate(count: count - 1)
}
}
In case of using UIFeedbackGenerator, There is a great library Haptica
Hope it helps.