How can I maintain IBOutlet variable data when switching between views - ios

Short version:
How can I "snapshot" my game's status when leaving that view and restore it in its exact state when going back into the view? Or simply maintain the values regardless of my current view?
Long version:
I have a "settings" view with its own view controller and a "game" view with its own view controller. I'd like to be able to switch back to settings to make a change to the game without reinitializing all my IBOutlets and their values in my game view when I go back.
I understand that it's where the variables are declared and thus will have to reinitialize when called but I even tried defining them in their declaration with global variables that I can update on "viewDidDisappear" but the values/attributes don't seem to populate. I would think there has to be a way to retain these values (or the games current status) in memory without deallocating when the view disappears.
I've added some sample code wherein the issue can be replicated though I don't think it's so much a problem with the code as it with my limited understanding of its functionality and/or capabilities.
Thanks in advance for any help that leads to a solution!
I have a custom class for my game pieces:
class gamePiece: UILabel {
var face = "face"
func updateFace() {
self.text = self.face
}
}
var isGameOver = true
var allLabels = [gamePiece]()
My gameViewController looks like this:
class gameViewController: UIViewController {
#IBOutlet var l1: gamePiece!
#IBOutlet var l2: gamePiece!
#IBOutlet var l3: gamePiece!
#IBOutlet var l4: gamePiece!
#IBAction func buttonPressed() {
l1.face = "hello world"
l1.updateFace()
}
override func viewDidLoad() {
super.viewDidLoad()
if isGameOver {
allLabels = [l1, l2, l3, l4]
isGameOver = false
} else {
l1.text = allLabels[0].text!
l2.text = allLabels[1].text!
l3.text = allLabels[2].text!
l4.text = allLabels[3].text!
allLabels = [l1, l2, l3, l4]
}
for label in allLabels {
label.updateFace()
}
// Do any additional setup after loading the view, typically from a nib.
}
override func viewDidDisappear(animated: Bool) {
allLabels = [l1, l2, l3, l4]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
The expectation is that l1.text would render "hello world". Instead it renders "face"; the default value of an object of that my gamePiece class.
I'm sure this is a simple fix.. I just think it's so close to my face I can't see it.

Related

Swift 3 - If else statement based on button state

I am really new to Swift and working on my first project (I have a bit of experience with Javascript and web development). I have run into trouble (going on 4 hours of trying different solutions).
I have an app where when a UIButton is pushed it logs a value to FireBase (ON). When it is pushed a second time it logs (OFF) to the database.
When I make the button change the view.backgroundColor and put if else tied to the colour it works.
But I can't for the life of me figure out how to build my if else based on the state of the button. I have now ended up trying to change the colour of the button itself and tie the if else to that. Which I know is a really messy improper way to go about it.
import UIKit
import Firebase
import FirebaseDatabase
class ViewController: UIViewController {
#IBAction func OnOffButton(_ sender: UITapGestureRecognizer){
if button.backgroundColor == UIColor.white {
OnOff(state:"ON")
OnOffButton.backgroundColor = UIColor.blue
} else{
OnOff(state:"OFF")
OnOffButton.backgroundColor = UIColor.white
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
OnOff(state: "Off")
OnOffButton.backgroundColor = UIColor.white
}
UIButton inherits from UIControl which has an isSelected property. Use this to track state. Typically you'll use button.isSelected == true to correspond to your on state, and false is your off state.
To toggle state you can use button.isSelected = !button.isSelected.
For your specific example:
// User pressed the button, so toggle the state:
button.isSelected = !button.isSelected
// Now do something with the new state
if button.isSelected {
// button is on, so ...
} else {
// button is off, so...
}
Here’s a quick and dirty implementation of Duncan C’s suggestion to use a buttonIsSelected variable to store the button state:
import UIKit
class ViewController: UIViewController {
var buttonIsSelected = false
#IBOutlet weak var onOffButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
updateOnOffButton()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func onOffButtonTapped(_ sender: Any) {
buttonIsSelected = !buttonIsSelected
updateOnOffButton()
}
func updateOnOffButton() {
if buttonIsSelected {
onOffButton.backgroundColor = UIColor.blue
}
else {
onOffButton.backgroundColor = UIColor.white
}
}
}
Par's solution should work. However, I would suggest a different approach.
In general it's not good design to store state in a view object. It's fragile, and doesn't follow the MVC design pattern, where view objects display and collect state information, not store it.
Instead I would create an instance variable in your view controller, buttonIsSelected. Toggle that when the button is tapped, and have it change the state of the button's selected property, the button's color, AND log the new state to FireBase.
If you store more complex state in your view controller it would be worth separating that out into a model object. It can be as simple as a struct that holds the different state values. That way you have a clear separation between your controller (view controller) and model.

Multiple Navigation Bar colours for a complex storyboard

I have 48 view controllers in my storyboard / project. I wish to have 2 different types of Navigation Bar designs.
Style 1 is a navigation bar and a status bar that is white and grey (colours not important to the question)
Style 2 is a navigation bar without a status bar. This is black.
I have set style 1 in the app delegate and set style 2 within one of the views. To a point this works and style 2 overwrites style 1. However, when I navigate away from the view, style 2 continues to override.
I could set each view controller explicitly but with 48 views and 4 or 5 lines of code to define the style is seems inefficient. If I later choose to change the style I then have 48 instances of code to edit.
My major experience is with PHP and if I had this situation I would make an include statement to reference style1 or style2 as needed.
I have tried to create a function in Swift to call the desired design but I cannot get it work as it doesn't reference the UIViewController in the same way you would when adding it directly. I have only been coding for Swift / Xcode for 3 months so it could my lack of knowledge.
I would like to find a solution that on each view I can call something like below (PseudoCode)
override func viewWillAppear(animated: Bool) {
navBarStyle1() or navBarStyle2()
}
I have not included my code to adjust the colours as I feel this is not needed for the answer.
What would be the best way to manage this efficiently? Is there an equivalent to a PHP include? If the solution is a function, could you provide an example? Or may be the solution is something different?
As requested, here is one of my view controllers:
import UIKit
class DeleteMatchViewController: UIViewController {
var idPass = ""
// OUTLETS
#IBOutlet weak var errorMessage: UILabel!
#IBOutlet weak var information: UILabel!
// ACTIONS
#IBAction func deleteMatch(sender: UIBarButtonItem) {
// connect and delete from server
// delete from core data
// load from core data
let urlParameters = "removed for privacy"
let status = sendSeverV2("\(apiUrl)/removedforprivacy.php", parameters: urlParameters)
if status == "OK"
{
// DELETESINGLE firstname|David
myDatabase("Matches", theCommand: "DELETESINGLE", theQuery: "userid|\(idPass)")
myDatabase("Messages", theCommand: "DELETEMULTIPLE", theQuery: "people|\(userId)-\(idPass)")
myDatabase("Messagesunsent", theCommand: "DELETEMULTIPLE", theQuery: "people|\(userId)-\(idPass)")
// core data
loadMatchesFromCoreData()
// segue to matches table
performSegueWithIdentifier("jumpMatches", sender: nil)
}
if status == "Error"
{
errorMessage.text = "Connection error"
}
if status == "Security"
{
errorMessage.text = "Authentication error"
authError = "yes"
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
self.navigationController?.navigationBarHidden = false
self.tabBarController?.tabBar.hidden = true
errorMessage.text = ""
information.text = "If you delete this match all messages will be erased and only a future mutual match will all you to contact them again."
}
}
A little OOP can go a long way here.
First, let's make some base view controllers:
class GrayStatusBarViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// set up the appearance
}
}
class BlackStatusBarViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// set up the appearance
}
}
Now, we can make one of these for every appearance we want, and we can move any shared behavior into these base classes we want. It probably makes sense for your app to have a BaseViewController which the two classes I just posted inherit from (rather than inheriting from UIViewController directly).
Then, all you have to do is make your pile of view controllers inherit from the correct controller based on theme.
class DeleteMatchViewController: GrayStatusBarViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated) // <-- this is super important
// the rest of the view will appear code for this VC
}
}
Alternatively, you can encapsulate the appearance logic in methods in a class like I previously mentioned, the BaseViewContoller, something like this:
class BaseViewController: UIViewController {
func setUpGrayStatusBar() {
// write that logic
}
func setUpBlackStatusBar() {
// write that logic
}
// etc, as many of these as you want
}
And now, you inherit from this class and call the appropriate method:
class DeleteMatchViewController: BaseViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
setUpGrayStatusBar()
}
}
When it comes to the run time performance of your application, neither of these solutions is any different from simply copy & pasting the same code into all of your individual view controllers. Just because you didn't paste it in more than one place doesn't mean it doesn't run more than once.

