Instantaneous data transfer to custom cell from viewcontroller - ios

I'm trying to activate a function inside my custom cell by setting the value of a boolean inside the custom cell class. This is my best attempt at doing this:
func blurViewActive(gestureRecognizer:UIGestureRecognizer) {
if (gestureRecognizer.state == UIGestureRecognizerState.Began) {
println("STATE BEGAN")
var point = gestureRecognizer.locationInView(self.tv)
if let indexPath = self.tv.indexPathForRowAtPoint(point) {
let data = messageList[indexPath.row] as Messages
let mcell: TableViewCell = self.tv.dequeueReusableCellWithIdentifier("cell") as TableViewCell
mcell.read = true
}
}
}
but this doesn't work, and I really have no idea how to do this any other way.
Here is the code for my custom cell class:
class TableViewCell: UITableViewCell, UIGestureRecognizerDelegate {
#IBOutlet weak var labelOutl: UILabel!
var timer = NSTimer()
var counter = 10
var read = Bool()
#IBOutlet weak var dateLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
if read == true{
println("hello")
}
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
func timerStarted(){
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "update", userInfo: nil, repeats: true)
}
func update(){
println(--counter)
}
}
My expected outcome is that once read has been set to "true" in my view controller, the function inside awakeFromNib-function should be executed instantaneously.

There seems to be a number of points of confusion here.
dequeueReusableCellWithIdentifier returns a cell for use in the table view it is called on. This will be either a newly instantiated cell or an existing cell not currently displayed in any of the table's visible rows. Setting the read property on this cell will therefore have no immediate visible effect.
If you want access to a visible cell you could use cellForRowAtIndexPath but even then changes made to that cell will not necessarily update the UI. Instead you probably want to update whatever model backs that cell and call reloadRowsAtIndexPaths to update a specific cell.
Additionally awakeFromNib will be called only when a new cell is created. That will be before it is returned from dequeueReusableCellWithIdentifier and therefore well before you can take any action on it, like setting its read property. It will also not be called once per row in your table or per displayed row since you are using a reuse identifier. Instead the table view will create at least one cell for each visible row and reuse them as row scroll into and out of sight. This is convenient because minimizing the number of objects created helps reduce memory use and reduces load which could slow down scrolling performance. However it means that your data source needs to be prepared to update these cells as they are reused from one row to the next.

Related

Change property of object in another view controller

I have a UITabBarController (as initial view controller) which checks connectivity status of the device. Everytime the connectivity status changes, a checkmark in a child UITableViewController cell (.accessoryType) should be set (.checkmark) or removed (.none)
Code in Tab Bar Controller:
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
let tvc = InfoTableViewController()
if path.status == .satisfied {
// set .checkmark in UITableViewController
let cell = tvc.statusOnlineCell
print("cell :", cell)
} else {
// set .none in UITableViewController
}
}
let queue = DispatchQueue.global(qos: .background)
monitor.start(queue: queue)
Outlet in UITableViewController:
#IBOutlet weak var statusOnlineCell: UITableViewCell!
I can change the accessory type from inside the UITableViewController class using .checkmark and .none.
statusOnlineCell.accessoryType = .checkmark
statusOnlineCell.accessoryType = .none
All fine so far!
However as soon as I try to access the UITableView.statusOnlineCell from UITabBar, I get nil. Hence, I cannot change its property outside the UITableViewController.
I saw 3 possible approaches:
A global variable, which reflects the online status. I could use the UITableView.viewDidAppear() method to change the statusOnlineCell accessory type. This works, but only if UITableView is not shown (only if another than UITableView is shown). If the UITableView is shown and I change the connectivity status, the view is not reloaded and I didn't find any way to achieve this. Is this possible?
Find a possibility to change the accessory type of UITableView.statusOnlineCell from the UITabBarController. Accessing the outlook returned in nil. Why is that? On top, after the accessory type would have changed, I would need to reload the view (for the case that the UITableView was active while changing connectivity status).
Is there any kind of (unknown to me) method which fires, when an object's property changed (à la needReload())? This would be too good to be true I believe.
To summarize - I need code to change the accessory type of a tableview cell, depending on the connectivity status, even whith this tableview being visible.
I read some tutorials and stackexchange articles, Google, ... but none did the job.
This was my top candidate, but I didn't manager to apply these examples to my situation.
https://learnappmaking.com/pass-data-between-view-controllers-swift-how-to/#back-delegation
I didn't want to use notifications since not really appropriate.
Any hint would be sufficient. Thanks in advance.
----- EDIT (14:52 UTC) ----- (requested by #vadian)
I added the (testing) code in the UITabBarController. The result of cell shows nil. So I cannot directly address the cell in UITableView from UITabBarController.
The UITableView doesn't have any related code yet since I directly address the property statusOnlineCell from UITabBarController in order to change its accessory type.
This approach is option 2. (of my 3 possibilities mentioned above).
I didn't find a solution, but a workaround.
Why no solution : the cell object was always nil if called from another class. Hence no chance to change it's property to .checkmark or .none. This was basically my key problem and unknown to me.
The workaround : a delegate! I declared the class, holding the cell, as delegate for the connectivity status change. Like that, the checkmark gets set or is removed from the cell, independantly whether the page is in view or not.
This runs when the app starts (UITabBarController):
protocol StatusOnlineChangedDelegate {
func updateStatusOnline(_ online: Bool)
}
class TabBarController: UITabBarController {
var socDelegate: StatusOnlineChangedDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let monitor = NWPathMonitor()
monitor.pathUpdateHandler = { path in
if path.status == .satisfied {
self.socDelegate?.updateStatusOnline(true)
} else {
self.socDelegate?.updateStatusOnline(false)
}
}
let queue = DispatchQueue.global(qos: .background)
monitor.start(queue: queue)
}
}
The UITableViewController holding the cell looks like this:
class InfoTableViewController: UITableViewController, StatusOnlineChangedDelegate {
#IBOutlet weak var statusOnlineCell: UITableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
TabBarController().socDelegate = self
}
func updateStatusOnline(_ online: Bool) {
DispatchQueue.main.async {
self.statusOnlineCell.accessoryType = (online ? .checkmark : .none)
}
}
}
Runs perfectly as desired.

