Temporarily suspend user from making another request - nsdate

I'm building a swift iOS-based app that limits users to submit only one request per 10 minutes.I'm suck now translating these three points to code
1- user clicks a button
2- button is disabled for 10 minute
3- button is enabled
I'm not expecting the full code :) just a method or advice.
Thank you

First you need to keep the state in persistent data, incase user go to another controller and go back, the button should still disabled.
class PersistentData {
static let sharedInstance = PersistentData();
var disableSubmitButton = false
}
Then in your controller
override func viewDidLoad() {
// each load need to check
if PersistentData.sharedInstance.disableSubmitButton == true {
submitButton.isEnabled = false
}
}
func onButtonClicked() {
// change button to disable
submitButton.isEnabled = false
// set state in persistent data so it can be the same wherever controller you go
PersistentData.sharedInstance.disableSubmitButton = true
// now set the timer to enable back
Timer.scheduledTimer(timeInterval: 10.0 * 60, target: self, selector: #selector(self.updateButtonState), userInfo: nil, repeats: false);
}
func updateButtonState() {
// update value in persitence data
PersistentData.sharedInstance.disableSubmitButton = false
// change button to enable back
submitButton.isEnabled = true
}

Note the following: timeInterval is in seconds so 60*10 is 10 minutes. UI Updates need to happen on the main thread which is why the time block gets dispatched back to main.
#IBAction func touchButton(_ sender: AnyObject) {
button.isEnabled = false
Timer.scheduledTimer(withTimeInterval: 60*10, repeats: false) { _ in
DispatchQueue.main.async {
self.button.isEnabled = true
}
}
}

Related

Chrome Extension | Multiple alarms going off at once

