I am trying to create an app where I have 4 buttons which each one corresponds to a different category. Now the categories are different Realm Objects saved in a swift file.
class HealthData: Object {
#objc dynamic var name : String = ""
}
class SelfImprovement: Object {
#objc dynamic var name : String = ""
}
class TopSecret: Object {
#objc dynamic var name : String = ""
}
class Ohter: Object {
#objc dynamic var name : String = ""
}
Now my problem is that I want a single view controller with a TableView to have different data that will get passed on to TableView from the corresponding category.
My idea was that I can create the var categories : Results<HealthData>! and use an if statement to change the categories to be Results etc using the prepare for a segue to know which button was pressed.
override func viewWillAppear(_ animated: Bool) {
if categoryNo == 1 {
title = "Health"
} else if categoryNo == 2 {
title = "Self Improvement"
categories = Results<SelfImprovement>!
}
}
But of course, XCode cannot assign the value of type 'Results?.Type' to type 'Results?'.
Any ideas?
Thank you all for your time!
So the issue is you want to re-use the tableView to display data from different tableView datasources. You're heading down the right path but the answer is to tell the tableView where to get it's data from.
I am pairing this down to really basic code so don't copy paste - trying to keep it short.
Assume we have a view controller with a tableView
class ViewController: NSViewController, NSTableViewDelegate, NSTableViewDataSource {
#IBOutlet weak var myTableView: NSTableView!
var myHealthArray = [String]() //one tableview datasource
var mySelfImprovementArray = [String]() //another tableview datasource
var tableType = "" //will tell the tableView what it should be displaying
then, further down, we have the tableView delegate methods that populate the tableview
func numberOfRows(in tableView: NSTableView) -> Int {
if self.tableType == "health" {
return self.myHealthArray.count
} else if self.tableType == "self_improvement" {
return self.mySelfImprovementArray.count
} //etc
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
let cell = tableView.makeView(withIdentifier: myIdentifier, owner: self) as! NSTableCellView
var s = ""
if self.tableType == "health" {
s = self.myHealthArray[row]
} else if self.tableType == "self_improvement" {
s = self.mySelfImprovementArray[row]
} //etc
cell.textField?.stringValue = s
return cell
}
So the idea here is to set the tableType to whatever is supposed to be displayed in the tableView then the logic within the tableView delegate methods will know which dataSource array to get its data from.
It's important to remember that Realm Results are homogenous so they can only store one type of object.
In this example, I defined the dataSources as strings but in your case they may be realm results objects. If they are strings, you could just re-use the array as it would only contain string objects. However, if the dataSources are Realm Results, you'll need separate arrays as shown in this answer.
Related
I have a UIViewController containing a UITableView that is populated via an array of custom class objects. These custom class objects have an array property. As you can see from my code below, the objects are equatable. When I segue to a second vC, the array of custom class objects (and obviously the array properties associated with each object) is passed over.
I have a function in the second vC that matches an object with one that is contained in the array. After matching, the property array of the object in the array that matched is updated. However, when I print what should be the updated property array, no change has been made. Below is a representation of my code:
class Object: Equatable {
var propertyArray: [String] = []
static func ==(lhs: object, rhs: object) -> Bool {
return lhs.someProperty == rhs.someProperty
}
}
class ArrayOfObjects {
var array: [Object] = []
}
class vC1: UIViewController, UITableViewDelegate, UITableViewDataSource {
var objectArray1 = ArrayOfObjects()
override viewDidLoad() {
//populate objectArray1.array
}
prepareForSegue(segue: UIStoryboardSegue) {
segue.destination.objectArray2 = objectArray1 //segue to vC2
}
}
class vC2: UIViewController {
var objectArray2 = ArrayOfObjects()
var someOtherObject = Object() //object that has same properties as an object in objectArray2
func someFunc() {
let indexOfMatchingObject = objectArray2.array.index(of: someOtherObject)
let matchingObject = objectArray2.array[indexOfSomeOtherObject]
matchingObject.propertyArray.append("TestString")
print("\(matchingObejct.propertyArray)") //prints []
}
}
Why doesn't it print ["TestString"]? The same goes for when I remove one of the values from the array, the update doesnt occur.
If you are wondering why I am doing this, it's because the objects are modified in the second vC and the UI of the tableView cells in the first vC are dependent upon the properties of the objects. Hence why my data is represented as a class (reference type).
Update your someFunc():
func someFunc() {
//try to find the same object
let indexOfMatchingObject = objectArray2.array.index(of: someOtherObject)
//obtain this common objec from array
let matchingObject = objectArray2.array[indexOfSomeOtherObject]
//update propertyArray from matchingObject
matchingObject.propertyArray.append("TestString")
}
Also update your first VC table view in viewWillApear.
I realised that the answer to my question is nothing to do with the class I had created or the segue, but is in actual fact to do with the state of the array. You cannot append to an array that has not been initialised. In order to append to it, you must first initialise it, then you may append to it. Playground code has been provided below to demonstrate this in the context of the question above:
//Create object class
class Object: Equatable {
var propertyArray: [String]?
var id: Int = Int()
static func ==(lhs: Object, rhs: Object) -> Bool {
return lhs.id == rhs.id
}
}
//Create class that will hold array of objects
class ArrayOfObjects {
var array: [Object] = []
}
//initialise class that will hold array of objects
var array = ArrayOfObjects()
//set the initial values for the array
func setArray() {
let object1 = Object()
object1.id = 1
object1.propertyArray = ["hello"]
let object2a = Object()
object2a.id = 2
object2a.propertyArray = ["bye"]
array.array = [object1, object2a]
}
setArray()
//Create new object that will be used to match with one already in the array
let object2b = Object()
object2b.id = 2
object2b.propertyArray = ["bye"]
//Find if the new object exists in the array
let index = array.array.index(of: object2b)
let matchingObject = array.array[index!]
matchingObject.propertyArray?.append("welcome")
//We were able to append to the matchingObject (object2a) because the property array had been initialised
print(matchingObject.propertyArray) //prints ["bye", "welcome"]
//Create new object with uninitialised propertyArray
let object3a = Object()
object3a.id = 4
//Append this new object to the array
array.array.append(object3a)
//Create another new object that will be used to match with object3a
var object3b = Object()
object3b.id = 4
//Find if object3b can be matched in the array
let index2 = array.array.index(of: object3b)
let matchingObject2 = array.array[index2!]
matchingObject2.propertyArray?.append("hello")
//A match was found for object3b, but the array had not been initialised and so we couldn't append to it
print(matchingObject2.propertyArray) //prints nil
//Initialise the array
matchingObject2.propertyArray = []
matchingObject2.propertyArray?.append("goodbye")
//We can now append to it as it has been initialised
print(matchingObject2.propertyArray) //prints ["goodbye"]
In my WalletTableViewController I have two functions, used to calculate the Wallet Value:
A. updateCellValue() Is called by reloadData() with the tableView and uses indexPath.row to fetch a value (price) and an amount (number of coins) corresponding to the cell and make a calculation to get the total value of that coin (amountValue = value * amount). That is then saved with Core Data.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! WalletTableViewCell
cell.delegate = self
cell.amountTextField.delegate = self
updateCellValue(cell, atRow: indexPath.row)
return cell
}
func updateCellValue(_ walletTableViewCell: WalletTableViewCell, atRow row: Int) {
var newCryptos : [CryptosMO] = []
var doubleAmount = 0.0
if CoreDataHandler.fetchObject() != nil {
newCryptos = CoreDataHandler.fetchObject()!
}
cryptoPrice = cryptos[row].code!
guard let cryptoDoublePrice = CryptoInfo.cryptoPriceDic[cryptoPrice] else { return }
let selectedAmount = newCryptos[row]
guard let amount = selectedAmount.amount else { return }
var currentAmountValue = selectedAmount.amountValue
doubleAmount = Double(amount)!
let calculation = cryptoDoublePrice * doubleAmount
currentAmountValue = String(calculation)
CoreDataHandler.editObject(editObject: selectedAmount, amount: amount, amountValue: currentAmountValue)
updateWalletValue()
}
B. updateWalletValue() Is a function that fetches all the amountValue objects in Core Data and adds them together to calculate the Wallet Value.
func updateWalletValue() {
var items : [CryptosMO] = []
if CoreDataHandler.fetchObject() != nil {
items = CoreDataHandler.fetchObject()!
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
In my MainViewController, the Wallet Value is displayed too, but how can I refresh it's value?
func updateMainVCWalletLabel() {
//... what can I do here??
}
This works great for the WalletViewController of course with the TableView and indexPath, but how can I call updateCellValue from the MainViewController to keep the value updated?
The WalletViewController is instantiated and pushed from the MainViewController :
#IBAction func walletButtonTapped(_ sender: Any) {
let walletViewController = self.storyboard?.instantiateViewController(withIdentifier: "walletTableViewController")
self.present(walletViewController!, animated: true)
}
If you want to use a single method in multiple view controllers you should implement that method where you can call that method from anywhere. For example you can use singleton class here.
Create a swift file and name it as your wish (like WalletHelper or WalletManager)
Then you will get a file with the following format
class WalletHelper: NSObject
{
}
Create a shared instance for that class
static let shared = WalletHelper()
Implement the method you want
func getWalletValue() -> Float {
// write your code to get wallet value`
// and return the calculated value
}
Finally call that method like
let walletValue = WalletHelper.shared. getWalletValue()
WalletHelper.swift looks like
import UIKit
class WalletHelper: NSObject
{
static let shared = WalletHelper()
func getWalletValue() -> Float {
// write your code to get wallet value
// and return the calculated value
}
}
Update (old answer below)
To me it is absolutly unclear what you want to achieve: Which value do you want to be updated? The staticTotal?
Seems a litte like an XYProblem to me. As #vadian commented yesterday, please clearly describe where the data is stored, how the controllers are connected, what you want to update when in order to achieve what. You could also provide a MCVE which makes clear what you are asking, instead of adding more and more code snippets.
And, even more interesting: Why do you modify CoreData entries (CoreDataHandler.editObject) when you are in the call stack of tableView(_: cellForRowAt:)? Never ever ever do so! You are in a reading case - reloadData is intended to update the table view to reflect the data changes after the data has been changed. It is not intended to update the data itself. tableView(_: cellForRowAt:) is called many many times, especially when the user scrolls up and down, so you are causing large write impacts (and therefore: performance losses) when you write into the database.
Old Post
You could just call reloadData on the table view, which then will update it's cells.
There are also a few issues with your code:
Why do you call updateWalletValue() that frequently? Every time a cell is being displayed, it will be called, run through the whole database and do some reduce work. You should cache the value and only update it if the data itself is modified
Why do you call fetchObject() twice (in updateWalletValue())?
You should do this:
func updateWalletValue() {
guard let items:[CryptosMO] = CoreDataHandler.fetchObject() else {
WalletTableViewController.staticTotal = 0.0
return
}
total = items.reduce(0.0, { $0 + Double($1.amountValue)! } )
WalletTableViewController.staticTotal = total
}
I'm working on an iOS application using Swift and I'm using NSMutableArray, and when I try to add UISearchController to my UITableViewController It give me this error
1.1 click here, please
But when I try to do it with NSArray it works great.
If you wondering about Why am I using NSMutableArray?
Because I need it to pass the object of the NSMutableArray that is in the selected row from the UITableViewController to another UIViewController like this:
1.2 click here, please
What I have to do now, how can I adding search bar?
Thanks in advance.
You can hack it by converting the mutable array to a nsarray and then using that.
Where your doing the stuff in 1.1 just add before the the self.businessNamesArray line add this line
var regArray = self.businessNamesArray as NSArray as! [String]
then change the self.businessNamesArray line to regArray.filter{ rest of your code here
If you use regular Swift arrays, which I think is the right idea since you're using Swift, you can get it working if you change your array variable type and fix your array filtering syntax. First, change the array declaration to this:
var BusinessNamesArray:[String]?
The filter function your are using on your self.BusinessNamesArray returns a filtered array rather than filtering the array in place (which is what it seems like you want). If you want to replace the content of your self.BusinessNamesArray, you would need to do something like this:
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.BusinessNamesArray = self.BusinessNamesArray.filter { (business:String) -> Bool in
return true
// You probably want to compare strings here like this instead:
// guard let searchText = searchController.searchBar.text else {
// return false
// }
// return business.hasPrefix(searchText)
}
// You probably also need to reload your table view at this point:
self.tableView.reloadData()
}
Keep in mind though that your BusinessNamesArray is now filtered and you can't un-filter it. Instead, you should probably keep a second array of your Strings called something like searchResults. Then you can use that for filtering and maintain your original list of business names in the original array. So you would add a class variable called var searchResults:[String]? and the search filtering code would change to.
func updateSearchResultsForSearchController(searchController: UISearchController) {
self.searchResults = self.BusinessNamesArray.filter { (business:String) -> Bool in
guard let searchText = searchController.searchBar.text else {
return false
}
return business.hasPrefix(searchText)
}
self.tableView.reloadData()
}
When you call reloadData on your table view at this point, you will probably want to check and see if your search controller is active in the table view delegate's numberOfSectionsInTableView as well other delegate functions and use the searchResults array if it is active and the BusinessNamesArray if its not--something like:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if (self.resultSearchController.active) {
return self.searchResults.count ?? 0
} else {
return self.BusinessNamesArray.count ?? 0
}
}
(The self.resultSearchController variable is a local instance of UISearchController. I'm not sure what you've named yours.)
You'll then use similar code to decide which items to grab for your prepareForSegue: code:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Guard your variables here so you can return early if they're
// not valid
guard let upcoming = segue.destinationViewController as? DetailsViewController,
myindexpath = self.tableView.indexPathForSelectedRow else {
return
}
var titleString = ""
if (self.resultSearchController.active) {
if let searchResults = self.searchResults {
titleString = searchResults[myindexpath.row]
}
} else {
if let businessNames = self.BusinessNamesArray {
titleString = businessNames[myindexpath.row]
}
}
upcoming.titleString = titleString
self.deselectRowAtIndexPath(myindexpath, animated: true)
}
I'm trying to figure out how the array works with Swift. I understand that you use let to create an immutable array and var to create a mutable array. Yet, Swift's array is not quite the same as Objective's NSArray and NSMutableArray. I get that.
In the example that follows, I create a mutable array with one element. Actually, I want to start with no element. But if I do, I won't be able to add a new string to it. If I start with one element, then I won't be able to add a new string to it after the original element is removed. So what am I doing wrong?
Thank you
EDIT
class ViewController: UIViewController {
let textCellIdentifier = "TextCell"
var myArray:[String] = ["GGG"] // or var myArray:[String] = []
#IBAction func add1Tapped(sender:AnyObject) {
let index = tableView1.indexPathForSelectedRow;
let selectedRow = index()?.row
if selectedRow < 0 {
return
} else {
let txt = nameField1.text
myArray.append(txt)
tableView1.reloadData()
}
}
func tableView(tableView: UITableView,numberOfRowsInSection section:Int) -> Int {
return myArray.count
}
func tableView(tableView:UITableView,cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell {
let cell:UITableViewCell=UITableViewCell(style: UITableViewCellStyle.Subtitle,reuseIdentifier:textCellIdentifier)
let row = indexPath.row
cell.textLabel!.text = myArray[row]
return cell
}
}
all your needs should work as expected:
// create an empty array of strings
var myArray: [String] = []
// add two elements
myArray.append("the first element")
myArray.append("the second element")
// remove both elements
myArray.removeAll()
// add another element
myArray.append("the third but now first element")
myArray.count
EDIT
try and change your add method like this:
#IBAction func add1Tapped(sender:AnyObject) {
if let _ = tableView1.indexPathForSelectedRow, txt = nameField1.text {
print("will append \(txt) to myArray")
myArray.append(txt)
tableView1.reloadData()
}
}
I'm playing around with custom cells.
With the great help from the stackoverflow community i've been able to put some code together. I'm able to fetch array values from a text string into a custom cell uilabel and uibutton, but the issue is - the fetched result is always the last object in the array.
Here is the code
func setUpQuestion()
{
// setting variables
var Question: String?
var option1: String?
// using a text string with custom separators
let text = ">>Here is the grocery question\n>>and another one\n--Apples\n-
-Oranges\n[pickApples]pickOranges\n[pickApples2]"
// splitting this string into four different arrays depending on the separator
let lines = split(text) { $0 == "\n" }
for line in lines {
if line.hasPrefix(">>") {
Question = line.substringFromIndex(advance(line.startIndex, 2))
} else if line.hasPrefix("[") {
if let index = line.rangeOfString("]")?.startIndex {
option1 = line.substringWithRange(Range<String.Index>(
start: advance(line.startIndex, 1), end: index))
}
}
}
// creating variables for appending the values - here I'm using a custom class called QuestionMark created in a separate .swift file
var question1 = QuestionMark(Question: Question!, option:option1!)
var question2 = QuestionMark(Question: Question!, option:option1!)
// appending the values into uilabel and uibutton in the custom cell
arrayOfQuestions.append(question1)
arrayOfQuestions.append(question2)
}
// regular tableView protocol functions
func tableView(tableView: UITableView, numberOfRowsInSection
section: Int) ->Int
{
return arrayOfQuestions.count
}
func updateCount(){
if let list = mainTableView.indexPathsForSelectedRows() as? [NSIndexPath] {
rowsCount.text = String(list.count)
}
}
func tableView(tableView: UITableView, cellForRowAtIndexPath
indexPath: NSIndexPath) -> UITableViewCell
{
let cell: CustomCellForTableViewTableViewCell = tableView.dequeueReusableCellWithIdentifier("Cell") as! CustomCellForTableViewTableViewCell
// the SetCell function i'm using here was created in a separate .swift file
let quest = arrayOfQuestions[indexPath.row]
cell.setCell(quest.Questme!, optionone: quest.optionize!)
cell.optionOne.backgroundColor = UIColor.redColor()
return cell
}
Here are the additional codes i'm using for the class and setCell function
class QuestionMark
{
var Questme: String?
var optionize: String?
init(Question: String, option: String)
{
self.Questme = Question
self.optionize = option
}
// separate swift file
func setCell(Question: String, optionone: String)
{
self.mainText.text = Question
self.optionOne.setTitle(optionone, forState:UIControlState.Normal)
}
As a result in both cells i'm getting the last object from the text string and it looks like this
And another one - PickApples2
And another one - PickApples2
How do i start appending cells from the first array value and then move forward to second,third,fourth ?
Any ideas are greatly appreciated.
Thank you.
First of all, the syntax of the text to parse is pretty complicated ;-) …
Second of all, the problem to get always the last object is that you create the array of questions after the repeat loop. At that moment the variables question and option contain always the last found string.
Here a solution:
After getting a question a new object QuestionMark is created and appended to the array (without the optionize property)
After getting an option the appropriate QuestionMark object is fetched from the array by an index counter, the property optionize is set and the counter is increased.
Two notes:
Variable names should always start with a lowercase letter. Even the syntax highlighter of StackOverflow follows that naming convention.
In my solution all variables are non-optionals.
class QuestionMark
{
var questme: String
var optionize: String
init(question: String, option: String = "")
{
self.questme = question
self.optionize = option
}
...
var arrayOfQuestions = [QuestionMark]()
func setupQuestion() {
let text = ">>Here is the grocery question\n>>and another one\n--Apples\n--Oranges\n[pickApples]pickOranges\n[pickApples2]"
// splitting this string into four different arrays depending on the separator
var counter = 0
var question = ""
var option = ""
let lines = split(text) { $0 == "\n" }
for line in lines {
if line.hasPrefix(">>") {
question = line.substringFromIndex(advance(line.startIndex, 2))
let questionMark = QuestionMark(question: question)
arrayOfQuestions.append(questionMark)
} else if line.hasPrefix("[") {
if let index = line.rangeOfString("]")?.startIndex {
option = line.substringWithRange(Range<String.Index>(
start: advance(line.startIndex, 1), end: index))
let questionMark = arrayOfQuestions[counter]
questionMark.optionize = option
counter++
}
}
}
}