How to store variable value of popup or dismissed ViewController - swift

Let's say I has a audio player app and there is song list (tableView) in VC1 to present all the songs and clicking any row of it to open another VC2 to present the playing scene. In this playing scene there is some buttons for playing sequence (repeat, repeatOne, shuffle).
Since user will frequently dismissed/popup this VC2 to get back to VC1 (song list), I store the playing sequence button value to another file called SongData.swift and define a static var static var repeatOneSequence = false to store the value of VC2. Then retrieve it if SongData.repeatOneSequence == true like this in VC2.
Now it is working, but it is just my way...I didn't get info about how to do it properly. I'm wondering if there is other way to store the data of dismissed VC2 or which way is the better one to do it.
/ / / Insert code snip for reference
#IBAction func repeatButton(_ sender: UIButton) {
if SongData.repeatOneSequence == true {
print("repeatOneSequence TRUE!")
repeatButton.setImage(UIImage(named: "icons8-repeat-50"), for: .normal)
repeatButton.imageView?.contentMode = .scaleAspectFit
SongData.repeatOneSequence = false
} else {
print("repeatOneSequence FALSE!")
repeatButton.setImage(UIImage(named: "icons8-repeat-one-50"), for: .normal)
repeatButton.imageView?.contentMode = .scaleAspectFit
SongData.repeatOneSequence = true
}
}

Related

How to update buttons status when the user reopens the app? (Swift 5)

