Assign Class to variable with a String in Swift - ios

I'm trying to do some of the project Euler problems in swift.
I originally tried using the Playground, but found that to be incredibly slow, so I've instead made a simple single view app that will run them for me in the simulator. See below for my example:
The way I did this was I made a customer class called ProblemClass and then I make a new class for each problem that inherits from that class. After that, all I have to do is override the functions in ProblemClass and then the View Controller just loads all the information from the Problem1 Class that inherited from ProblemClass.
When it segue's to the view that loads from Problem1 (or any other problem), it gets the details by me setting the currentProblem variable during the segue. see below
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destination = segue.destination as! ProblemVC
let cell = sender as! ProblemsTVCell
let label = cell.label!
switch label.text! {
case "Problem 1":
destination.currentProblem = Problem1()
case "Problem 2":
destination.currentProblem = Problem2()
case "Problem 3":
destination.currentProblem = Problem3()
case "Problem 4":
destination.currentProblem = Problem4()
default:
break
}
}
Is there a way to assign the Problem#() with a string, so I don't have to make hundreds of these cases? I have no idea what it would look like, and trying to google kept returning answers about #selectors. I don't know what it really would look like, but in my mind I'm thinking something like this.
destination.currentProblem = Class(name: "Problem4")
Let me know if what I'm asking isn't clear and I'll try to explain better.
Thank you in advance!
---EDIT---
Adding in my solution from the selected answer below.
So I changed the prepare function to this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destination = segue.destination as! ProblemVC
let cell = sender as! ProblemsTVCell
let label = cell.label!
let problemName = label.text!.replacingOccurrences(of: " ", with: "")
let problemClass = NSClassFromString("Project_Euler.\(problemName)")! as! ProblemClass.Type
destination.currentProblem = problemClass.init()
}
And to get the init() in my ProblemClass I made my class look like this:
class ProblemClass {
required init() {
return
}
func title() -> String {
return "Title"
}
func problem() -> String {
return "Problem #"
}
func question() -> [String] {
return ["No Question Yet"]
}
func answer() -> [String] {
return ["No Answer Code Entered Yet"]
}
}

Perhaps:
let problemNumber: Int = 1
var className = "AppName.Problem\(problemNumber)"
let problemClass = NSClassFromString(className) as! Problem.Type
destination.currentProblem = problemClass

Related

How to iterate through a dictionary, loading the next item with button press in swift

