英文:
Why isn't my data not transferring between my two View Controllers using UITableViews?
问题
Based on the additional information you provided, it seems like the issue you're facing is related to the order of execution in your code and the timing of data transfer to the custom cell. Let me address your concerns and provide some suggestions:
-
Order of Execution:
In yourRoutineTableViewCell
class, you are using a lazy property forstackView
, which means it gets initialized the first time it's accessed. However, thecell.randomSelectedExercises
is being set in yourtableView(_:cellForRowAt:)
method, which might happen after the lazy property is initialized. This is why you see an emptyrandomSelectedExercises
in yourstackView
. -
Data Transfer:
You're trying to setrandomSelectedExercises
in your cell after you create the cell instance, but as you've rightly mentioned, the order of execution makes it problematic.
To address this issue, you can consider the following approach:
- In your
RoutineTableViewCell
, you can add a method to set therandomSelectedExercises
array explicitly. This method should be called after you have configured the cell intableView(_:cellForRowAt:)
.
Here's an example of how you can modify your RoutineTableViewCell
class:
class RoutineTableViewCell: UITableViewCell {
// ... Other properties ...
// Function to set randomSelectedExercises
func setRandomSelectedExercises(_ exercises: [PFObject]) {
self.randomSelectedExercises = exercises
// After setting the data, you can update your UI elements here
// For example, you can create your stackView elements here.
// Make sure to reload the UI if necessary.
}
// ... Other code ...
}
In your tableView(_:cellForRowAt:)
method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
// ... Configure other aspects of the cell ...
cell.setRandomSelectedExercises(self.selectedExercises)
return cell
}
With this approach, you ensure that randomSelectedExercises
is set after the cell is created, allowing you to perform UI-related tasks in the setRandomSelectedExercises
method as needed.
Please adapt this example to your specific code structure and needs. This should help you make sure your data is properly transferred to your custom cell and is available when you configure your UI elements.
英文:
I am attempting to pass data from a the UITableView function cellForRowAt to a custom UITableViewCell. The cell constructs a UIStackView with n amount of UIViews inside of it. n is a count of items in an array and is dependent on the data that is suppose to be transferred (a count of items in that array). Something very confusing happens to me here. I have checked in the VC with the tableView that the data has successfully passed by using the following snippet of code
print("SELECTED EXERCISES: ", self.selectedExercises)
cell.randomSelectedExercises = self.selectedExercises
print("PRINTING FROM CELL: ", cell.randomSelectedExercise)
I can confirm that both of these print statements return non-empty arrays. So to me, this means that the custom cell has the data I require it to have. But, when I try to print out the very same array in the UITableViewCell swift file (randomSelectedExercises) , it returns empty to me. How is this possible? From what I understand, the cell works on creating the property initializers first, then 'self' becomes available. I had a previous error telling me this and to fix it, I turned my UIStackView initializer to lazy, but this is how I ended up with my current problem.
Here is the code in beginning that is relevant to the question that pertains to the table view. I have decided to present this code incase the issue is not in my cell but in my table view code:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
let workout = selectedWorkouts[indexPath.row]
//get the information we need - just the name at this point
let name = workout["name"] as! String
var randomInts = [Int]()
//perform a query
let query = PFQuery(className: "Exercise")
query.includeKey("associatedWorkout")
//filter by associated workout
query.whereKey("associatedWorkout", equalTo: workout)
query.findObjectsInBackground{ (exercises, error) in
if exercises != nil {
//Created an array of random integers... this code is irrelevant to the question
//Picking items from parent array. selectedExercises is a subarray
for num in randomInts {
//allExercises just contains every possible item to pick from
self.selectedExercises.append(self.allExercises[num-1])
}
//now we have our selected workouts
//Both print statements successfully print out correct information
print("SELECTED EXERCISES: ", self.selectedExercises)
cell.randomSelectedExercises = self.selectedExercises
print("PRINTING FROM CELL: ", cell.randomSelectedExercises)
//clear the arrays so we have fresh ones through each iteration
self.selectedExercises.removeAll(keepingCapacity: false)
self.allExercises.removeAll(keepingCapacity: false)
} else {
print("COULD NOT FIND WORKOUT")
}
}
//***This works as expected - workoutName is visible in cell***
cell.workoutName.text = name
//clear the used arrays
self.allExercises.removeAll(keepingCapacity: false)
self.selectedExercises.removeAll(keepingCapacity: false)
return cell
}
Below is the code that gives me a problem in the cell swift file. the randomSelectedExercise
does not have any data in it when I enter this area. This is an issue because in my for loop I am iterating from 1 to randomSelectedExercise.count. If this value is 0, I receive an error. The issue is focused in the UIStackView initializer:
import UIKit
import Parse
//Constants
let constantHeight = 50
//dynamic height number
var heightConstantConstraint: CGFloat = 10
class RoutineTableViewCell: UITableViewCell {
//will hold the randomly selected exercises that need to be displayed
//***This is where I thought the data would be saved, but it is not... Why???***
var randomSelectedExercises = [PFObject]()
static var reuseIdentifier: String {
return String(describing: self)
}
// MARK: Overrides
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentView.addSubview(containerView)
containerView.addSubview(workoutName)
containerView.addSubview(stackView)
NSLayoutConstraint.activate(staticConstraints(heightConstantConstraint: heightConstantConstraint))
//reset value
heightConstantConstraint = 10
}
//MARK: Elements
//Initializing workoutName UILabel...
let workoutName: UILabel = {...}()
//***I RECEIVE AN EMPTY ARRAY IN THE PRINT STATEMENT HERE SO NUM WILL BE 0 AND I WILL RECEIVE AN ERROR IN THE FOR LOOP***
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.backgroundColor = .gray
stackView.translatesAutoresizingMaskIntoConstraints = false
//not capturing the data here
print("rSE array:", randomSelectedExercises)
var num = randomSelectedExercises.count
for i in 1...num {
let newView = UIView(frame: CGRect(x: 0, y: (i*50)-50, width: 100, height: constantHeight))
heightConstantConstraint += CGFloat(constantHeight)
newView.backgroundColor = .purple
let newLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
newLabel.text = "Hello World"
newView.addSubview(newLabel)
stackView.addSubview(newView)
}
return stackView
}()
//initializing containerView UIView ...
let containerView: UIView = {...}()
//Setting the constraints for each component...
private func staticConstraints(heightConstantConstraint: CGFloat) -> [NSLayoutConstraint] {...}
}
Why is my data not properly transferring? How do I make my data transfer properly?
Edit
I believe I understand the problem better now thanks to @DonMag, so thank you very much. My data is not transferring properly because I am initializing my custom cell (creating the UILabel, UIStackView, randomSelectedExercise) with the default values I have given it. Then I am trying to update randomSelectedExercise
afterwards, which will do me no good with my current code for stackView
.
In respect to this, I have made a couple of changes and have omitted my background query as I felt it overcomplicated things for me. In my parse database, I have added the attribute associatedExercises
to my Workout
class. This attribute will contain an array of PFObjects of all the exercises associated with the working. At the beginning of this process, I have access to the selectedWorkouts
which I iterate through when creating each cell in my table view, which is simply called workout
in my cellForRowAt
function. Following this, I extract the associated workouts data and put it into allExercises
array. This is all I have changed since the initial question.
For full transparency, I will be pasting the entire code from the two files in question, followed with the questions I have now:
DisplayRoutineViewController.swift
import UIKit
import Parse
class DisplayRoutineViewController: UIViewController, UITableViewDelegate, UITableViewDataSource{
var workoutDict = [PFObject:Int]() //Int is how many of the object was picked from the previous VC
var selectedWorkouts = [PFObject]()
var allExercises = [PFObject]()
var selectedExercises = [PFObject]()
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
// Do any additional setup after loading the view.
print(workoutDict)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
//cell...
let workout = selectedWorkouts[indexPath.row]
//get the information we need - just the name at this point
let name = workout["name"] as! String
var randomInts = [Int]()
allExercises = workout["associatedExercises"] as! [PFObject] //new attribute in Prase database
let loopNumber = self.workoutDict[workout]!
print("This is for: ", name)
let maxExercises = self.allExercises.count
//needs to be a while loop so i have control over iterator
var iterator = 1
var breakFlag = false
while iterator <= loopNumber {
var n = Int.random(in: 1...maxExercises) //will give us numbers inclusive 1 through maxExercises
print("Random Number = \(n)")
if randomInts.isEmpty {
//case for when randomInts is empty
randomInts.append(n)
} else {
//case for when randomInt does have n in it
for i in randomInts {
breakFlag = false
if n == i {
iterator = iterator - 1
breakFlag = true
break
}
}
//case for when randomInts does not have n in it
if breakFlag == false {
randomInts.append(n)
}
}
print("This is randomInts array: ", randomInts)
iterator = iterator + 1
}
//at this point, we have randomInts filled with the numbers we need
for num in randomInts {
self.selectedExercises.append(self.allExercises[num-1]) //num-1 because indexes start at 0 and num is never 0
}
print("SELECTED EXERCISE HAPPENS NOW", self.selectedExercises)
//tableView.reloadData() //maybe this will help? - no it does not just makes an infinite loop
cell.randomSelectedExercises = self.selectedExercises
cell.workoutName.text = name
print("PRINTING FROM CELL: ", cell.randomSelectedExercises)
//upload the cells stackview with the information
//clear the arrays so we have fresh ones through each iteration
self.selectedExercises.removeAll(keepingCapacity: false)
self.allExercises.removeAll(keepingCapacity: false)
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return workoutDict.count
}
}
RoutineTableViewCell.swift
import UIKit
import Parse
//Constants
let constantHeight = 50
//dynamic height number
var heightConstantConstraint: CGFloat = 10
class RoutineTableViewCell: UITableViewCell {
//will hold the randomly selected exercises that need to be displayed
var randomSelectedExercises = [PFObject]()
static var reuseIdentifier: String {
return String(describing: self)
}
// MARK: Overrides
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentView.addSubview(containerView)
containerView.addSubview(workoutName)
containerView.addSubview(stackView)
NSLayoutConstraint.activate(staticConstraints(heightConstantConstraint: heightConstantConstraint))
//reset value
heightConstantConstraint = 10
}
//MARK: Elements
let workoutName: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 16)
label.lineBreakMode = .byTruncatingTail
// Setting the max number of allowed lines in title to 1
label.numberOfLines = 1
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
//added a lazy
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.backgroundColor = .gray
stackView.translatesAutoresizingMaskIntoConstraints = false
//make the views inside the stackview
//not capturing the data
print("Showing Captured Data", randomSelectedExercises)
var num = randomSelectedExercises.count
print("Expecting Number: ", num)
for i in 1...num {
//create UIViews
}
return stackView
}()
private let containerView: UIView = {
// wrapper to contain all the subviews for the UITableViewCell
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private func staticConstraints(heightConstantConstraint: CGFloat) -> [NSLayoutConstraint] {...}
}
In my console, the print statements in lazy var stackView
appear before any of the print statements in cellForRowAt
because I initialize cell
before these print statement. This is the issue DonMag I think was trying to point out to me but I do not understand how to switch the order of these outcomes. If I can do so, I'm guessing that information will pass to the cell correctly? Also, I have thought of doing
cell.randomSelectedExercises = self.selectedExercises
before let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
but obviously the namespace cell has not been created yet.
Please let me know how to solve this. I am at a complete roadblock...
答案1
得分: 1
以下是您在cellForRowAt
函数中正在执行的操作:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 获取一个单元格实例
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
// ...其他杂项操作
// 启动后台进程
query.findObjectsInBackground { (exercises, error) in
// 此处不会立即发生任何事情...它在后台进行
}
// ...其他杂项操作
return cell
// 在此时,您已经返回了单元格
// 并且您的后台查询正在执行其工作
}
因此,您实例化单元格,设置标签的文本,然后返回它。
该单元格创建了堆栈视图(带有空的“randomSelectedExercises”)并执行其他初始化/设置任务...
然后 - ***在后台查找对象完成后***,您设置了“randomSelectedExercises”。
您最有可能要做的是在生成“workout”对象数组时运行查询。
然后,您在`cellForRowAt`中已经拥有“random exercises”数组作为“workout”对象的一部分。
-------------
<details>
<summary>英文:</summary>
Here is what you're doing in your `cellForRowAt` func...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// get a cell instance
let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineTableViewCell") as! RoutineTableViewCell
// ... misc stuff
// start a background process
query.findObjectsInBackground { (exercises, error) in
// nothing here will happen yet... it happens in the background
}
// ... misc stuff
return cell
// at this point, you have returned the cell
// and your background query is doing its work
}
So you instantiate the cell, set the text of a label, and return it.
The cell creates the stack view (with an empty `randomSelectedExercises`) and does its other init /setup tasks...
And then - ***after your background find objects*** task completes, you set the `randomSelectedExercises`.
What you most likely want to do is run the queries while you are generating your array of "workout" objects.
Then you will already have your "random exercises" array as part of the "workout" object in `cellForRowAt`.
-------------
**Edit**
This is a fairly common program flow...
- at the beginning of `viewDidLoad()`
- setup the UI
- set the table view's datasource and delegate
- register the cell class
- at the end of `viewDidLoad()`
- add a `UIActivityIndicatorView` - a "spinner" - probably on top of your table view
- start the spinner animating
- call an async / background func to retrieve your data
When the background func has finished retrieving the data:
- stop the spinner animation (hide it)
- call `.reloadData()` on the table view
You're also confusing yourself on how to create and populate the stack view (list of exercises) in your cell. Right now you're trying to "populate" the stack view when it's created ... but at that point, the cell has no data to do so.
Instead, when you **initialize** your cell:
- create / add / constrain subviews (title label and stack view)
in `cellForRowAt`, you would:
- assign the data to the cell and **then** populate the "rows" in the stack view.
It is a little tough to help you any further without a better description of what you're trying to do... What are your data structures? Are you retrieving the "exercises" from your Parse database? Your code shows `randomSelectedExercises` ... does that mean every time the table view is shown there will be different exercises for each workout? Or, are you setting exercises for each "workout" in that database?
-----------
**Edit 2**
You're still confusing yourself about how to setup your cell and how to "fill" it with data.
In your cell class, you create a label and add it as a subview during init:
let workoutName: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 16)
// etc...
return label
}()
private func commonInit() {
contentView.addSubview(containerView)
containerView.addSubview(workoutName)
containerView.addSubview(stackView)
// etc...
}
Notice that you did ***NOT*** try to set the text of the label yet -- because you have no data for the cell at that point.
So, you're sort-of doing the same thing with your stack view, **but** you're trying to ***fill it with data*** when you create it.
What you want to do is create your stack view *empty* and then populate it **when you get the data**:
class RoutineTableViewCell: UITableViewCell {
var randomSelectedExercises = [PFObject]() {
didSet {
// populate your stack view here
}
}
By the way, you ***absolutely do NOT*** want to be running a query inside `cellForRowAt`. It will run every time you need a cell... ***including*** running again and again when you scroll cells in and out of view.
You need to setup your data structure so you can:
- query your db for ALL exercises
- query your db for Workouts
- loop through the Workouts and assign the random associated exercises
and **then** reload the table view. Now in `cellForRowAt` all you'll be doing is setting the `workoutName.text` and the `randomSelectedExercises`.
Now, as a side note... in this question (and your other, related questions), you've never explained or shown us what your ultimate goal is. Based on the information you've provided so far, I would probably tell you this would be much easier (and more flexible) if it was implemented as a multiple-section table view (where each "workout" is a section) rather than this approach.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论