I have a question about saving the last status of the buttons for a "Hung Person" game. By using UserDefaults I was able to save almost all the status of the user when the game was closed (Screen1 shows the user's game status while playing), as you can see, the user chose 2 wrong letters that were not in the hidden word (M and N, which are in red and disabled) and the user correctly guessed the O, B, C letters (which disappeared from the alphabet and also are disabled).
When the user closes the app and when it is reopened again, I was able to reload almost all the previous data of the last session played, except for the buttons status. You can see this in Screen2. I used UserDefaults to save and load the game. I save the status of the game every time the user taps on any button (trying to guess the hidden word's characters), and I load the data back in viewDidLoad().
The game is made programmatically, I created the buttons using something like the following:
for row in 0..<5 {
for column in 0..<6 {
if counterLetter < 26 {
let letterButton = UIButton(type: .system)
letterButton.titleLabel?.font = UIFont.systemFont(ofSize: titleLabelFontSize)
letterButton.setTitle(englishAlphabet[counterLetter], for: .normal)
letterButton.isHidden = false
letterButton.isEnabled = true
letterButton.alpha = 1
letterButton.setTitleColor(.red, for: .disabled)
letterButton.addTarget(self, action: #selector(letterTapped), for: .touchUpInside)
letterButton.layer.borderWidth = 1
letterButton.layer.borderColor = UIColor.black.cgColor
// Button's frame creation
let frame = CGRect(x: column*widthButton, y: row*heightButton,
width: widthButton, height: heightButton)
letterButton.frame = frame
buttonsView.addSubview(letterButton)
letterButtons.append(letterButton)
counterLetter += 1
} else {
continue
}
}
}
It is in the letterTapped method (inside the #selector(letterTapped) part in the previous code) where I save the user's progress when any alphabet button is tapped. As I said before, I recall the player's last status in viewDidLoad().
I tried to save the buttons status inside the letterTapped method as well, but I haven't been able to save or reload the buttons status as the player's had.
Can you give me please a hand about where I have to use userDefaults to save the last session status of the alphabet buttons, please? So that when the players reopens the app, the screen2 is the same as screen1.
If you need me to share my code, I can do willingly it.
Thanks in advance.
Regards!
What you are looking for is a K.V.O called "UIApplicationWillEnterForeground" and "UIApplicationDidEnterBackground"
In your viewDidLoad of your ViewController you should do the following:
func viewDidLoad() {
super.viewDidLoad()
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil)
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
then implement these function in the same ViewController:
func appMovedToForeground() {
print("Application moved to Foreground!")
}
func appMovedToBackground() {
print("Application moved to Background!")
}
now when your app goes to the background or returns to the foreground those functions will get called

kill and re-run background timer (DispatchSourceTimer)

In a game screen, I use a background queue to count elapsed time in a game, this func is based on Daniel Galasko solution, a perfect solution for my app: it allows user to navigate through other VC, while timer is still on.
The VC hierarchy is quite simple : the game settings VCs are handled in a tabBarController. The game screen is apart. User can change settings while timer is on. Settings are stored in CoreData.
In my game screen, where I need to display the timer, I have a label that displays the elapsed time and 2 buttons : Play/Pause button and Reset button.
I call my setup timer func in ViewDidLoad.
The default value for my timer is the stored value in CoreData, it has been defined in settings. And this value is incremented by 1 every second when timer is on.
I also have a static let shared that keep timer status (resumed / suspended).
When I'm on the game screen, and if my timer is suspended, my play/Pause button works perfect : I can navigate to others views (mean dismiss my screen game), present again my screen game and resume my counter. It updates my label correctly.
The problem is when I dismiss the game screen view while timer is running. Timer works (print func shows that timer is still running), but when I present the screen again, I'm unable to pause/resume/restart it, and my label is stopped at the second I came back... while timer is still running back.
private var counter: Int16?
var t = RepeatingTimer(timeInterval: 1)
let gameIsOn = isGameOnManager.shared
override func viewDidLoad() {
super.viewDidLoad()
print("is timer On ? \(String(describing: gameIsOn.isgameOn))")
buildTimer()
if gameIsOn.isgameOn == true {
resumeTapped = true
t.resume()
PlayB.setImage(UIImage(named:"pause"), for: .normal)
} else {
resumeTapped = false
}
}
func buildTimer(){
self.t.eventHandler = {
self.counter! += 1
print("counter \(String(describing: self.counter!))")
self.coreDataEntity?.TimeAttribute = self.counter ?? 0
self.save()
DispatchQueue.main.async {
self.dataField.text = String(describing: self.counter ?? 0)
}
}
}
#objc func didTapButton(_ button: UIButton) {
if resumeTapped == false {
t.resume()
resumeTapped = true
gameIsOn.isgameOn = true
PlayB.setImage(UIImage(named:"pause"), for: .normal)
}
else if resumeTapped == true {
t.suspend()
resumeTapped = false
gameIsOn.isgameOn = false
PlayB.setImage(UIImage(named:"play"), for: .normal)
}
}
You have a strong reference cycle between your view controller, the timer, and the timer’s event handler. You should use a weak reference in the closure to break this cycle:
func buildTimer() {
t.eventHandler = { [weak self] in
guard let self = self else { return }
...
}
}
That fixes the strong reference cycle.
But when you fix this, there is a chance that you’re going to see your timer stop running when you dismiss this view controller. If this is the case, that will indicate that the timer is not in the right object. It should be in some higher level object that persists throughout the app, not inside this view controller which is presented and dismissed.

Swift save text to an instance/global variable when UIButton is pressed

So I have a UItextfield and a UIButton I was wondering if there is a way to save the text after the button is pressed in an instance variable so that I could use it in my other class.
The thing I am trying to do is to have a user input some text in the textfield on screen 1 and then when the user taps the button I want to take him to screen 2 however in the class of that screen 2 I want to know what text user entered in order to display data accordingly.
Right now I just have an action function for the button and I access text inside it and save it to an instance variable but when I call it in my other class it is empty because I initialized it as empty. So can somebody please help me and tell me how to go about this.
Thanks!
Here is the code for first screen
var searchRecipe = ""
#IBAction func SearchButton(sender: UIButton) {
if let recipe = RecipeSearchBar.text {
searchRecipe = recipe
}
}
Here is the code for second screen. I have connected the button in first screen this screen so when user taps the button he gets here.
var recipeName:[String] = []
var imageURL:[String] = []
var timeInSeconds:[Float] = []
func apiCall()
{
//search recipe API call
var searchRecipe = ""
searchRecipe = RecipesViewController().searchRecipe
print (searchRecipe) //prints nothing
endpoint = "http://api.yummly.com/v1/api/recipes?_app_id=apiId&_app_key=apiKey&q=\(searchRecipe)" //this is where I want to use the searchRecipe text from class 1
}
Well personally I would do what follows (Based on my interpretation of your issue):
Please note that I take for granted you are implementing a view-controller-based navigation and you know what IBOutlets, IBActions & Segues are
In the first view controller create an IBOutlet property of
UITextField named RecipeSearchBar and connect it to the relative text
field.
In the second view controller create a variable value of type
string;
Change the storyboard segue identifier to "toSecondViewController"
Then in the first view controller create an IBAction to be called
when the UIButton is pressed.
In your case:
#IBAction func SearchButton(sender: UIButton) {
if let recipe = RecipeSearchBar.text {
searchRecipe = recipe
}
//If you are navigating into a new view view controller
//here you need to call self.presentViewController(...)
}
Inside this action, you are going to call self.presentViewController
to display your second view controller, but you have to do one last
very important step: pass the value of the UITextField to the instance
that will hold the second view controller
To achieve this, override prepareForSegue in your first view controller
and share the UITextField value. This method is triggered once you called self.presentViewController for further implementation.
//Did not tested the code but should just be ok
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "toSecondViewController")
{
//Create the instance
let secondViewController = segue.destinationViewController
//Set the value
secondViewController.value = searchRecipe;
}
}
You are now good to go.
Hope this helped, if so mark the question for others.
Bye