I am making an app that will ask a series of questions, giving a value when answering yes or no. I have an initial view controller with a collection view of question types. This view controller passes the type information to a new view controller with the question layout (image, label, buttons). On the second view controller I have a switch/case statement to provide the proper dictionary results for the type that was passed in (via questionTypeReceived), populating the initial view correctly. Once you answer the question (simple yes or no) I want to update with the next question. Any advice to get this done?
Extra info: I have the questions view controller (referred to here as secondary VC) refactored because I initially thought I wanted to reuse it, not just reload it. All aspects work EXCEPT the reloading with the next question... Yes, that is because I don't have the code in there to do that. I have been trying different for loops in different places but keep getting errors so I go back to the current setup because there are no errors
Initial VC
let names = [ "Work", "School", "Family", "Friends", "Random" ]
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
questionType = names[indexPath.row] as String
let storyboard:UIStoryboard = UIStoryboard(name: "questions", bundle: nil)
let questionsVC = storyboard.instantiateViewController(withIdentifier: "questionsViewController") as! questionsViewController
questionsVC.questionTypeReceived = questionType
self.navigationController?.pushViewController(questionsVC, animated: true)
performSegue(withIdentifier: "question_segue", sender: questionType)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let dest = segue.destination as? questionsViewController else {
print("Can't perform downcast")
return
}
dest.questionTypeReceived = questionType
}
secondary VC
var questionTypeReceived:String = String()
var answerTotalValue:Double = Double()
var questionNumber:Int = Int()
var questionBody = ""
var questionType = ""
let answerYesValue:Double = Double.random(in: 1...5)
let answerNoValue:Double = Double.random(in: 0..<3)
switch questionTypeReceived {
case "Random":
questionTypeReceived = "Random"
questionsImageView.image = UIImage(named: randomImages.randomElement()!)
let questionNumber = randomQuestions.keys.sorted()
let questionBody = randomQuestions.values
for (questionNumber, questionBody) in randomQuestions {
questionTitleLabel.text = "Question #\(questionNumber)"
questionsTextLabel.text = questionBody
}
default: return
}
let randomQuestions: [Int : String] = [
1: "Did you already agree to attend?",
2: "Does it cost money to attend?",
3: "Is it time consuming (4+ hours)?"
]
#IBAction func yesButtonPressed(_ sender: UIButton) {
answerTotalValue += answerYesValue
print(answerTotalValue)
print(questionTypeReceived)
}
There seems to be some confusion on how to handle properties and local variables, you have for instance 1 property and 2 local variables named questionNumber and you are also iterating the whole dictionary when as I see it you only want one question.
One way to get away from this and a solution that is also good for other reason like not doing everything in your controller class and separating responsibilities is to move the questions and handling of them into a separate struct or class.
Let's create a Questions struct that holds the questions, keeps track of what questions has been asked and can deliver the next question to ask.
struct Questions {
private randomQuestions: [Int : String] = [
1: "Did you already agree to attend?",
2: "Does it cost money to attend?",
3: "Is it time consuming (4+ hours)?"
]
private var questionIndex = 0
mutating func nextQuestion() -> (number: Int, question: String?) {
questionIndex += 1
return (questionIndex, randomQuestions[questionIndex])
}
}
Note the question mark for the string in the return tuple, if no questions exists for a index (questionIndex > 3) the question returned will be nil
Then you can use it in you code like this, first declare it as a property in your VC
var questions = Questions()
Then when getting the question
switch questionTypeReceived {
case "Random":
questionsImageView.image = UIImage(named: randomImages.randomElement()!)
let nextQuestion = question.nextQuestion()
if let question = nextQuestion.question {
questionTitleLabel.text = "Question #\(nextQuestion.number)"
questionsTextLabel.text = question
} else {
// no more questions to ask, this needs to be handled
}
default: return
}
So a little more work to write a new struct but your code in the VC becomes simpler and a lot of properties and variables are no longer needed.

How to send multiple variables through segue