Error trying to transfer data from variable between storyboards

I have a game where I store the value of the high score of a player as "highScore", in the first view controllers.
What I want to do is to try to transfer this variable data, to the second view controller.
I entered in the following code in the first view controller's storyboard.
Btw, highscore2, is the variable that I delcared in the second view controller, to store the data from the first highscore variable.:
override func prepareForSegue(segue: UIStoryboardSegue!, sender:AnyObject!)
{
if (segue.identifier == "segueTest")
{
var svc = segue!.destinationViewController as! viewTwo;
svc.highScore2 = "High Score \(highScore)"
}
}
Now here is the code in my second view controller (viewTwo):
class viewTwo: UIViewController
{
#IBOutlet var highScoretwolbl: UILabel!
var highScore2:String!
override func viewDidLoad() {
highScoretwolbl.text = highScore2
}
}
For some reason, the code compiles, but the high score is not displayed, and is "nil".
Avoid forced unwrapping.
if let svc = segue.destinationViewController as? viewTwo{
svc.highScore2 = "High Score \(highScore)"
}
Then put in a breakpoint and make sure you got to all of these lines.
As stated in other answers, viewDidLoad only gets fired once. Since you
are using a forced unwrapped string, it is initially nil. It (highScore2) won't get a value until you set it which happens after viewDidLoad. Add some print statements so you can see the sequence of events.
Your best option is to use viewWillAppear. This will get fired every time
your view appears. This is what you want.
You don't need to use a force unwrapped variable. Just make a string a
make it empty initially.
override func prepareForSegue(segue: UIStoryboardSegue, sender:AnyObject?)
{
if segue.identifier == "segueTest"
{
if let svc = segue.destinationViewController as? viewTwo {
print("setting score in prepareForSegue")
svc.highScore2 = "High Score \(highScore)"
}
else {
print("something bad happened")
}
}
}
class viewTwo: UIViewController
{
#IBOutlet var highScoretwolbl: UILabel!
// The following uses forced upwrapping which means it could be nil
// if you never set it and your app will crash.
// var highScore2: String!
// Instead, do this:
var highScore2: String = ""
override func viewWillAppear(animated: Bool) {
// putting this here will ensure that every time the screen
// is shown, the label will get set from the string.
highScoretwolbl.text = highScore2
}
}
If I recall correctly, the viewDidLoad method is called only once when the view controller is first initialized. So you are setting the highScoretwolbl variable before highScore2 actually has any data, this is why it is nil.
I would recommend setting the text inside viewWillAppear so it updates every time the segue happens.
So something like
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(true)
highScoretwolbl.text = highScore2
}
Here is a reference to the lifecycle of all the methods of a view controller. View controller lifecycle