I am creating a task reminder extension. The user has an option to keep adding tasks and set reminders for each task.
I am using chrome.storage to store these tasks and using onChanged listener on storage to create an alarm for each task added to the storage.
But the issue is that if I set a reminder of 2 mins for a task and 3 mins for another task. Then at the end of 2 mins I am getting notification for both the tasks and at the end of 3mins I again get notifications for both the tasks.
background.js
chrome.storage.onChanged.addListener(function(changes, namespace) {
let id = (changes.tasks.newValue.length)-1
let data = changes.tasks.newValue[id]
if(data.task && data.hrs && data.min){
let totalMins = (parseInt(data.hrs*60))+parseInt(data.min)
let alarmTime = 60*1000*totalMins
chrome.alarms.create("remind"+id,{when:Date.now() + alarmTime})
}
chrome.alarms.onAlarm.addListener(()=>{
let notifObj = {
type: "basic",
iconUrl:"./images/logo5.png",
title: "Time to complete you task",
message: data.task
}
chrome.notifications.create('remindNotif'+id, notifObj)
})
popup.js
let hrs = document.querySelector("#time-hrs")
let min = document.querySelector("#time-min")
let submitBtn = document.querySelector("#submitBtn")
let task = document.querySelector("#task")
hrs.value = 0;
min.value = 1
hrs.addEventListener('change',()=>{
if (hrs.value < 0){
hrs.value =0;
}
})
min.addEventListener('change',()=>{
if (min.value < 1){
min.value = 1;
}
})
submitBtn.addEventListener("click", ()=>{
if(task.value){
chrome.storage.sync.get('tasks',(item)=>{
let taskArr = item.tasks ? item.tasks : []
linkArr.push({task:task.value, hrs:hrs.value, min:min.value})
chrome.storage.sync.set({ 'tasks' : taskArr })
})
};
});
manifest.json
{
"name" : "Link Snooze",
"description" : "This extension reminds you to open your saved links",
"manifest_version":2,
"version":"0.1.0",
"icons":{
"16":"./images/logo5.png",
"48":"./images/logo5.png",
"128":"./images/logo5.png"
},
"browser_action":{
"default_popup":"popup.html",
"default_icon":"./images/logo5.png"
},
"permissions":["storage", "notifications","alarms"],
"background" : {
"scripts": ["background.js"],
"persistent" : false
},
"options_page":"options.html"
}
Problem.
You register a new onAlarms listener when the storage changes in addition to the old listeners. All of them run each time one alarm is triggered.
Solution.
When using a non-persistent background script, all API listeners must be registered just once for the same function and it must be done synchronously, not inside an asynchronous callback or await or then(), otherwise the event will be lost when the background script auto-terminates and then wakes up for this event. The convention is to do it at the beginning of the script. The reason it worked for you until now is that the background script is kept alive while the popup is open or while devtools for the background script was open.
Such listeners evidently won't be able to use the variables from an asynchronous callback directly like data.task in your code. The solution is to use a different method of attaching data to an event, for example, create the alarm with a name that already contains the data, specifically data.task.
chrome.alarms.create(data.task, {delayInMinutes: hrs * 60 + min});
onAlarm event provides the alarm as a parameter so you can use its name, see the documentation.
Random hints:
An object can be used as an alarm name if you call JSON.stringify(obj) when creating and JSON.parse(alarm.name) in onAlarm.
In the popup, instead of manually adjusting out-of-range values, use a number input in html:
<input id="time-min" type=number min=0 max=59 step=1>
Then read it as a number: document.querySelector("#time-min").valueAsNumber || 0

Swift Combine - delaying a publisher

TL;DR
I want to delay a publication, but can't figure out how to, er, combine the parts
In Brief
I have a Publisher
let generator = PassthroughSubject<Bool, Never>()
and want somehow to use the modifier
.delay(for: 2, scheduler: RunLoop.main)
so that when I call
generator.send(true)
the message is sent two seconds after the call to send()
Looking at the docs for Publishers.Delay made the type error more clear, but doesn't help me to find the right way to hook things up.
Code
import SwiftUI
import Combine
// Exists just to subscribe.
struct ContainedView : View {
private let publisher: AnyPublisher<Bool, Never>
init(_ publisher: AnyPublisher<Bool, Never> = Just(false).dropFirst().eraseToAnyPublisher()) {
self.publisher = publisher
}
var body: some View {
Rectangle().onReceive(publisher) { _ in print("Got it") }
}
}
struct ContentView: View {
let generator = PassthroughSubject<Bool, Never>()
// .delay(for: 2, scheduler: RunLoop.main)
// Putting it here doesn't work either.
var body: some View {
VStack {
Button("Tap") {
// Does not compile
self.generator.delay(for: 2, scheduler: RunLoop.main).send(true)
// Value of type 'Publishers.Delay<PassthroughSubject<Bool, Never>, RunLoop>' has no member 'send'
// https://developer.apple.com/documentation/combine/publishers/delay
// Does not compile
self.generator.send(true).delay(for: 2, scheduler: RunLoop.main)
// Value of tuple type '()' has no member 'delay'
// Just a broken-up version of the first try.
let delayed = self.generator.delay(for: 2, scheduler: RunLoop.main)
delayed.send(true)
// This, of course, builds and works.
self.generator.send(true)
print("Sent it")
}
ContainedView(generator.eraseToAnyPublisher())
.frame(width: 300, height: 200)
}
}
}
You can use the debounce property of a publisher to delay the publishing.
$yourProperty
.debounce(for: 0.8, scheduler: RunLoop.main)
.eraseToAnyPublisher()
.delay(for: 2, scheduler: RunLoop.main) is likely exactly what you need, but it'll be key to see how you're subscribing to fully understand the issue. Delay doesn't delay the sending of the value when using send() with a subject - that's a link for imperative code and sends the data whenever send is invoked, typically against some already existing subscription.
While you have a subscriber in the first bit of code, there isn't one with the subject to pin these together.
For example, if you updated:
Just(false).dropFirst().eraseToAnyPublisher()
to
Just(false).dropFirst().eraseToAnyPublisher().delay(for: 2, scheduler: RunLoop.main)
Then the print statement should trigger ~2 second after the the init() was invoked. Depending on what you're trying to accomplish here, using a closure trigger such as onAppear might make a lot more sense, having that call the subject.send(), which you can then delay as you like in the publisher chain that happens before whatever subscribes to it.
var cancellables: [AnyCancellable] = []
let generator = PassthroughSubject<Bool, Never>()
let generated = generator.delay(for: 2, scheduler: RunLoop.main).sink { value in
print(value.description + " " + Date().timeIntervalSinceReferenceDate.description)
}
print(Date().timeIntervalSinceReferenceDate.description)
generator.send(true)
generator.send(false)
output
641453284.840604
true 641453286.841731
false 641453286.847715

UISlider with buffering progress - AVPlayer

I'm using AVPlayer to stream audio from a server and what I want to do now is to show a custom UISlider who shows the progress of the buffering.
Anyone come up with this? Please give me solution.
Now player load whole duration in once and show in UISlider.
you need to use preferredforwardbufferduration
This property defines the preferred forward buffer duration in seconds. If set to 0, the player will choose an appropriate level of buffering for most use cases. Setting this property to a low value will increase the chance that playback will stall and re-buffer, while setting it to a high value will increase demand on system resources.
also check here for detailed example of how to use : Is it possible to pause and resume buffering of AVPlayer?
inside your player you need to observe time range using below function
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
like this:
let timeRanges = change![NSKeyValueChangeKey.newKey] as? NSArray
if timeRanges != nil && timeRanges!.count != 0 {
DispatchQueue.main.async {
let cmTimeRange = (timeRanges![0] as AnyObject).timeRangeValue as CMTimeRange
self.playerView.timeSlider.bufferValue = Float(CMTimeGetSeconds(cmTimeRange.start) + CMTimeGetSeconds(cmTimeRange.duration))
}
}
I have a custom slider with a variable I defined as bufferValue you can override func draw(_ rect: CGRect)inside your slider and apply changes while buffer is progressing.
I get draw to call anytime bufferValue changes by calling self.setNeedsDisplay()inside didSet
#IBInspectable var bufferValue : Float = 0 {
didSet {
if(bufferValue < 0 ) {
bufferValue = 0
} else if (bufferValue > maximumValue) {
bufferValue = maximumValue
}
self.setNeedsDisplay()
}
}

Swift: command center not working, even though I explicitly set buttons to enable?

Ok, having some problems with Apple's command center here, playing background audio/on lock screen and cant understand why. Seems pretty simple but I don't even have the episode information displaying reliably in command center, definitely cant play/pause.
First I start the Audio session:
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: .mixWithOthers)
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
then I set up my command center buttons to be enabled explicitly:
UIApplication.shared.beginReceivingRemoteControlEvents()
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.isEnabled = true
// commandCenter.previousTrackCommand.isEnabled = true
// commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action:#selector(togglePlayPause))
// commandCenter.nextTrackCommand.addTarget(self, action:#selector(nextTrackForward))
// commandCenter.previousTrackCommand.addTarget(self, action:#selector(nextTrackBack))
commandCenter.skipForwardCommand.addTarget(self, action:#selector(ffPressed))
commandCenter.skipBackwardCommand.addTarget(self, action:#selector(rwPressed))
Here is the lldb plist keys if that matters for audio:
What's wrong here?
Maybe your question is old but someone could need this too, so this is how I did it :
Swift 5
let player = AVPlayer()
func commandCenter () {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in self.play(); return .success }
commandCenter.pauseCommand.addTarget { (commandEvent) -> MPRemoteCommandHandlerStatus in self.pause(); return .success }
}
func pause () {
player.pause ()
print("paused")
}
func play () {
player.play()
print("play")
}
You don't need to enable commandCenter.playCommand to true

disable/hide UISearchBar when datasource is nil

I want to make sure that no search bar is displayed when the datasource of my table view is empty. (Makes sense, no? shouldn't that be default behaviour?)
Here's a piece of my code that tries (currently uncommented) different things to accomplish that, but somehow it doesn't work.
Can anybody advise me what I'm doing wrong? Let me know if you need more snippets.
messagesArray=loadMessages()
DispatchQueue.main.async {
if (self.messagesArray.count==0){
self.noMessageview.isHidden=false
//self.searchController.searchBar.isHidden = true
//self.searchController.isActive = false
} else{
self.noMessageview.isHidden=true
//self.searchController.searchBar.isHidden = false
//self.searchController.isActive = true
}
self.spinner.stopAnimating()
self.refreshControl.endRefreshing()
self.tableView.reloadData()
}
UPDATE:
I declare the search controller like this:
let searchController = UISearchController(searchResultsController: nil)
and in the ViewDidLoad I do:
navigationItem.searchController = searchController
I believe you are using iOS 11 because of setting UISearchController from navigationItem, Thus you can use the following code to remove it:
if #available(iOS 11.0, *) {
self.navigationItem.largeTitleDisplayMode = .never;
self.navigationItem.searchController = nil
} else {
// Fallback on earlier versions
self.navigationItem.titleView = nil
};
I had some problem and i think its iOS 11 bug, when removing the UISearchController, the view doesn't get adjusted thus i had to call this right before removing UISearchController.
self.navigationItem.largeTitleDisplayMode = .never;
Thats all.

Resources