How can I send multiple variables through a segue in Swift? The QBBust gets sent over fine and prints on the view controller, but the QBName doesn't get sent over for some reason. Can anyone spot why?
if let send = sender as? Double{
destination.QBBust = send
}
if let sent = sender as? String{
destination.QBName = sent
}
}
}
private var _QBName:String!
var QBName: String{
get{
return _QBName
} set {
_QBName = newValue
}
}
private var _QBBust:Double!
var QBBust: Double {
get {
return _QBBust
} set{
_QBBust = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
let bust = String(Int(_QBBust))
QBBustLabel.text = "\(bust)%"
QBNameLabel.text = _QBName
}
This next part is in the button function that triggers the segue
performSegue(withIdentifier: "QBResultVC", sender: QBBust)
performSegue(withIdentifier: "QBResultVC", sender: QBName)
As in Tiago's answer, you can create a new struct or class which has QBName and QBBust properties. In addition, you can also use tuple in Swift.
This is an example:
in Destination ViewController
declare var QBInfo:(name: String, bust: Double)?
and in the button function that triggers the segue
let QBInfo = (name: QBName, bust: QBBust)
performSegue(withIdentifier: "QBResultVC", sender: QBBust)
then in prepareForSegue:sender:method
destination.QBInfo = QBInfo
This question is duplicate, but you can create a Struct or new Class, and storage your data how properties and send the 'transport' object in segue.
For detail, look this answser:
Swift sending Multiple Objects to View Controller

UserDefault not being saved when going back to viewcontroller 1 and then back to viewcontroller 2

Okay so I have two view controllers. One view controller loads up all the cells that is on my Plist and the second view controller opens up the cell and shows you the description. For example:
View Controller 1:
Dog
Cat
Mouse
Click on Dog cell it will take you to View Controller 2:
Dog goes Woof.
view controller 1 is written:
ovverride func prepare(for segue: UIStoryBoardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
let animals: Animals
if isFiltering() {
animals = filteredData[indexPath.row]
}
else {
animals = originalData[indexPath.row]
}
let controller = (segue.destination as! UINavigationController).topViewController as! SecondViewController
controller.detailedAnimals = animals
controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem
contrller.navigationItem.leftItemsSupplementBackButton = true
}
}
}
this is what i wrote in viewcontroller 2 updated
var isFavorite : Bool = false
#IBAction func addToFav(_ sender:UIButton) {
isFavorite = !isFavorite
UpdateButtonAppearance()
saveData()
}
private func UpdateButtonAppearance(){
if isFavorite{
let image = UIImage(named: "addFav")
favButton.setImage(image, for: . normal)
savedData()
}
else {
let image = UIImage(named: "addFavFilled")
favButton.setImage(image, for: . normal)
savedData()
}
}
ovveride func viewDidLoad(){
UpdateButtonAppearance()
saveData()
}
//updated code
func getFilePath () -> String {
var path: [AnyObject] = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) as [AnyObject]
let documentsDirectory: String = path[0] as! String
let filepath = documentsDirectory.appending("Animals.plist")
return filepath
}
func saveData(){
let myDict : NSMutableDictionary = NSMutableDictionary()
myDict["fav"] = NSNumber(booleanLiteral: isFavorite)
myDict.write(toFile: self.getFilePath(), atomically: true)
}
func getBool(){
if FileManager.default.fileExists(atPath: self.getFilePath()) {
var myDict = NSDictionary(contentsOfFile: self.getFilePath()) as!
[String:AnyObject]
let myBool: Bool = myDict["fav"]!.boolValue
isFavorite = myBool
}
I saw a tutorial on how to change the bool in the Plist and wrote it this way. The code compiles but I don't think it is changing the bool value. So on my Animals Plist i have a Item 0 type dictionary, first key is called Animal, type is string, value is "dog" and second key is Description, type is string, value is "dog goes woof" and third key is called fav, type is Bool and value is No for now but I am trying to change this value to Yes but it is not working. Also thank you so much for your comment it was easy to follow and easy to understand.
The star is not filled when you go back to the 2nd view controller because you set the images in the #IBAction func addToFav(_ sender:UIButton) method. That is when you tap the button.
You should have that part of the code in another method that you also call in didLoad().
2nd View Controller should be something like this:
var isFavorite = UserDefaults.standard.bool(forKey: "isFavorite")
func didLoad() {
super.didLoad()
updateButtonAppearance()
}
private updateButtonAppearance() {
if isFavorite {
let image = UIImage(named: "addFavFilled")
button.setImage(image, for: . normal)
}
else {
let image = UIImage(named: "addFav")
button.setImage(image, for: . normal)
}
}
#IBAction func addToFav(_ sender:UIButton) {
isFavorite = !isFavorite
UserDefaults.standard.set(isFavorite, forKey: "isFavorite")
updateButtonAppearance()
}
Also the code could be improved to not save the variable in UserDefaults in addToFav method, but whenever isFavourite is changed. Maybe you will later want to change the state of isFavourite in another method which will require code duplication.
Also note that you save a value for all pets in UserDefaults under isFavourite. That means if you favor one pet, all other pets will be favored and vice versa. Consider replacing the bool value in user defaults with a dictionary that has keys for each pet and booleans as values.

Issue passing data from API