buttons and labels resetting when scrolling through collection view

I have a collection view in which each cell possess the ability to be interacted with by the user. Each cell has a like button and a number of likes label. When the button is pressed, the button should turn cyan, and the label (which holds the number of likes) should increment. This setup currently works. However, when I scroll through the collection view and scroll back, the button reverts to its original color (white) and the label decrements down to its original value. I have heard of an ostensibly helpful method called prepareForReuse(), but perhaps I'm not using it correctly. Here is my code:
Here is the array which holds all the cells
var objects = [LikableObject]()
Here is the class definition for these objects
class LikableObject {
var numOfLikes: Int?
var isLikedByUser: Bool?
init(numOfLikes: Int, isLikedByUser: Bool) {
self.numOfLikes = numOfLikes
self.isLikedByUser = isLikedByUser
}
}
Mind you, there is more functionality present in this object, but they are irrelevant for the purposes of this question. One important thing to be noted is that the data for each cell are grabbed using an API. I'm using Alamofire to make requests to an API that will bring back the information for the numOfLikes and isLikedByUser properties for each cell.
Here is how I load up each cell using the collection view's delegate method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ObjectCell", for: indexPath) as! ObjectCell
cell.configureCell(
isLikedByUser: objects[indexPath.row].isLikedByUser!,
numOfLikes: objects[indexPath.row].numOfLikes!,
)
return cell
}
The ObjectCell class has these three fields:
var isLikedByUser: Bool?
#IBOutlet weak var numOfLikes: UILabel!
#IBOutlet weak var likeBtn: UIButton!
And that configureCell() method, which belongs to the cell class, is here:
public func configureCell(numOfLikes: Int, isLikedByUser: Bool) {
self.isLikedByUser = isLikedByUser
self.numOfLikes.text = String(numOfLikes)
if isLikedByUser {
self.likeBtn.setFATitleColor(color: UIColor.cyan, forState: .normal)
} else {
self.likeBtn.setFATitleColor(color: UIColor.white, forState: .normal)
}
}
And lastly, the prepareForReuse() method is here:
override func prepareForReuse() {
if isLikedByUser! {
self.likeBtn.setTitleColor(UIColor.cyan, for: .normal)
} else {
self.likeBtn.setTitleColor(UIColor.white, for: .normal)
}
}
This doesn't work. And even if it did, I still don't know a way to keep the numOfLikes label from decrementing, or if it should anyway. I'm speculating that a big part of this problem is that I'm not using the prepareForReuse() method correctly... Any help is appreciated, thank you.
prepareForReuse is not the place to modify the cell, as the name states, you "only" have to prepare it for reuse. if you changed something (for example isHidden property of a view), you just have to change them back to initial state.
What you should do though, you can implement didSet for isLikedByUser inside the cell, and apply your modifications to likeBtn in there. (this is of-course the fast solution)
Long solution: It's an anti-pattern that your cell has a property named isLikedByUser, TableViewCell is a View and in all architectures, Views should be as dumb as they can about business logic. the right way is to apply these modifications in configure-cell method which is implemented in ViewController.
If you feel you'll reuse this cell in different viewControllers a lot, at least defined it by a protocol and talk to your cell through that protocol. This way you'll have a more reusable and maintainable code.
Currently all of this is good , the only missing part is cell reusing , you have to reflect the changes in the number of likes to your model array
class ObjectCell:UICollectionViewCell {
var myObject:LikableObject!
}
In cellForRowAt
cell.myObject = objects[indexPath.row]
Now inside cell custom class you have the object reflect any change to it , sure you can use delegate / callback or any observation technique
The prepareForResuse isn't needed here.
You do need to update the model underlying the tableview. One way to verify this is with mock data that is pre-liked and see if that data displays properly.

