i am trying to make func completion to use it everywhere. I want to have only one tap on button per second.
i have code like this
var isAllowedToExit = true
if isAllowedToExit {
interactor.exitProfile()
isAllowedToExit = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
self?.isAllowedToExit = true
}
it works perfect, but it is a lot of code to use it everywhere
I am trying to do like this
func onlyOneTap(completion: () -> ()) {
var isAvailableToTap: Bool = true
if isAvailableToTap {
isAvailableToTap = false
completion()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
isAvailableToTap = true
}
}
then use it like this
onlyOneTap {
interactor.exitProfile()
}
but it doesnot even work. how can i fix my code, and do i have some variants to make it global like extension to use everywhere?
you can declare the button that you want to use function as outlet and use that function
#IBOutlet weak var someButtonOut: UIButton!
func onlyOneTap(buttonInput:UIButton) {
buttonInput.isEnabled = false
// your completion
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
buttonInput.isEnabled = true
}
}
Related
I am creating a simple card game (Set) in SwiftUI. I have a button that will deal X new cards when tapped. Currently, it makes all cards show up at once. I was wondering how I could make them come out one at a time.
Deal works by appending a new card to a Deck array in the model. ContentView displays each card in the grid.
This is what I currently have after looking online. Displays first card then next all at once
func deal(_ numberOfCards: Int) {
withAnimation(Animation.easeInOut(duration: 1)) {
viewModel.deal()
}
for _ in 1..<numberOfCards {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
withAnimation(.easeInOut) {
viewModel.deal()
}
}
}
}
Try this
func deal(_ numberOfCards: Int) {
withAnimation(Animation.easeInOut(duration: 1)) {
viewModel.deal()
}
for i in 1..<numberOfCards {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.7) {
withAnimation(.easeInOut) {
viewModel.deal()
}
}
}
}
The problem is that you’re starting all of them 0.7 seconds from now. You want to multiply that interval by the for loop index. You can probably also simplify it a bit, e.g.:
func deal(_ numberOfCards: Int) {
for i in 0..<numberOfCards {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.7) {
withAnimation(.easeInOut) {
viewModel.deal()
}
}
}
}
This pattern isn’t ideal, though, because if you dismiss the view in question, it’s still going to be trying to flip cards on view that isn’t present anymore. Also, this pattern of issuing multiple asyncAfter is not great because it’s subject to timer coalescing (where latter calls may be coalesced together to save battery). This latter issue might not be an issue here, but as a general rule, we use Timer to prevent coalescing and to be able to cancel the timer when the view is dismissed.
func deal(_ numberOfCards: Int) {
var cardNumber = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.7, repeats: true) { timer in
withAnimation(.easeInOut) {
viewModel.deal()
}
cardNumber += 1
if cardNumber >= numberOfCards {
timer.invalidate()
}
}
timer.fire()
}
If this was in a class, I might use [weak self] in the timer closure with
guard let self = self else {
timer.invalidate()
return
}
I have the following extension which I want to make usable for both an UITextView and an UILabel.
extension UITextView {
func setTextWithTypeAnimation(typedText: String, characterDelay: TimeInterval = 2.5) {
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)
}
}
}
I tried to set the type to UIView found here: Stack Overflow: Single extension for UITextView and UITextField in Swift
extension UIView {
func setTextWithTypeAnimation(typedText: String, characterDelay: TimeInterval = 2.5) {
if self is UILabel || self is UITextView {
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)
}
}
}
}
That of course doesn't work because UIView has no text property so I get the following error:
Use of unresolved identifier 'text'
How can I solve this?
That of course doesn't work because UIView has no text property
This is exactly the piece you want to think about. What do you need in order to write this method? Two things: it needs a text property. And it needs to be a class type (because you use a weak modifier on it). So say that.
protocol TypeAnimated: AnyObject {
var text: String? { get set }
}
Except that for historical reasons, UITextView has a String! while UILabel has a String?. That's very frustrating, but we can bridge the two with a new property:
protocol TypeAnimated: AnyObject {
var animatedText: String { get set }
}
Now, given that protocol, you can write you method:
extension TypeAnimated {
func setTextWithTypeAnimation(typedText: String, characterDelay: TimeInterval = 2.5) {
animatedText = ""
var writingTask: DispatchWorkItem?
writingTask = DispatchWorkItem { [weak weakSelf = self] in
for character in typedText {
DispatchQueue.main.async {
weakSelf?.animatedText!.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)
}
}
}
Now, you just need to label any types you want to conform to this protocol, and they'll get the extension.
extension UILabel: TypeAnimated {
var animatedText: String {
get { return text ?? "" }
set { text = newValue }
}
}
extension UITextView: TypeAnimated {
var animatedText: String {
get { return text ?? "" }
set { text = newValue }
}
}
As a side note, generating a new queue every time this is executed is almost certainly not what you mean. You should probably just set this up as a series of asyncAfter calls to the main queue, or use a Timer, or call asyncAfter inside the asyncAfter block. But none of this really impacts your question.
I haven't tested this, but this is how I would probably approach the problem:
extension TypeAnimated {
func setTextWithTypeAnimation(typedText: String, characterDelay: TimeInterval = 2.5/100) {
func addNextCharacter(from string: Substring) {
DispatchQueue.main.asyncAfter(deadline: .now() + characterDelay) { [weak self] in
if let self = self, let nextChar = string.first {
self.animatedText.append(nextChar)
addNextCharacter(from: string.dropFirst())
}
}
}
animatedText = ""
addNextCharacter(from: typedText[...])
}
}
I have view controller. Inside it I have view
lazy var statusView: StatusView = {
var statusView = StatusView()
return statusView
}()
Inside statusView I have button
lazy var backButton: UIButton = {
var button = UIButton(type: .system)
button.titleLabel?.font = UIFont().regularFontOfSize(size: 20)
return button
}()
In controller I have
override func viewDidLoad() {
super.viewDidLoad()
setupRx()
}
func setupRx() {
_ = statusView.backButton.rx.tap.subscribe { [weak self] in
guard let strongSelf = self else { return }
print("hello")
}
}
But when I tap to button, nothing get printed to console.
What am I doing wrong ?
In general nothing wrong, but there's a minor hidden trick.
You use
backButton.rx.tap.subscribe { [weak self] in
But you need to use
backButton.rx.tap.subscribe { [weak self] _ in ...
Did you notice underscore in the second version? The second version calls method
public func subscribe(_ on: #escaping (Event<E>) -> Void)
of ObservableType. In this case on closure to deliver an event is provided, but we just ignore incoming parameter of this closure using underscore
It looks like the subscription is going out of scope as soon as setupRx returns. If you add a DisposeBag to the view controller and add the subscription to the dispose bag, does that solve the problem? Something like this:
func setupRx() {
statusView.backButton.rx.tap
.subscribe { [weak self] in
guard let strongSelf = self else { return }
print("hello")
}
}
.addDisposableTo(self.disposeBag)
}
Hope that helps.
Working in Swift3; I've got a pretty expensive operation running in a loop iterating through stuff and building it into an array that on return would be used as the content for an NSTableView.
I wanted a modal sheet showing progress for this so people don't think the app is frozen. By googling, looking around in here and not a small amount of trial and error I've managed to implement my progressbar and have it show progress adequately as the loop progresses.
The problem right now? Even though the sheet (implemented as an NSAlert, the progress bar is in the accesory view) works exactly as expected, the whole thing returns before the loop is finished.
Here's the code, hoping somebody can tell me what am I doing wrong:
class ProgressBar: NSAlert {
var progressBar = NSProgressIndicator()
var totalItems: Double = 0
var countItems: Double = 0
override init() {
progressBar.isIndeterminate = false
progressBar.style = .barStyle
super.init()
self.messageText = ""
self.informativeText = "Loading..."
self.accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
self.accessoryView?.addSubview(progressBar)
self.layout()
self.accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
self.addButton(withTitle: "")
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width:290, height: 16))
progressBar.usesThreadedAnimation = true
self.beginSheetModal(for: ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window!, completionHandler: nil)
}
}
static var allUTIs: [SWDAContentItem] = {
var wrappedUtis: [SWDAContentItem] = []
let utis = LSWrappers.UTType.copyAllUTIs()
let a = ProgressBar()
a.totalItems = Double(utis.keys.count)
a.progressBar.maxValue = a.totalItems
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
a.countItems += 1.0
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
Thread.sleep(forTimeInterval:0.0001)
DispatchQueue.main.async {
a.progressBar.doubleValue = a.countItems
if (a.countItems >= a.totalItems && a.totalItems != 0) {
ControllersRef.sharedInstance.thePrefPane!.mainCustomView.window?.endSheet(a.window)
}
}
}
}
Swift.print("We'll return now...")
return wrappedUtis // This returns before the loop is finished.
}()
In short, you're returning wrappedUtis before the asynchronous code has had a chance to finish. You cannot have the initialization closure return a value if the update process itself is happening asynchronously.
You clearly successfully diagnosed a performance problem in the initialization of allUTIs, and while doing this asynchronously is prudent, you shouldn't be doing that in that initialization block of the allUTIs property. Move this code that initiates the update of allUTIs into a separate function.
Looking at ProgressBar, it's really an alert, so I'd call it ProgressAlert to make that clear, but expose the necessary methods to update the NSProgressIndicator within that alert:
class ProgressAlert: NSAlert {
private let progressBar = NSProgressIndicator()
override init() {
super.init()
messageText = ""
informativeText = "Loading..."
accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
accessoryView?.addSubview(progressBar)
self.layout()
accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
addButton(withTitle: "")
progressBar.isIndeterminate = false
progressBar.style = .barStyle
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width:290, height: 16))
progressBar.usesThreadedAnimation = true
}
/// Increment progress bar in this alert.
func increment(by value: Double) {
progressBar.increment(by: value)
}
/// Set/get `maxValue` for the progress bar in this alert
var maxValue: Double {
get {
return progressBar.maxValue
}
set {
progressBar.maxValue = newValue
}
}
}
Note, this doesn't present the UI. That's the job of whomever presented it.
Then, rather than initiating this asynchronous population in the initialization closure (because initialization should always be synchronous), create a separate routine to populate it:
var allUTIs: [SWDAContentItem]?
private func populateAllUTIs(in window: NSWindow, completionHandler: #escaping () -> Void) {
let progressAlert = ProgressAlert()
progressAlert.beginSheetModal(for: window, completionHandler: nil)
var wrappedUtis = [SWDAContentItem]()
let utis = LSWrappers.UTType.copyAllUTIs()
progressAlert.maxValue = Double(utis.keys.count)
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
DispatchQueue.main.async { [weak progressAlert] in
progressAlert?.increment(by: 1)
}
}
DispatchQueue.main.async { [weak self, weak window] in
self?.allUTIs = wrappedUtis
window?.endSheet(progressAlert.window)
completionHandler()
}
}
}
Now, you declared allUTIs to be static, so you can tweak the above to do that, too, but it seems like it's more appropriate to make it an instance variable.
Anyway, you can then populate that array with something like:
populateAllUTIs(in: view.window!) {
// do something
print("done")
}
Below, you said:
In practice, this means allUTIs is only actually initiated when the appropriate TabViewItem is selected for the first time (which is why I initialize it with a closure like that). So, I'm not really sure how to refactor this, or where should I move the actual initialization. Please keep in mind that I'm pretty much a newbie; this is my first Swift (also Cocoa) project, and I've been learning both for a couple of weeks.
If you want to instantiate this when the tab is selected, then hook into the child view controllers viewDidLoad. Or you can do it in the tab view controller's tabView(_:didSelect:)
But if the population of allUTIs is so slow, are you sure you want to do this lazily? Why not trigger this instantiation sooner, so that there's less likely to be a delay when the user selects that tab. In that case, you might trigger it the tab view controller's own viewDidLoad, so that the tab that needs those UTIs is more likely to have them.
So, if I were considering a more radical redesign, I might first change my model object to further isolate its update process from any specific UI, but rather to simply return (and update) a Progress object.
class Model {
var allUTIs: [SWDAContentItem]?
func startUTIRetrieval(completionHandler: (() -> Void)? = nil) -> Progress {
var wrappedUtis = [SWDAContentItem]()
let utis = LSWrappers.UTType.copyAllUTIs()
let progress = Progress(totalUnitCount: Int64(utis.keys.count))
DispatchQueue.global(qos: .default).async {
for uti in Array(utis.keys) {
wrappedUtis.append(SWDAContentItem(type:SWDAContentType(rawValue: "UTI")!, uti))
DispatchQueue.main.async {
progress.completedUnitCount += 1
}
}
DispatchQueue.main.async { [weak self] in
self?.allUTIs = wrappedUtis
completionHandler?()
}
}
return progress
}
}
Then, I might have the tab bar controller instantiate this and share the progress with whatever view controller needed it:
class TabViewController: NSTabViewController {
var model: Model!
var progress: Progress?
override func viewDidLoad() {
super.viewDidLoad()
model = Model()
progress = model.startUTIRetrieval()
tabView.delegate = self
}
override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
super.tabView(tabView, didSelect: tabViewItem)
if let item = tabViewItem, let controller = childViewControllers[tabView.indexOfTabViewItem(item)] as? ViewController {
controller.progress = progress
}
}
}
Then the view controller could observe this Progress object, to figure out whether it needs to update its UI to reflect this:
class ViewController: NSViewController {
weak var progress: Progress? { didSet { startObserving() } }
weak var progressAlert: ProgressAlert?
private var observerContext = 0
private func startObserving() {
guard let progress = progress, progress.completedUnitCount < progress.totalUnitCount else { return }
let alert = ProgressAlert()
alert.beginSheetModal(for: view.window!)
progressAlert = alert
progress.addObserver(self, forKeyPath: "fractionCompleted", context: &observerContext)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let progress = object as? Progress, context == &observerContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
dispatchPrecondition(condition: .onQueue(.main))
if progress.completedUnitCount < progress.totalUnitCount {
progressAlert?.doubleValue = progress.fractionCompleted * 100
} else {
progress.removeObserver(self, forKeyPath: "fractionCompleted")
view.window?.endSheet(progressAlert!.window)
}
}
deinit {
progress?.removeObserver(self, forKeyPath: "fractionCompleted")
}
}
And, in this case, the ProgressAlert only would worry about doubleValue:
class ProgressAlert: NSAlert {
private let progressBar = NSProgressIndicator()
override init() {
super.init()
messageText = ""
informativeText = "Loading..."
accessoryView = NSView(frame: NSRect(x:0, y:0, width: 290, height: 16))
accessoryView?.addSubview(progressBar)
self.layout()
accessoryView?.setFrameOrigin(NSPoint(x:(self.accessoryView?.frame)!.minX,y:self.window.frame.maxY))
addButton(withTitle: "")
progressBar.isIndeterminate = false
progressBar.style = .barStyle
progressBar.sizeToFit()
progressBar.setFrameSize(NSSize(width: 290, height: 16))
progressBar.usesThreadedAnimation = true
}
/// Set/get `maxValue` for the progress bar in this alert
var doubleValue: Double {
get {
return progressBar.doubleValue
}
set {
progressBar.doubleValue = newValue
}
}
}
I must note, though, that if these UTIs are only needed for that one tab, it raises the question as to whether you should be using a NSAlert based UI at all. The alert blocks the whole window, and you may want to block interaction with only that one tab.
I use an animation for specify a tip to help the interaction with delay using these:
let delay = 1.8 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
//call the method which have the steps after delay.
self.rain.alpha = 0
UIView.animateWithDuration(5, animations: {
self.rain.alpha = 1
})
self.tip.startAnimating()
}
But, I need to stop this delay process if, before animation start, user touch the screen.
iOS 8 and OS X Yosemite introduced dispatch_block_cancel that allow you to cancel them before they start executing
You declare one variable in class as follows:
var block: dispatch_block_t?
Init block variable and provide it in dispatch_after:
block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) {
print("I executed")
}
let time: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(5 * NSEC_PER_SEC))
dispatch_after(time, dispatch_get_main_queue(), block!)
After that you can cancel it as follows:
dispatch_block_cancel(block!)
Swift 3.0 Example DispatchQueue cancel or stop
var dispatchQueue: DispatchQueue?
var dispatchWorkItem: DispatchWorkItem?
func someOnClickButtonStart() {
self.dispatchQueue = DispatchQueue.global(qos: .background) // create queue
self.dispatchWorkItem = DispatchWorkItem { // create work item
// async code goes here
}
if self.dispatchWorkItem != nil {
self.dispatchQueue?.asyncAfter(
deadline: .now() + .seconds(1),
execute: self.dispatchWorkItem!
) // schedule work item
}
}
func someOnClickButtonCancel() {
if let dispatchWorkItem = self.dispatchWorkItem {
dispatchWorkItem.cancel() // cancel work item
}
}
Here's a general solution I wrote to cancel a dispatch_after in Swift:
typealias cancellable_closure = (() -> ())?
func dispatch_after(#seconds:Double, queue: dispatch_queue_t = dispatch_get_main_queue(), closure:()->()) -> cancellable_closure {
var cancelled = false
let cancel_closure: cancellable_closure = {
cancelled = true
}
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, Int64(seconds * Double(NSEC_PER_SEC))), queue, {
if !cancelled {
closure()
}
}
)
return cancel_closure
}
func cancel_dispatch_after(cancel_closure: cancellable_closure) {
cancel_closure?()
}
Usage:
let doSomethingLater = dispatch_after(seconds: 3.0) {
something()
}
....
if shouldCancelForSomeReason {
cancel_dispatch_after(doSomethingLater)
}
By default it runs on the main queue, but you can pass in a parameter for it to run on another queue:
let doSomethingLater = dispatch_after(seconds: 3.0, queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
something()
}
Just sharing, in Swift 4.x, I do this:
var block: DispatchWorkItem?
self.block = DispatchWorkItem { self.go(self) }
// execute task in 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: self.block!)
and then to cancel the block, self.block?.cancel()
Try this sample project:
import UIKit
class ViewController: UIViewController {
var block: DispatchWorkItem?
#IBAction func go(_ sender: Any) {
self.block?.cancel()
let vc2 = VC2()
self.navigationController?.pushViewController(vc2, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.block = DispatchWorkItem { self.go(self) }
// execute task in 2 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: self.block!)
}
}
class VC2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .green
}
}
You can create a boolean variable shouldCancelAnimation and test it inside the dispatch_after block to prevent the execution of your animation.
var shouldCancelAnimation = false // property of class
func runAnimation()
{
let delay = 1.8 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
if !self.shouldCancelAnimation
{
self.rain.alpha = 0
UIView.animateWithDuration(5, animations: {
self.rain.alpha = 1
})
self.tip.startAnimating()
}
self.shouldCancelAnimation = false
}
}
func viewWasTouched() // This could be touches began or taprecognizer event
{
shouldCancelAnimation = true
}