This code is from the locations tableView view controller, where the locations are serialized from a Four Square API and I grab the data, passing it back the view controller where I create an event. The data is serialized correctly, populating the tableView and all, so I won't bother posting that for now, just the segue method to pass the data.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let indexPath = self.tableView.indexPathForSelectedRow
let item = self.responseItems![indexPath!.row]
var eventVC = CreateEventViewController()
if segue.identifier == "pushBackToEventVC" {
if let venue = item["venue"] as? NSDictionary {
let locationString = venue["name"] as! String
let location = venue["location"]
//get lat/long strings
print(location)
eventVC.updateWithLocation(locationString)
eventVC.updateWithGeoPoint(PFGeoPoint(latitude: locationLatitude, longitude: locationLongitude))
}
eventVC = segue.destinationViewController as! CreateEventViewController
//pass location coordinates
}
//the rest of the segue method
}
The update methods in the crete event view controller, when I put a breakpoint on these methods I can see the data has been passed correctly, and the location (a PFGeoPoint) works okay but the locationString (a string) throws a "Bad instruction error" although it does print it correctly to the console
func updateWithLocation(locationString: String) {
print(locationString)
self.locationLabel.text = locationString
}
func updateWithGeoPoint(venueLocation: PFGeoPoint) {
// print(venueLocation)
self.event?.location = venueLocation
self.eventLocation = venueLocation
print(self.eventLocation)
}
Any ideas? It's basically working but won't update the label with the string although I can see it's been passed correctly. Thanks for the help as always.
Try using this code block instead,
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "pushBackToEventVC") {
let detailViewController = ((segue.destinationViewController) as! CreateEventViewController)
let indexPath = self.tableView!.indexPathForSelectedRow!
detailViewController.locationString = venue["location"]
}
}

Swift and Parse nil on prepareforsegue

Im writing a table view controller using an array from Parse. I need to send data through the segue but I dont know why its always returning nil. I will have to send images and text, but for now im just taking the text from the label title1 to insert it into a variable type String named example.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toDetail" {
var detailScene = segue.destinationViewController as! detailViewController
let cell = sender as! UITableViewCell
let indexPath = tableView?.indexPathForCell(cell)
detailScene.example = self.timelineData[indexPath!.row].title1?.text
}
}
For the array I have used:
var timelineData : NSMutableArray = NSMutableArray()
And the function loaddata, that is used with the function viewDidAppear
func loaddata () {
timelineData.removeAllObjects()
var findTimelineData:PFQuery = PFQuery(className: "ii")
findTimelineData.findObjectsInBackgroundWithBlock{
(objects, error)->Void in
if error == nil{
for object in objects!{
let ii:PFObject = object as! PFObject
self.timelineData.addObject(ii)
}
let array:NSArray = self.timelineData.reverseObjectEnumerator().allObjects
self.timelineData = NSMutableArray(array: array)
self.tableView.reloadData()
}
}
}
Try this. It should at least get you to the point where you're grabbing the information from the correct cell. I'm assuming your timelineData array is full of text, so that's how I accessed it, by casting it as a String. If it is full of a different type of object you need to cast it differently.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "toDetail" {
if let detailScene = segue.destinationViewController as? detailViewController {
if let indexPath = tableView.indexPathForSelectedRow()?.row {
var object = self.timelineData[indexPath] as! PFObject
var title = object["title"] as! String
detailScene.example = title
}
}
}
}
It could be that the return value from indexPathForCell is returning nil or a bad value. There are quite a few articles about this function being unreliable.
A common suggestion is to use indexPathForRowAtPoint(cell.center) instead.
There is definitely something wrong with your last line of code:
detailScene.example = self.timelineData[indexPath!.row].title1?.text
I assume that self.timelineData array that holds some data.
If your array contain String class you don't need to append .title1?.text part.
If your array contain UILabel class you need change line to
detailScene.example = self.timelineData[indexPath!.row].text
because UILabel have no property called title1.
In general I think this happened because class in self.timelineData array have no property called title1.

Resources