Need help setting UISwitch in custom cell (XIB, Swift 4, Xcode 9)

Successes so far: I have a remote data source. Data gets pulled dynamically into a View Controller. The data is used to name a .title and .subtitle on each of the reusable custom cells. Also, each custom cell has a UISwitch, which I have been able to get functional for sending out both a “subscribe” signal for push notifications (for a given group identified by the cell’s title/subtitle) and an “unsubscribe” signal as well.
My one remaining issue: Whenever the user "revisits" the settings VC, while my code is "resetting" the UISwitches, it causes the following warnings in Xcode 9.2:
UISwitch.on must be used from main thread
UISwitch.setOn(_:animated:) must be used from main thread only
-[UISwitch setOn:animated:notifyingVisualElement:] must be used from main thread
The code below "works" -- however the desired result happens rather slowly (the UISwitches that are indeed supposed to be "on" take a good while to finally flip to "on").
More details:
What is needed: Whenever the VC is either shown or "re-shown," I need to "reset" the custom cell’s UISwitch to "on" if the user is subscribed to the given group, and to "off" if the user is not subscribed. Ideally, each time the VC is displayed, something should reach out and touch the OneSignal server and find out that user’s “subscribe state” for each group, using the OneSignal.getTags() function. I have that part working. This code is in the VC. But I need to do it the right way, to suit proper protocols regarding threading.
VC file, “ViewController_13_Settings.swift” holds a Table View with the reusable custom cell.
Table View file is named "CustomTableViewCell.swift"
The custom cell is called "customCell" (I know, my names are all really creative).
The custom cell (designed in XIB) has only three items inside it:
Title – A displayed “friendly name” of a “group” to be subscribed to or unsubscribed from. Set from the remote data source
Subtitle – A hidden “database name” of the aforementioned group. Hidden from the user. Set from the remote data source.
UISwitch - named "switchMinistryGroupList"
How do I properly set the UISwitch programmatically?
Here is the code in ViewController_13_Settings.swift that seems pertinent:
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomTableViewCell
// set cell's title and subtitle
cell.textLabelMinistryGroupList?.text = MinistryGroupArray[indexPath.row]
cell.textHiddenUserTagName?.text = OneSignalUserTagArray[indexPath.row]
// set the custom cell's UISwitch.
OneSignal.getTags({ tags in
print("tags - \(tags!)")
self.OneSignalUserTags = String(describing: tags)
print("OneSignalUserTags, from within the OneSignal func, = \(self.OneSignalUserTags)")
if self.OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
}, onFailure: { error in
print("Error getting tags - \(String(describing: error?.localizedDescription))")
// errorWithDomain - OneSignalError
// code - HTTP error code from the OneSignal server
// userInfo - JSON OneSignal responded with
})
viewWillAppear(true)
return cell
}
}
In the above portion of the VC code, this part (below) is what is functioning but apparently not in a way the uses threading properly:
if OneSignalUserTags.range(of: cell.textHiddenUserTagName.text!) != nil {
print("The \(cell.textHiddenUserTagName.text!) UserTag exists for this device.")
cell.switchMinistryGroupList.isOn = true
} else {
cell.switchMinistryGroupList.isOn = false
}
It's not entirely clear what your code is doing, but there seems to be a few things that need sorting out, that will help you solve your problem.
1) Improve the naming of your objects. This helps others see what's going on when asking questions.
Don't call your cell CustomTableViewCell - call it, say, MinistryCell or something that represents the data its displaying. Rather than textLabelMinistryGroupList and textHiddenUserTagName tree ministryGroup and userTagName etc.
2) Let the cell populate itself. Make your IBOutlets in your cell private so you can't assign to them directly in your view controller. This is a bad habit!
3) Create an object (Ministry, say) that corresponds to the data you're assigning to the cell. Assign this to the cell and let the cell assign to its Outlets.
4) Never call viewWillAppear, or anything like it! These are called by the system.
You'll end up with something like this:
In your view controller
struct Ministry {
let group: String
let userTag: String
var tagExists: Bool?
}
You should create an array var ministries: [Ministry] and populate it at the start, rather than dealing with MinistryGroupArray and OneSignalUserTagArray separately.
In your cell
class MinistryCell: UITableViewCell {
#IBOutlet private weak var ministryGroup: UILabel!
#IBOutlet private weak var userTagName: UILabel!
#IBOutlet private weak var switch: UISwitch!
var ministry: Ministry? {
didSet {
ministryGroup.text = ministry?.group
userTagName.text = ministry?.userTag
if let tagExists = ministry?.tagExists {
switch.isEnabled = false
switch.isOn = tagExists
} else {
// We don't know the current state - disable the switch?
switch.isEnabled = false
}
}
}
}
Then you dataSource method will look like…
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! MinistryCell
let ministry = ministries[indexPath.row]
cell.ministry = ministry
if ministry.tagExists == nil {
OneSignal.getTags { tags in
// Success - so update the corresponding ministry.tagExists
// then reload the cell at this indexPath
}, onFailure: { error in
print("Error")
})
}
return cell
}