Load data in new view when button is clicked with swift

I am developing an app, where I create buttons programmatically. When I click a button, it will request data from a database and show it in another view.
I use button.tag to determine what data to request, and I can only get the tag once the button is clicked.
However when I click the button, it shows nothing the first time. I must click it again to see the data which I want.
override func viewDidLoad() {
super.viewDidLoad()
//parseJSON(tagId)
createButton()
// Do any additional setup after loading the view, typically from a nib.
}
func createButton(){
var j:CGFloat=60
for var i:Int = 0 ; i < myImages.count;i = i+1 {
let myButton = UIButton()
myButton.setImage(UIImage(named: "carasusto.jpg"), forState: UIControlState.Normal)
myButton.setTitleColor(UIColor.blueColor(), forState: .Normal)
myButton.frame = CGRectMake(j, j+60, 50, 50)
myButton.tag = i //assign a tag to every button
myButton.addTarget(self, action: "segueToCreate:", forControlEvents: UIControlEvents.TouchUpInside)
self.view.addSubview(myButton)
j=j+60
print(myImages[i])
}
}
and
#IBAction func segueToCreate(sender: UIButton){
tagId = String(sender.tag)//tagId needs to fetch the information
parseJSON(tagId)
performSegueWithIdentifier("segueToView", sender:self)
}
func parseJSON(tagID:String){
Alamofire.request(.GET, "http://smarttags-001-site1.btempurl.com/SmartTagsRequests.aspx", parameters: ["AjaxFunc":"GetTagAttr","TagID":"\(tagID)"]).validate().responseJSON{ response in
switch response.result{
case .Success:
if let value = response.result.value {
let json = JSON(value)
print("JSON: \(json)")
self.TagName = json[0]["TagName"].stringValue
NSLog("\(self.TagName)")
self.ContentTitle = json[0]["ContentTitle"].stringValue
NSLog("\(self.ContentTitle)")
}
case .Failure(let error):
print(error)
}enter code here
}
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
var ViewTest : ViewTwo = segue.destinationViewController as! ViewTwo
var TagNameLabel = UILabel()
TagNameLabel.frame = CGRectMake(74, 112, 182, 64)
ViewTest.view.addSubview(TagNameLabel)
TagNameLabel.text = TagName
var ContentTitleLabel = UILabel()
ContentTitleLabel.frame = CGRectMake(74, 180, 182, 64)
ViewTest.view.addSubview(ContentTitleLabel)
ContentTitleLabel.text = ContentTitle
}
}
To follow up to MirekE's answer, here are some other steps you may want to consider:
Consider using Auto Layout in place of hard-coded frames, so your UI would adapt to different size classes.
Consider alternate approaches for showing an interactive list (of images) instead of programmatically adding buttons. For example, you could use a prototype (table view or collection view) cell. Cells are selectable and can take the place of a button. Other benefits include:
A scrollable container, should you have more buttons than would fit on screen.
A single segue (or selection) is handled by the storyboard prototype cell, instead of needing to make each programmatic "button" perform a segue (or action). Since you'd know which cell was selected, you'd no longer need a tag to figure that out.
Consider passing parameters to your destination view controller, instead of trying to instantiate and create controls in prepareForSegue. The destination's view is not loaded at that point.
Consider allowing the UI to feel more responsive, such as by performing the segue and showing placeholder details which you can then update once the network request completes. Otherwise, the user may have to wait for the network response before the segue occurs (which might make them think nothing happened and needlessly tap again, leading to an additional network request).
In general, Apple (or someone else) has already provided some way to do what you want, ideally leading to less code that you have to write, debug, test, and maintain to accomplish what you want your app to do.
Most likely cause of the problem is your call to parseJson followed by prepareForSegue. parseJson is asynchronous and you don't get data back from your service before prepareForSegue is called. As a first step, move the prepareForSegue to the completion block of the parseJson.