Adding Model file to view controller, help getting UI working

I wrote a Model file for my basic app and I'm in the process of applying the model to the UI. It has 3 manual inputs and a text area for the "results" once the calculate button has been pressed.
I'm really struggling with making the UI do what I want it to do, I just want the results displaying, I tried print() but get unresolved identifiers in view controller.
How do I get the .value of my results from the Model file in the view controller? Or have to initialize those again?
Model File (Struct)
struct FuelMaths {
var rate: Double = 3.7 //set initialValvue
var tank: Int = 110
var laps: Int = 13
var totalFuel: Double
var totalStops: Double
init (rate: Double, tank: Int, laps: Int) {
self.rate = rate // declare an instance
self.tank = tank
self.laps = laps
totalFuel = rate * Double(laps)
totalStops = totalFuel / Double(tank)
}
func result () {
print(totalFuel.value)
print(totalStops.value)
}
}
View Controller
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var rate: UITextField!
#IBOutlet weak var laps: UITextField!
#IBOutlet weak var tank: UITextField!
let calculate = FuelMaths(rate: 3.7, tank: 110, laps: 13)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func calculate(sender: UIButton) {
}
#IBOutlet weak var display: UITextView!
}
Thanks all.
(Edit: I originally said you didn't instantiate the model at class scope; you did, but you misnamed it as a verb calculate rather than a good noun like fuelCalculationModel. But anyway, I'm using it the way you've written it: which is as sort of a one-off where the values are passed in at construction time and those values are assumed final so that the result is calculated immediately. It is possible to write a model that is designed to persist and adjust to changes in its source value over time, but you need to understand property observers or computed properties for that.)
See if the solution is helpful:
#IBAction func calculate(sender: UIButton) {
if let rateVal = Double(rate.text!),
tankVal = Int(tank.text!),
lapVal = Int(laps.text!) {
let fuelModel = FuelMaths(rate: rateVal, tank: tankVal, laps: lapVal)
display.text = "Total fuel: \(fuelModel.totalFuel) Total Stops: \(fuelModel.totalStops)"
}
else {
display.text = "One of the text fields did not have a valid number."
}
}
Ultimately you want the model to be a persistent object at class-level scope, not function scope, and you want it to recalculate its results whenever its source values are changed, but wait on that until you understand this, especially if-let when constructing numbers out of parsed Strings.

