I am using xcode 8 and swift 3. I have made a mini game where you have to answer questions as fast as you can. You are timed by a stopwatch. I have the code to actually store the times(eg 23 seconds) but instead of storing the time, it replaces it with "Label". My stopwatch label is called down and the label that displays the values is called resultLabel I have been told the problem is when i set the text of the label. Any ideas on where i need to fix it. An example is ["Label", "Label", "Label"]
Stopwatch code:
if timerSecond != nil {
timerSecond?.invalidate()
timerSecond = nil
} else {
timerSecond = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.currentTime += 1
let minutePortion = String(format: "%02d", self.currentTime / 60 )
let secondsPortion = String(format: "%02d", self.currentTime % 60 )
self.down.text = "\(minutePortion):\(secondsPortion)"
}
}
extension for User Defaults:
extension UserDefaults {
var timesSecond: [String] {
get {
if let times = UserDefaults.standard.object(forKey: "times") as? [String] {
return times
} else {
return []
}
}
set {
UserDefaults.standard.set(newValue, forKey: "times")
}
}
}
Button code to display the array of values:
let arrayOfTimes = UserDefaults.standard.timesSecond
resultLabel.text = "\(arrayOfTimes)"
code for storing the data:
UserDefaults.standard.timesSecond.append(resultLabel.text!)
Related
I am looking for some advice when it comes to saving a score in my app - I currently have a coin count working in which through my HUD class when I collide through Coins the value increases accordingly. My next step is trying to initiate a high-score that initiates, after some research I think the topic I am looking into is based around NSUserDefaults..
The current code :
import SpriteKit
class HUD: SKNode {
//An SKLabelNode to print the coin score:
let coinCountText = SKLabelNode(text: "000000")
func createHUDNodes(){
// Configure the coin text label:
coinCountText.fontName = "STHupo-Heavy-Italic"
let coinTextPosition = CGPoint(x: -cameraOrigin.x + 770, y: coinPosition.y)
coinCountText.position = coinTextPosition
coinCountText.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.left
coinCountText.verticalAlignmentMode = SKLabelVerticalAlignmentMode.center
// Add the text label and coin icon to the HUD:
self.addChild(coinCountText)
}
func setCoinCountDisplay(newCoinCount:Int) {
let formatter = NumberFormatter()
let number = NSNumber(value: newCoinCount)
formatter.minimumIntegerDigits = 6
if let coinStr =
formatter.string(from: number) {
// Update the label node with the new count:
coinCountText.text = coinStr
}
}
Working in the HUD class I think naturally I would need to create another 'SKlabelNode'for the highscore node which I can duplicate from the code that works above.. my problem is how can I implement a highscore feature in the app, as when closed and re-opening the application it will remember the highscore..
I assume the following code would need to be added but I do not understand how it ties together in the class.
var currentHighScore =
NSUserDefaults.standardUserDefaults().integerForKey("coin_highscore")
var highScoreLabel = SKLabelNode(fontNamed:"STHupo-Heavy-Italic")
highScoreLabel.text = ""
highScoreLabel.fontSize = 45
highScoreLabel.position = CGPoint(x: viewSize.width * 0.5, y: viewSize.height * 0.30)
self.addChild(highScoreLabel)
if (score > currentHighScore) {
NSUserDefaults.standardUserDefaults().setInteger(score, forKey: "player_highscore")
NSUserDefaults.standardUserDefaults().synchronize()
highScoreLabel.text = "New High Score: \(score) !"
} else {
highScoreLabel.text = "Better Luck Next Time: \(score)"
}
}
Any advice on how I could integrate this code would be much appreciated.
, AJ
Revised :
else if nodeTouched.name == "returnToMenu"{
// Transition to the main menu scene
self.view?.presentScene(MenuScene(size: self.size),transition.crossFade(withDuration: 0.6))
let currentHighScore = UserDefaults.standard.integer(forKey: "Player_highscore")
let highScoreLabel = SKLabelNode(fontNamed:"STHupo-Heavy-Italic")
highScoreLabel.text = "Value: \ (String(describing: coin.value))"
highScoreLabel.text = ""
highScoreLabel.fontSize = 45
highScoreLabel.zPosition = 100
highScoreLabel.position = CGPoint(x: 50, y: 50)
self.addChild(highScoreLabel)
if (coin.value > currentHighScore) {
UserDefaults.standard.set(coin.value, forKey: "Player_highscore")
UserDefaults.standard.synchronize()
highScoreLabel.text = "New High Value: \(String(describing: coin.value)) !"
} else {
highScoreLabel.text = "Better Luck Next Time: \(String(describing: coin.value))"
}
}
}
}
I am assuming it's the referencing to my original HUD that is causing the problem? In my coin class I declared the coin.value = 5, each time the coin is collected by coinCountDisplay increases accordingly. However still does not show - :/
Add a property to your view controller with both get and set and use it in all the places. The get method should get the value from UserDefaults and the set should update the value in UserDefaults.
var coinHighscore: Int {
get {
UserDefaults.standard.integer(forKey: "coin_highscore")
}
set {
UserDefaults.standard.set(newValue, forKey: "coin_highscore")
}
}
Extending on Frankenstein's answer, IF you want to use Swift 5 you can use Property Wrappers to avoid writing get and set every time:
#propertyWrapper struct UserDefaultsBacked<Value> {
let key: String
let defaultValue: Value
var storage: UserDefaults = .standard
var wrappedValue: Value {
get {
let value = storage.value(forKey: key) as? Value
return value ?? defaultValue
}
set {
storage.setValue(newValue, forKey: key)
}
}
}
And use it like this:
#UserDefaultsBacked(key: "coin_highscore", defaultValue: 0)
var coinHighscore: Int
I have a pop up that is supposed to show the user instructions with letter by letter animation. The problem is whenever the user clicks "next" the letters get mixed up with the previous text.
The animation code:
extension UILabel {
func animate(newText: String, characterDelay: TimeInterval) {
DispatchQueue.main.async {
self.text = ""
for (index, character) in newText.enumerated() {
DispatchQueue.main.asyncAfter(deadline: .now() + characterDelay * Double(index)) {
self.text?.append(character)
}
}
}
}
The button action/calling method (as you can see I tried to empty the variable each time the user clicks "next" but it didn't work out)
#IBAction func nextbtt(_ sender: Any) {
var instructions = ["text"]
counter = counter + 1
var w1 = " لكن الوصول إليه يتطلب مواجهة وحل تحديات مختلفة"
var w2 = "هل بإمكانك مساعدتي في الحصول على الكنز؟"
let userId = UserDefaults.standard.object(forKey: "userId") as? String
ref = Database.database().reference()
let userLang = ref.child("users").child(userId!).child("lang")
userLang.observeSingleEvent(of: .value, with: { (snapshot) in
let lang = snapshot.value as? Int
if(lang==1){
////////////// if user's langague is English
w1 = "But finding it requires confronting and solving different challenges"
w2 = " Could you help me in getting the treasure" }
instructions.append(w1)
instructions.append(w2)
if(self.intrCounter < 3){
self.mytext.text = ""
var new = instructions[self.intrCounter]
self.mytext.text = new
self.mytext.animate(newText: new ?? "May the source be with you", characterDelay: 0.1)
self.intrCounter = self.intrCounter + 1
if(self.intrCounter == 3){
if(lang==1){
(sender as AnyObject).setBackgroundImage(UIImage(named: "engready"), for: UIControl.State.normal)
}
else {
(sender as AnyObject).setBackgroundImage(UIImage(named: "ready"), for: UIControl.State.normal)}
}
}
else{
}
})
if ( counter == 4){
status[0] = true
popUp.removeFromSuperview()
}
}
Screenshots:
one: text is showing to the user, user clicks "next" at the middle of the animation
two: when user clicks next before text 1 is complete
Code:
The issue occurs because you are calling an asynchronous block for each character and probably each char takes another amount of time.
Just to test this try this change, if it will help change the code accordingly :
var someCounter = 1
extension UILabel {
func animate(newText: String, characterDelay: TimeInterval) {
DispatchQueue.main.async {
self.text = ""
for (index, character) in newText.enumerated() {
someCounter += 1
DispatchQueue.main.asyncAfter(deadline: .now() +
someCounter + characterDelay * Double(index)) {
self.text?.append(character)
}
}
}
}
Let me know if this is indeed the issue, if so I will upload a more optimised code.
I am trying to add up a value that is entered in the text field with a value specified as a double and then returning the value on a label. The code that I have is :
#IBOutlet weak var enterField: UITextField!
var weekOneTotal:Double = 0
#IBAction func addButton(_ sender: Any) {
addCorrectValue()
}
func addCorrectValue () {
guard let addAmount = convertAmount(input: enterField.text!) else {
print("Invalid amount")
return
}
let newValue = weekOneTotal += addAmount
secondScreen.weekOneAmountLabel.text = String(newValue)
}
func convertAmount (input:String) -> Double? {
let numberFormatter = NumberFormatter ()
numberFormatter.numberStyle = .decimal
return numberFormatter.number(from: input)?.doubleValue
}
Try this:
func addCorrectValue () {
guard let addAmount = Double(enterField.text!) else {
print("Invalid amount")
return
}
let newValue = weekOneTotal + addAmount
secondScreen.weekOneAmountLabel.text = "\(String(format: "%.1f", newValue))"
}
The .1 is the number of decimals that are shown. You can adjust that to your needs. Hope I understood the question and this works for you!
You probably want to increase value of weekOneTotal variable by converted amount and then you want to use this value as text of some label
weekOneTotal += addAmount
secondScreen.weekOneAmountLabel.text = String(weekOneTotal)
The problem is described in title, but to be more specific here is a full picture.
I have a custom table view cell subclass with label inside it displaying the countdown timer. When there a small portion of timers it works fine, but with a lot of data I need to display timers far beyond the visible cells and when I scroll down fast and then scroll up fast, the timer values in cells start to show different values until a certain point in time, after which it shows the right value.
I tried different variants for those reuseable cells, but I can’t spot a problem. Help needed!!!
Here is the code of implementation of logic.
Custom cell subclass:
let calendar = Calendar.current
var timer: Timer?
var deadlineDate: Date? {
didSet {
updateTimeLabel()
}
}
override func awakeFromNib() {
purchaseCellCardView.layer.cornerRadius = 10
let selectedView = UIView(frame: CGRect.zero)
selectedView.backgroundColor = UIColor.clear
selectedBackgroundView = selectedView
}
override func prepareForReuse() {
super.prepareForReuse()
if timer != nil {
print("Invalidated!")
timer?.invalidate()
timer = nil
}
}
func configure(for purchase: Purchase) {
purchaseSubjectLabel.text = purchase.subject
startingPriceLabel.text = purchase.NMC
stageLabel.text = purchase.stage
fzImageView.image = purchase.fedLaw.contains("44") ? #imageLiteral(resourceName: "FZ44") : #imageLiteral(resourceName: "FZ223")
timeLabel.isHidden = purchase.stage == "Работа комиссии"
warningImageView.image = purchase.warningImage
}
func updateTimeLabel() {
setTimeLeft()
timer = Timer(timeInterval: 1, repeats: true) { [weak self] _ in
guard let strongSelf = self else {return}
strongSelf.setTimeLeft()
}
RunLoop.current.add(timer!, forMode: .commonModes)
}
#objc private func setTimeLeft() {
let currentDate = getCurrentLocalDate()
if deadlineDate?.compare(currentDate) == .orderedDescending {
var components = calendar.dateComponents([.day, .hour, .minute, .second], from: currentDate, to: deadlineDate!)
let dayText = (components.day! == 0 || components.day! < 0) ? "" : String(format: "%i", components.day!)
let hourText = (components.hour == 0 || components.hour! < 0) ? "" : String(format: "%i", components.hour!)
switch (dayText, hourText) {
case ("", ""):
timeLabel.text = String(format: "%02i", components.minute!) + ":" + String(format: "%02i", components.second!)
case ("", _):
timeLabel.text = hourText + " ч."
default:
timeLabel.text = dayText + " дн."
}
} else {
stageLabel.text = "Работа комиссии"
timeLabel.text = ""
timeLabel.isHidden = true
timer?.invalidate()
}
}
private func getCurrentLocalDate() -> Date {
var now = Date()
var nowComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now)
nowComponents.timeZone = TimeZone(abbreviation: "UTC")
now = calendar.date(from: nowComponents)!
return now
}
deinit {
print("DESTROYED")
timer?.invalidate()
timer = nil
}
The most important part of tableView(_cellForRowAt:)
case .results:
if filteredArrayOfPurchases.isEmpty {
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCellIdentifiers.nothingFoundCell,
for: indexPath)
let label = cell.viewWithTag(110) as! UILabel
switch segmentedControl.index {
case 1:
label.text = "Нет закупок способом\n«Запрос предложений»"
case 2:
label.text = "Нет закупок способом\n«Конкурс»"
case 3:
label.text = "Нет закупок способом\n«Аукцион»"
default:
label.text = "Нет закупок способом\n«Запрос котировок»"
}
return cell
} else {
let cell = tableView.dequeueReusableCell(
withIdentifier: TableViewCellIdentifiers.purchaseCell,
for: indexPath) as! PurchaseCell
cell.containerViewTopConstraint.constant = indexPath.row == 0 ? 8.0 : 4.0
cell.containerViewBottomConstraint.constant = indexPath.row == filteredArrayOfPurchases.count - 1 ? 8.0 : 4.0
let purchase = filteredArrayOfPurchases[indexPath.row]
cell.configure(for: purchase)
if cell.timer != nil {
cell.updateTimeLabel()
} else {
search.getDeadlineDateAndTimeToApply(purchase.purchaseURL, purchase.fedLaw, purchase.stage, completion: { (date) in
cell.deadlineDate = date
})
}
return cell
}
And the last piece of a puzzle:
func getDeadlineDateAndTimeToApply(_ url: URL?, _ fedLaw: String, _ stage: String, completion: #escaping (Date) -> ()) {
var deadlineDateAndTimeToApply = Date()
guard stage != "Работа комиссии" else { return }
if let url = url {
dataTask = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
if let error = error as NSError?, error.code == -403 {
// TODO: Add alert here
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let data = data, let html = String(data: data, encoding: .utf8), let purchasePageBody = try? SwiftSoup.parse(html), let purchaseCard = try? purchasePageBody.select("td").array() else {return}
let mappedArray = purchaseCard.map(){String(describing: $0)}
if fedLaw.contains("44") {
guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td class=\"fontBoldTextTd\">Дата и время окончания подачи заявок</td>"))! + 1].text().components(separatedBy: " ") else {return}
dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
let deadlineDateToApply = deadlineDateToApplyString.first!
let deadlineTimeToApply = deadlineDateToApplyString[1]
guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else {return}
deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
} else {
guard let deadlineDateToApplyString = try? purchaseCard[(mappedArray.index(of: "<td>Дата и время окончания подачи заявок<br> (по местному времени заказчика)</td>"))! + 1].text().components(separatedBy: " ") else {return}
dateFormatter.dateFormat = "dd.MM.yyyy HH:mm"
let deadlineDateToApply = deadlineDateToApplyString.first!
let deadlineTimeToApply = deadlineDateToApplyString[2]
guard let deadlineDateAndTimeToApplyCandidate = dateFormatter.date(from: "\(deadlineDateToApply) \(deadlineTimeToApply)") else {return}
deadlineDateAndTimeToApply = deadlineDateAndTimeToApplyCandidate
}
DispatchQueue.main.async {
completion(deadlineDateAndTimeToApply)
}
})
dataTask?.resume()
}
}
A few notes:
Tried resetting deadlineDate to nil in prepareForReuse() - doesn’t help;
Using SwiftSoup Framework to parse HTML as you can see in the last code example if it matters.
This is quite a lot of code but from what you are describing your issue is in reusing cells.
You would do well to separate the timers out of the cells and put them inside your objects. It is where they belong (or in some manager like view controller). Imagine having something like the following:
class MyObject {
var timeLeft: TimeInterval = 0.0 {
didSet {
if timeLeft > 0.0 && timer == nil {
timer = Timer.scheduled...
} else if timeLeft <= 0.0, let timer = timer {
timer.invalidate()
self.timer = nil
}
delegate?.myObject(self, updatedTimeLeft: timeLeft)
}
}
weak var delegate: MyObjectDelegate?
private var timer: Timer?
}
Now all you need is is a cell for row at index path to assign your object: cell.myObject = myObjects[indexPath.row].
And your cell would do something like:
var myObject: MyObject? {
didSet {
if oldValue.delegate == self {
oldValue.delegate = nil // detach from previous item
}
myObject.delegate = self
refreshUI()
}
}
func myObject(_ sender: MyObject, updatedTimeLeft timeLeft: TimeInterval) {
refreshUI()
}
I believe the rest should be pretty much straight forward...
Your problem is here:
search.getDeadlineDateAndTimeToApply(purchase.purchaseURL,
purchase.fedLaw,
purchase.stage,
completion: { (date) in
cell.deadlineDate = date
})
getDeadlineDateAndTimeToApply runs asynchronously, calculates something, and then updates the cell.deadlineData in the main thread (which is fine). But in the meantime, while calculating something, the user might have scrolled up and down, the cell might have been reused for another row, and now the update updates the cell incorrectly.
What you need to do is: Do not store the UITableViewCell directly. Instead, keep track of the IndexPath to be updated, and once the caluclation is done, retrieve the the cell that belongs to that IndexPath and update this.
so I have been trying to create a "Period" class, with the following attributes:
class PTPeriod {
// MARK: Stored Properties
var start: Date
var end: Date
var place: PTPlace?
//MARK: DESIGNATED NITIALIZER
init(start: Date, end: Date, place: PTPlace? = nil) {
self.start = start
self.end = end
self.place = place
}
And I want to initialize it with a convenience init that accepts a dictionary like the following:
{
"close" : {
"day" : 1,
"time" : "0000"
},
"open" : {
"day" : 0,
"time" : "0900"
}
}
This is my initializer, originally I put the heavy lifting into a helper method. However, i was getting the same error I have now so I removed the helper method and the error is still occurring. Unsure where it thinks I am calling self.
UPDATE: moved the date formatting into a date formatter extension, but the error still persists! unsure WHY. new init shown:
My new Convenience init:
// MARK: Convenience init for initializing periods pulled from Google Place API
convenience init?(placeData: [String:Any], place: PTPlace? = nil) throws {
var start: Date? = nil
var end: Date? = nil
var formatter = DateFormatter()
do {
for (key, value) in placeData {
let period = value as! [String: Any]
if (key == "open") {
start = try formatter.upcoming_date(with: period)
} else if (key == "close") {
end = try formatter.upcoming_date(with: period)
} else {
break
}
}
if (start != nil && end != nil) { self.init(start: start!, end: end!, place: place) }
else { print("we f'd up") }
}
catch { print(error.localizedDescription) }
}
Here is my DateFormatter.upcoming_date method:
func upcoming_date(with googlePlacePeriod: [String: Any]) throws -> Date? {
if let day = googlePlacePeriod["day"] as? Int {
if (day < 0) || (day > 6) { throw SerializationError.invalid("day", day) } // throw an error if day is not between 0-6
if let time = googlePlacePeriod["time"] as? String {
if time.characters.count != 4 { throw SerializationError.invalid("time", time) } // throw an error if time is not 4 char long
var upcoming_date: Date
let gregorian = Calendar(identifier: .gregorian)
let current_day_of_week = Date().getDayOfWeekInt()
let dayOfPeriod = day + 1
///TRUST THAT THIS WORKS... NO JODAS
var distance = dayOfPeriod - current_day_of_week // inverse = false
if (dayOfPeriod < current_day_of_week) {
switch distance {
case -1: distance = 6
case -2: distance = 5
case -3: distance = 4
case -4: distance = 3
case -5: distance = 2
case -6: distance = 1
default: break
}
}
/** time example: "1535" translates into 3:35PM
first two characters in string are "hour" (15)
last two characters in string are "minute(35)
**/
let hour = Int(time.substring(to: time.index(time.startIndex, offsetBy: 2)))!
let minute = Int(time.substring(from: time.index(time.endIndex, offsetBy: -2)))
upcoming_date = Date().fastForward(amount: distance, unit: "days", inverse: false)!
var components = gregorian.dateComponents([.year, .month, .day, .hour, .minute, .second], from: upcoming_date)
components.hour = hour
components.minute = minute
upcoming_date = gregorian.date(from: components)!
return upcoming_date
}
else { throw SerializationError.missing("time") }
}
else { throw SerializationError.missing("day") }
}
You have a failable initializer, so if it fails, you must return nil to let it know it failed:
if (start != nil && end != nil) {
self.init(start: start!, end: end!, place: place) }
} else {
print("we f'd up")
return nil
}
Also, in your do-catch, you need to either
re-throw the error in the catch block
catch {
print(error.localizedDescription)
throw error
}
eliminate the do-catch blocks altogether; or
just return nil:
catch {
print(error.localizedDescription)
return nil
}
If you do this, you'd presumably not define this failable initializer as one that throws because you're no longer throwing.
Given that you're unlikely to do anything meaningful with the thrown error, I'd just make it a failable initializer that doesn't throw:
convenience init?(placeData: [String: Any], place: PTPlace? = nil) {
var start: Date?
var end: Date?
let formatter = DateFormatter()
for (key, value) in placeData {
let period = value as! [String: Any]
if key == "open" {
start = try? formatter.upcoming_date(with: period)
} else if key == "close" {
end = try? formatter.upcoming_date(with: period)
} else {
break // I'm not sure why you're doing this; it seems extremely imprudent (esp since dictionaries are not ordered)
}
}
if let start = start, let end = end {
self.init(start: start, end: end, place: place)
} else {
print("either start or end were not found")
return nil
}
}