performSegueWithIdentifier not working for programmatically generated UIButtons (iOS/Swift)

Xcode 6, Swift, iOS8
I have a view controller that is populated by dynamically generated UIButtons. The number of buttons is dependent on a data feed, and is not static. I count the number of objects in the feed and generate a button for each object. Each button is supposed to segue into a details view that displays the information for its corresponding object.
In the Interface Builder I have created a segue between the two View Controllers and named it. I have not added an IBAction to initiate the segue as I cannot tie it to a specific button.
Inside the View Controller Class I execute the following:
#IBOutlet weak var localScrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
//Define the button dimensions
let buttonWidth:CGFloat = 200
let buttonHeight:CGFloat = 113
var xPos:CGFloat = 0
var scrollViewContent:CGFloat = 0
var thumbURL:String
//keeps track of the number of videos. The count is used to set a tag on the button to help identify it.
var vidCount:Int = 0
//loop through the array of recommended video objects
for index in recommended{
//Create a button for each object
var myButton = UIButton.buttonWithType(UIButtonType.System) as! UIButton
myButton.frame = CGRectMake(xPos, 0.0, buttonWidth, buttonHeight)
//For the button action I call the handleTap function detailed below
myButton.addTarget(self, action: "handleTap:", forControlEvents: .TouchUpInside)
myButton.tag = vidCount
//The image for the button is pulled from a CDN. This code sets the image in the button.
let curVal = index.thumbURL
if let url = NSURL(string: curVal) {
if let data = NSData(contentsOfURL: url){
myButton.setBackgroundImage(UIImage(data:data), forState: UIControlState.Normal)
}
}
//add the button to the scroll view
localScrollView.addSubview(myButton)
let spacer:CGFloat = 10
xPos+=buttonWidth + spacer
scrollViewContent += buttonWidth + spacer
localScrollView.contentSize = CGSize(width: scrollViewContent, height: buttonHeight)
vidCount += 1
}
}
//function to handle the tap action
func handleTap(sender:UIButton){
//Set the variable that will be passed to the next view controller
curRecVid = recommended[sender.tag]
//initiate the segue
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("toSingle", sender: self)
}
}
//prepare the data to be transferred to the next view controller
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
if (segue.identifier == "toSingle") {
var targetViewController = segue.destinationViewController as! SingleViewController
targetViewController.targetVid = curRecVid
}
}
}
When I run the app in the simulator, it gets to the self.performSegueWithIdentifier("toSingle", sender: self) call and then terminates to the following uncaught exception: 'NSInvalidArgumentException', reason: '-[UILabel copyWithZone:]: unrecognized selector sent to instance 0x7fd468c8ddc0'
Any assistance in helping to track down the cause of this exception would be greatly appreciated.
It appears that the class file for the target View Controller had some sort of problem. After a comment from Epic Defeater, I wiped out its swift file, re-generated it and put in the exact same code (literally, copy/paste) and that did the trick.