How can I continue a timer in Today extension when the user swipes back up and closes the view? Every time I close my view, my label resets back to five minutes. The timer doesn't necessarily have to animate while the view is disappeared, but when the view loads again I would like the animation showing the updated time that passed while the view was gone to be smooth and fast. How can I keep it counting down in the background? What's the most efficient and logical way to do this? Thank you.
Here is my code thus far:
class TodayViewController: UIViewController, NCWidgetProviding {
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
#IBOutlet weak var timerLabel: UILabel!
var counter = 300
var newCount = Int()
var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateInfo), userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
}
func updateInfo(){
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
self.timerLabel.text = formatter.string(from: TimeInterval(self.counter))
counter -= 1
print(counter)
}
func widgetPerformUpdate(completionHandler: (#escaping (NCUpdateResult) -> Void)) {
completionHandler(NCUpdateResult.newData)
}
override func viewDidDisappear(_ animated: Bool) {
counter -= 1
}
}
Related
I am working on an attendance app that uploads and stores all of its data in firebase. Most things are currently working except that sometimes when a switch is pressed in the array of students in the club, 2 of the switches activate in the array but the information normally sent when a switch is pressed is only sent for the switch that I actually pressed.
Here is the TableView cell code:
import UIKit
import Firebase
class StudentCell: UITableViewCell {
var student: Student? { didSet { self.updateUI() }}
#IBOutlet weak var lblStudentName: UILabel!
#IBOutlet weak var lblStudentId: UILabel!
#IBOutlet weak var lblStudentAttending: UISwitch!
let ref = Database.database().reference()
var user: Student! {
didSet{
lblStudentName.text = user.FullName()
lblStudentId.text = user.StudentId
lblStudentAttending.isOn = false
}
}
var switchReference: DatabaseReference?
var switchHandle: DatabaseHandle?
func stopObservation() {
if let handle = switchHandle {
switchReference?.removeObserver(withHandle: handle)
}
}
func startObservation() {
stopObservation()
if let uid = student?.StudentId {
switchReference = ref.child("attendance").child(Global.shared.currentDate).child(uid)
switchReference?.observe(.value, with: { [weak self] (snapshot: DataSnapshot) in
DispatchQueue.main.async {
let isOn = (snapshot.value as? Bool) ?? false
self?.lblStudentAttending.isOn = isOn
}
})
}
}
func updateUI() {
startObservation()
self.lblStudentName.text = self.student?.FullName() ?? ""
self.lblStudentId.text = self.student?.StudentId ?? ""
}
#IBAction func `switch`(_ sender: UISwitch) {
switchReference?.setValue(sender.isOn)
}
}
Does anyone know why this might be happening? If you need more information lmk. TIA -Ben
Does anyone know why this might be happening?
Tables reuse their cells so that they don't have to keep allocating new ones, which is expensive when the user is trying to scroll through a lot of rows quickly. You're setting up an observer for the switch in each cell, which means that you need to stop observing that switch when the cell is no longer visible. You could probably call stopObservation() in the cell's prepareForReuse() method and solve the problem.
A better solution would be to avoid observing the switch altogether, and instead use the target/action mechanism that controls typically use to trigger an action when the user changes their value. That way you don't have to worry about constantly starting and stopping observation.
I am working on a ToDo List application. Currently, the ToDo list shows in Table View as a list and the Detail View shows only the first item, a completed switch and the date completed. I want my Detail View to show all of the items and dates completed, not just the first item.
My app delegate looks like this:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let splitViewController = self.window!.rootViewController as! UISplitViewController
let navigationController = splitViewController.viewControllers.first as! UINavigationController
let masterViewController = navigationController.topViewController as! MasterViewController
let detailViewController = splitViewController.viewControllers.last as! DetailViewController
let firstItem = masterViewController.listManager.getFirstItem()
detailViewController.toDoItem = firstItem
return true
}
Now, I've tried to change firstItem to toDoItem to show my whole array but I am getting an error:
Cannot assign value of type '[ToDoItem]' to type 'ToDoItem!'
Also, what to I create in Detail View to accommodate an every changing array?
This is my current Detail View:
import UIKit
class DetailViewController: UIViewController {
#IBOutlet weak var descriptionLabel: UILabel!
#IBOutlet weak var dateAddedLabel: UILabel!
#IBOutlet weak var completedSwitch: UISwitch!
var toDoItem: ToDoItem! {
didSet(newItem) {
self.refreshUI()
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
refreshUI()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func refreshUI() {
if(toDoItem == nil) {
return
}
descriptionLabel?.text = toDoItem.itemDescription
completedSwitch?.setOn(toDoItem.completed, animated: true)
dateAddedLabel?.text = toDoItem.getDateAdded()
}
Im creating a quiz app to where once you answer a question correctly it segues to another view controller where there is a label that says "correct answer!" and a contains a button that label reads "Continue?". One you press the button it sends you back to the original view controller where the questions are asked. How ever once you segue from the viewcontroller with the "Continue?" button back to the view controller that asks the questions the data is reset and the user is asked questions they have already answered. How could I make it to where the "continue?" button view controller keeps track of the completed questions so they are not asked again in the question view controller? I am new to swift so would I use a delegate or protocol? Or are those things the same? Ive correctly made it segue from the question view controller to the "continue" view controller however I want the data to stay updated once I segue back to the question view controller so no questions that have already been answered are asked again.
Here is my code from the question view controller:
Import UIKit
class ViewController: UIViewController {
var questionList = [String]()
func updateCounter() {
counter -= 1
questionTimer.text = String(counter)
if counter == 0 {
timer.invalidate()
wrongSeg()
counter = 15
}
}
func randomQuestion() {
//random question
if questionList.isEmpty {
questionList = Array(QADictionary.keys)
questionTimer.text = String(counter)
}
let rand = Int(arc4random_uniform(UInt32(questionList.count)))
questionLabel.text = questionList[rand]
//matching answer values to go with question keys
var choices = QADictionary[questionList[rand]]!
questionList.remove(at: rand)
//create button
var button:UIButton = UIButton()
//variables
var x = 1
rightAnswerBox = arc4random_uniform(4)+1
for index in 1...4
{
button = view.viewWithTag(index) as! UIButton
if (index == Int(rightAnswerBox))
{
button.setTitle(choices[0], for: .normal)
}
else {
button.setTitle(choices[x], for: .normal)
x += 1
}
randomImage()
}
}
let QADictionary = ["Who is Thor's brother?" : ["Atum", "Loki", "Red Norvell", "Kevin Masterson"], "What is the name of Thor's hammer?" : ["Mjolinr", "Uru", "Stormbreaker", "Thundara"], "Who is the father of Thor?" : ["Odin", "Sif", "Heimdall", "Balder"]]
//wrong view segue
func wrongSeg() {
performSegue(withIdentifier: "wrongViewSegue", sender: self)
}
//proceed screen
func continueSeg() {
performSegue(withIdentifier: "continueSeg", sender: self)
}
//variables
var rightAnswerBox:UInt32 = 0
var index = 0
//Question Label
#IBOutlet weak var questionLabel: UILabel!
//Answer Button
#IBAction func buttonAction(_ sender: AnyObject) {
if (sender.tag == Int(rightAnswerBox))
{
counter = 15
questionTimer.text = String(counter)
print ("Correct!")
continueSeg()
}
else if (sender.tag != Int(rightAnswerBox)) {
wrongSeg()
print ("Wrong!")
timer.invalidate()
questionList = []
}
randomQuestion()
}
override func viewDidAppear(_ animated: Bool)
{
randomQuestion()
}
//variables
var counter = 15
var timer = Timer()
#IBOutlet weak var questionTimer: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
timer = Timer.scheduledTimer(timeInterval: 1, target:self, selector: #selector(ViewController.updateCounter), userInfo: nil, repeats: true)
}
Here is my code for the "continue" view controller (all it contains right now is a segue back to the question view controller but I want it to keep track of which questions have already been answered by storing the data from the array of questions in the question view controller):
import UIKit
class ContinueView: UIViewController {
//correct answer label
#IBOutlet weak var correctLbl: UILabel!
//background photo
#IBOutlet weak var backgroundImage: UIImageView!
func backToQuiz() {
performSegue(withIdentifier: "continueSeg", sender: self)
}
#IBAction func `continue`(_ sender: Any) {
backToQuiz()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
Move your call to randomQuestion from viewDidAppear to viewDidLoad. The viewDidLoad only called once, but viewDidAppear will always be called every time the controller is shown.
Also take note that Apple says
If a view controller is presented by a view controller inside of a popover, this method is not invoked on the presenting view controller after the presented controller is dismissed.
Take a look on discussion about the differences
In my ViewController.swift I have this method which presents a viewcontroller modally after the animations for the boxes are completed:
#objc func sceneTapped(recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: sceneView)
let hitResults = sceneView.hitTest(location, options: nil)
if hitResults.count > 0 {
let result = hitResults[0]
let box = result.node
let fadeAction = SCNAction.fadeOut(duration: 1.0)
let rotateAction = SCNAction.rotate(by: CGFloat(Double.pi*2), around: SCNVector3(x: 0.5, y: 1.5, z: 0), duration: 1.0)
let groupActions = SCNAction.group([rotateAction, fadeAction])
if box.name == Present.left.rawValue || box.name == Present.middle.rawValue || box.name == Present.right.rawValue {
box.runAction(groupActions, completionHandler: presentWebView)
}
}
}
presentWebView uses the storyboard to instantiate the viewcontroller:
func presentWebView(){
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "webViewController")
self.present(vc, animated: true, completion: nil)
}
But every time the app crashes on self.present(vc, animated: true, completion: nil) with EXC_BREAKPOINT (code=1, subcode=0x1a1579b50)
HOWEVER, when I don't present the view controller in the completion hander, i.e.:
#objc func sceneTapped(recognizer: UITapGestureRecognizer) {
presentWebView()
}
This works completely fine
So I don't understand why presenting a view controller in a completion handler causes a crash.
Any help would be greatly appreciated!
the SCNAction completion handler will not be called in the main thread.
Per UIKit guidelines your UI code needs to run on the main thread. You should be able to do that using DispatchQueue.main.async { ... }
I have searched around but have not found an answer. It may be simple but I can nor figure out how you are suposed to do it. So...
I want a button to show an array on a label. When I press the first time, the label show the first number in the array, an pressing a second time makes the label print the second number in the array.
var primeString = ["60","52","81","61","85"]
#IBOutlet var PrimeLabel: UILabel!
#IBAction func NewAction(sender: AnyObject) {
// Here is where I want to make the label show the array in the order when I press it.
}
Declare a variable count and initialise to 0
var primeString = ["60","52","81","61","85"]
var count = 0
#IBOutlet var PrimeLabel: UILabel!
And then action for Button Click.
#IBAction func NewAction(sender: AnyObject) {
PrimeLabel.text = primeString[count%primeString.count]
count++
}
So just to get this straight. Each time you press the button you want to display the next element in the array through the label. Okay, that should be fairly simple. Something like below will do that for you.
var primeString = ["60","52","81","61","85"]
var currentElement = 0
#IBOutlet var PrimeLabel: UILabel!
#IBAction func NewAction(sender: AnyObject) {
if currentElement < primeString.count {
PrimeLabel.text = primeString[currentElement]
currentElement++
} else {
print("No more elements to display.")
}
}
No need to have the view did load, as we don't need to do any setup once the view has loaded. I hope this helps you out.
Why don't you use Generator for this?
E.g. something like:
var primeStringGenerator = ["60","52","81","61","85"].generate()
#IBOutlet var PrimeLabel: UILabel!
#IBAction func NewAction(sender: AnyObject) {
PrimeLabel.text = primeGenerator.next() ?? "n/a"
}
var primeString = ["60","52","81","61","85"]
#IBOutlet var PrimeLabel: UILabel!
var number = 0
#IBAction func NewAction(sender: AnyObject) {
PrimeLabel.text = primeString[i]
i++
}