Get a control's original frame for both portrait and landscape

I am working on an application that has different UI constraints and control positions for portrait and landscape. These are all done on the storyboard. In addition to this, I am repositioning controls based on a user shutting off one of the controls. I am doing this by grabbing the frame for each of the controls in viewDidLoad. Once I have these values, then it is easy to reposition the controls and restore them to what they should be when unhidden. The thing is that I need all of the frames for both portrait and landscape. That way I can do the repositioning regardless of orientation.
How can I get the control positioning information for both portrait and landscape when coming through viewDidLoad? Is there any way to do this?
After adding constraints to a view, and the view readjusting its position and size according to device size and orientation. This readjusting of the view size is done in the method viewDidLayoutSubviews, which is called after viewDidAppear.
If you can log out the positions and size of the control in this method, you will get the updated (size and position as it is seen in the device).
But this method is called multiple times after viewDidAppear, so if you want to add anything i recommend adding the control in viewDidLoad and then updating the position in this method.
After working with this a bit more, I came up with this:
import UIKit
class ViewController: UIViewController {
var pButtonFrame: CGRect!
var lButtonFrame: CGRect!
#IBOutlet weak var testButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "screenRotated", name: UIDeviceOrientationDidChangeNotification, object: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func screenRotated() {
//Set this only once, the first time the orientation is used.
if lButtonFrame == nil && UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation)
{
lButtonFrame = testButton.frame
}
else if pButtonFrame == nil && UIDeviceOrientationIsPortrait(UIDevice.currentDevice().orientation)
{
pButtonFrame = testButton.frame
}
}
}
I set up a test button and positioned it using constraints on the storyboard. I added an observer to NSNotificationCenter to watch for screen rotation. I'm storing the frames for each of the orientations in CGRect variables. By checking each of the variables for nil, I can ensure that they only get set once, before I have done any modifications to the screen. That way, I can restore the values to their originals, if needed. I can set up the showing and hiding of controls here, or in viewDidLayoutSubviews.
import UIKit
class ViewController: UIViewController {
var pButtonFrame: CGRect!
var lButtonFrame: CGRect!
#IBOutlet weak var btntest: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
screenRotate()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func screenRotate() {
//Set this only once, the first time the orientation is used.
if lButtonFrame == nil && UIDeviceOrientationIsLandscape(UIDevice.currentDevice().orientation)
{
lButtonFrame = btntest.frame
}
else if pButtonFrame == nil && UIDeviceOrientationIsPortrait(UIDevice.currentDevice().orientation)
{
pButtonFrame = btntest.frame
}
}
}

Resources