Swift animate view in tableviewcell

For an app that I'm developing, I'm using a tableview. My tableview, has of course a tableviewcell. In that tableviewcell I'm adding a view programmatically (so nothing in storyboard). It's a view where I draw some lines and text and that becomes a messagebubble. If the messagebubble is seen by the other user you sent it too , a line of the bubble will go open.
So I have the animation function inside the class of that UIView (sendbubble.swift)
Now, it already checks if it is read or not and it opens the right bubble. But normally it should animate (the line that goes open should rotate) in 0.6 seconds. But it animates instantly. So my question is, how do I animate it with a duration?
I would also prefer to still call it in my custom UIView class (sendbubble.swift) . Maybe I need code in my function to check if the cell is presented on my iphone?
Thanks in advance!
func openMessage() {
UIView.animate(withDuration: 0.6, delay: 0.0, options: [], animations: {
var t = CATransform3DIdentity;
t = CATransform3DMakeRotation(CGFloat(3 * Float.pi / 4), 0, 0, 1)
self.moveableLineLayer.transform = t;
}, completion:{(finished:Bool) in })
}
First you need to grab the cell.
Get the indexPath of that cell where you need to show open bubble.
Get the cell from tableView.cellForRowAt(at:indexPath)
We will now have access to that bubble view now you can animate using the same function func openMessage()
Any question? comment.
You need to implement the UITableViewDelegate method tableView(_:willDisplay:forRowAt:) https://developer.apple.com/reference/uikit/uitableviewdelegate/1614883-tableview
That is triggered when the cell is about to be drawn - not when it is created. You will also need to store a state in your model that says if the animation has occurred, otherwise it will happen every time the cell comes back into view.
EXAMPLE
In the view controller (pseudo code)
class CustomViewController: UITableViewDelegate {
//where ever you define your tableview
var tableView:UITableView
tableView.delegate = self
var dataSource //some array that is defining your cells - each object has a property call hasAnimated
func tableView(_ tableView: UITableView, willDisplay cell: ITableViewCell,
forRowAt indexPath: IndexPath) {
if let cell = cell as? CustomTableViewCell, dataSource[indexPath.row].hasAnimated == false {
dataSource[indexPath.row].hasAnimated = true
cell.openMessage()
}
}
}
class CustomTableViewCell: UITableViewCell {
func openMessage() { //your method
}
}

Trouble with tap gestures

I have a tap gesture on a UIImageView within a class that extends UITableViewCell. This code should work, I don't see why it doesn't. The only thing I am iffy on is what the "target" should be - should it be the profileImage, or the overall ViewController that things are in?
#IBOutlet weak var profileImage: UIImageView!
var vc: TweetsViewController? = nil
override func awakeFromNib() {
super.awakeFromNib()
let tapGester = UITapGestureRecognizer(target: vc, action: Selector("handleTapGester:"))
tapGester.delegate = self
profileImage.addGestureRecognizer(tapGester)
}
func handleTapGester(tapGesture: UITapGestureRecognizer) {
print("*******hi*******")
vc?.performSegueWithIdentifier("showProfile", sender: nil)
}
And for the record, as this may seem like a relevant error, I initialize vc when the table cell loads.
The target should be the object that will handle the tap gesture and the handleTapGester function should be inside the object class you specified as the target, not inside the UITableViewCell subclass.
You also need to enable user interaction on the UIImageView by saying:
imageView.userInteractionEnabled = true
Why not just add a tap gesture recogniser to the view, then when called query indexPathForRowAtPoint to find out which cell is being tapped?
If you know the cell you can then determine if the UIImage is being tapped and make your call to performSegueWithIdentifier from there.
If it's tapped on a cell that you're not interested in let it fall through and be handled by the table by calling cancelsTouchesInView on the recogniser.

Resources