I'm trying to implement a "forward message" feature in my chat app. So, when I click on the button "forward" in my chatVC the popover VC appears with the users that are on my friends list.
The question is, can I call my chatVC viewDidLoad to refresh the user information? (Otherwise, the user information would be the same as it was before clicking the "forward" button.)
Here's my code where delegate is my chatVC:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let friend = friendsList[indexPath.row]
delegate?.friendProfileImage = friend.profileImage
delegate?.friendName = friend.name
delegate?.friendId = friend.id
delegate?.friendIsOnline = friend.isOnline
delegate?.friendLastLogin = friend.lastLogin
delegate?.forwardedMessage = true
delegate?.messages = []
delegate?.collectionView.reloadData()
delegate?.loadNewMessages = false
delegate?.viewDidLoad()
dismiss(animated: true, completion: nil)
}
As a rule you should try and loosely couple independent classes as much as possible. That is one of the purposes of a delegation pattern - Class "A" can inform a delegate that something has happened and provide some data. Class A doesn't need to know (and shouldn't know) anything more about the delegate object, other than it implements the delegate method.
Your current code is tightly coupled; you are updating properties on the delegate object and reloading a collection view.
Your delegation pattern should look something like this:
protocol FriendSelectionDelegate {
func didSelectRecipient(friend: Friend)
}
Then in your pop up view controller all you need is:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let friend = friendsList[indexPath.row]
delegate?.didSelectRecipient(friend: friend)
dismiss(animated: true, completion: nil)
}
And in your ChatVC:
extension ChatVC: FriendSelectionDelegate {
func didSelectRecipient(friend: Friend) {
self.friendProfileImage = friend.profileImage
self.friendName = friend.name
self.friendId = friend.id
self.friendIsOnline = friend.isOnline
self.friendLastLogin = friend.lastLogin
self.forwardedMessage = true
self.messages = []
self.loadNewMessages = false
self.updateView()
}
}
The updateView function performs whatever operations are required to update the views. You can call that same function in your viewDidLoad to set the initial state of the views
As a further improvement, consider whether you need all of those individual properties (friendName, friendId etc). Can you just store a single property, var selectedFriend: Friend or something and access the properties from that Friend. You can then use a didSet on that selectedFriend property to call your updateView function.
You need put the code in a function that you call from viewDidLoad and your delegate
Related
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
}
I am looking for some clarification and hope someone can help me understand.
My app is completely built programmatically and I’m not using storyboards at all, therefore I don’t have any segues.
I’m trying to pass a CoreData object from one VC to another via the didSelectRowAt method of a tableView.
I have the object being returned at the indexPath correctly, but appear to have issues in passing that object across to the new VC.
In this code extract, my only concern at the moment is to get the CoreData object passed so I can then access all of the associated data.
ie: when the place is passed, I would like to print the name, latitude, longitude.
This will allow me to continue on with the app and use that data in the new VC.
Thanks in advance.
// PlacesVC
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let placeOnMap = fetchedResultsController.object(at: indexPath)
let destVC = MapVC()
destVC.placePassed = placeOnMap
self.navigationController?.pushViewController(destVC, animated: true)
print(placeOnMap.placeName!)
}
// MapVC
import UIKit
class MapVC: UIViewController {
var placePassed = PlaceEntity?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
// print(placePassed.placeName!)
// print(placePassed.latitude!)
// print(placePassed.longitude!)
}
}
Updated Information
In the PlacesVC(), the following is occurring.
If I option click on the let placeOnMap, I get…
Declaration: let placeOnMap: PlaceEntity
The line of code… destVC.placePassed = placeOnMap, gives the following error.
Cannot assign value of type 'PlaceEntity' to type 'PlaceEntity?.Type'
On the MapVC()
If I option click on the placePassed var, I get…
Declaration: let placePassed: PlaceEntity?.Type
Also, this line of code… var placePassed = PlaceEntity?, gives the following error.
Expected member name or constructor call after type name
and If I uncomment the print statement on the destVC, this error pops up…
Type 'PlaceEntity?' has no member 'placeName'
I believe my issues are something to do with the way I am setting the var on the receiving VC because it is adding the .Type to the declared var.
This is the part I am not understanding. Thanks
I have a UITableView that gets populated by the following firebase database:
"games" : {
"user1" : {
"game1" : {
"currentGame" : "yes",
"gameType" : "Solo",
"opponent" : "Computer"
}
}
}
I load all the games in viewDidLoad, a user can create a new game in another UIViewController, once a user does that and navigates back to the UITableView I want to update the table with the new row. I am trying to do that with the following code:
var firstTimeLoad : Bool = true
override func viewDidLoad() {
super.viewDidLoad()
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef =
Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
let game = snapshot
self.games.append(game)
self.tableView.reloadData()
})
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if firstTimeLoad {
firstTimeLoad = false
} else {
if let currentUserID = Auth.auth().currentUser?.uid {
let gamesRef = Database.database().reference().child("games").child(currentUserID)
gamesRef.observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.reloadData()
})
}
}
}
Lets say there is one current game in the data base, when viewDidLoad is run the table displays correctly with one row. However anytime I navigate to another view and navigate back, viewDidAppear is run and for some reason a duplicate game seems to be appended to the games even though no child is added.
The cells are being populated by the games array:
internal func tableView(_ tableView: UITableView, cellForRowAt indexPath:
IndexPath) -> UITableViewCell {
let cell = Bundle.main.loadNibNamed("GameTableViewCell", owner:
self, options: nil)?.first as! GameTableViewCell
let game = games[indexPath.row]
if let gameDict = game.value as? NSDictionary {
cell.OpponentName.text = gameDict["opponent"] as? String
}
return cell
}
UPDATE:
Thanks to everyone for their answers! It seems like I misunderstood how firebase .childAdded was functioning and I appreciate all your answers trying to help me I think the easiest thing for my app would be to just pull all the data every time the view appears.
From what I can see, the problem here is that every time you push the view controller and go back to the previous one, it creates a new observer and you end up having many observers running at the same time, which is why your data appears to be duplicated.
What you need to do is inside your viewDidDisappear method, add a removeAllObservers to your gameRef like so :
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
guard let currentUserId = Auth.auth().currentUser?.uid else {
return
}
let gamesRef = Database.database().reference().child("games").child(currentUserId)
gamesRef.removeAllObservers()
}
I cannot see all your code here so I am not sure what is happening, but before adding your child added observer, you need to remove all the elements from your array like so :
games.removeAll()
Actually, as per best practices, you should not call your method inside your ViewDidLoad, but instead you should add your observer inside the viewWillAppear method.
I cannot test your code right now but hopefully it should work like that!
Let me know if it doesn't :)
UPDATE:
If you want to initially load all the data, and then pull only the new fresh data that is coming, you could use a combination of the observeSingleEvent(of: .value) and observe(.childAdded) observers like so :
var didFirstLoad = false
gamesRef.child(currentUserId).observe(.childAdded, with: { (snapshot) in
if didFirstLoad {
// add your object to the games array here
}
}
gamesRef.child(currentUserId).observeSingleEvent(of: .value, with: { (snapshot) in
// add the initial data to your games array here
didFirstLoad = true
}
By doing so, the first time it loads the data, .childAdded will not be called because at that time didFirstLoad will be set to false. It will be called only after .observeSingleEvent got called, which is, by its nature, called only once.
Try following code and no need to check for bool , Avoid using bool here its all async methods , it created me an issue in between of my chat app when its database grows
//Remove ref in didLoad
//Remove datasource and delegate from your storyboard and assign it in code so tableView donates for data till your array don't contain any data
//create a global ref
let gamesRef = Database.database().reference()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.games.removeAllObjects()
if let currentUserID = Auth.auth().currentUser?.uid {
gamesRef.child("games").child(currentUserID)observe(.childAdded, with: { (snapshot) in
self.games.append(snapshot)
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.reloadData()
})
gamesRef.removeAllObserver() //will remove ref in disappear itself
//or you can use this linen DidDisappear as per requirement
}
else{
//Control if data not found
}
}
//TableView Delegate
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if self.games.count == 0{
let emptyLabel = UILabel(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: self.view.bounds.size.height))
emptyLabel.text = "No Data found yet"
emptyLabel.textAlignment = .center
self.tableView.backgroundView = emptyLabel
self.tableView.separatorStyle = .none
return 0
}
else{
return self.games.count
}
}
observe(.childAdded) is called at first once for each existing child, then one time for each child added.
Since i also encounter a similar issue, assuming you don't want to display duplicate objects, in my opinion the best approach, which is still not listed in the answers up above, is to add an unique id to every object in the database, then, for each object retrieved by the observe(.childAdded) method, check if the array which contains all objects already contains one with that same id. If it already exists in the array, no need to append it and reload the TableView. Of course observe(.childAdded) must also be moved from viewDidLoad() to viewWillAppear(), where it belongs, and the observer must be removed in viewDidDisappear. To check if the array already includes that particular object retrieved, after casting snapshot you can use method yourArray.contains(where: {($0.id == retrievedObject.id)}).
I have a problem in which when I select a UITextField, out pops up a view controller where the user can type into a search bar and search for particular items via the Google Places API. It looks something like this:
//When clicking on the UITextField, this function is called
#IBAction func selectLocation(){
gpaViewController.placeDelegate = self
presentViewController(gpaViewController, animated: true, completion: nil)
}
Through a the GooglePlacesAutocomplete github I found online: https://github.com/watsonbox/ios_google_places_autocomplete the below Extension was written for me so that everytime you select an entry from the UITableView from the presented gpaViewController, the place.description is printed along with its details as shown below:
extension EventFormController: GooglePlacesAutocompleteDelegate {
func placeSelected(place: Place){
var test: Place?
println(place.description)
place.getDetails { details in
println(details)
}
}
func placeViewClosed() {
dismissViewControllerAnimated(true, completion: nil)
}
}
However, upon doing so, I want it so that I can return the place.description from the placeSelected function as shown above but I am not too sure on how to do so. I tried to edit the placeSelected function inside the library but I don't think you can return anything from the didSelectRowAtIndexPath UITableView function.
public func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
delegate?.placeSelected?(self.places[indexPath.row])
}
Any ideas on how to return the place.description and set that place.description as the text inside the UITextField after selecting an option? Any help would be appreciated!
func placeSelected(place: Place) is an protocol method, you can't change the signature by adding a return value without breaking the callback chain, i.e, if you change the signature of the protocol method, it won't be called. Instead of returing place.description from the function, you're supposed to use it for updating your UI, for example:
func placeSelected(place: Place){
var test: Place?
println(place.description)
// update UI
textField.text = place.description // assuming that your EventFormController has reference to the textField you're trying to access
place.getDetails { details in
println(details)
}
}
It would be better though if you can explain more concretely what you're trying to achieve. I still don't understand the connection between your placeSelected and the didSelectRowAtIndexPath implementations.
How about something like:
place.getDetails { details in
self.textField.text = details.name
}
I didn't read through the documentation but I believe getDetails is a function something like getDetail(completionHandler: (Details) -> Void), which completionHandler is a closure executed after details are fetched.
So I guess what you want to do after fetching the data is to let your textField in your view controller to display the information fetched.
I have been pulling my hair out trying to get this 'Delegate' thing to work in Swift for an App I am working on.
I have two files: CreateEvent.swift and ContactSelection.swift, where the former calls the latter.
CreateEvent's contents are:
class CreateEventViewController: UIViewController, ContactSelectionDelegate {
/...
var contactSelection: ContactSelectionViewController = ContactSelectionViewController()
override func viewDidLoad() {
super.viewDidLoad()
/...
contactSelection.delegate = self
}
func updateInvitedUsers() {
println("this finally worked")
}
func inviteButton(sender: AnyObject){
invitedLabel.text = "Invite"
invitedLabel.hidden = false
toContactSelection()
}
/...
func toContactSelection() {
let contactSelection = self.storyboard?.instantiateViewControllerWithIdentifier("ContactSelectionViewController") as ContactSelectionViewController
contactSelection.delegate = self
self.navigationController?.pushViewController(contactSelection, animated: true)
}
ContactSelection's contents are:
protocol ContactSelectionDelegate {
func updateInvitedUsers()
}
class ContactSelectionViewController: UITableViewController {
var delegate: ContactSelectionDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate?.updateInvitedUsers()
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Stuff
self.delegate?.updateInvitedUsers()
}
}
What am I doing wrong? I am still new and don't fully understand this subject but after scouring the Internet I can't seem to find an answer. I use the Back button available in the Navigation Bar to return to my CreateEvent view.
var contactSelection: ContactSelectionViewController = ContactSelectionViewController()
This is instantiating a view controller directly, and the value never gets used. Since it looks like you're using storyboards, this isn't a good idea since none of the outlets will be connected and you'll get optional unwrapping crashes. You set the delegate of this view controller but that's irrelevant as it doesn't get used.
It also isn't a good idea because if you do multiple pushes you'll be reusing the same view controller and this will eventually lead to bugs as you'll have leftover state from previous uses which might give you unexpected outcomes. It's better to create a new view controller to push each time.
In your code you're making a brand new contactSelection from the storyboard and pushing it without setting the delegate.
You need to set the delegate on the instance that you're pushing onto the navigation stack.
It's also helpful to pass back a reference in the delegate method which can be used to extract values, rather than relying on a separate reference in the var like you're doing.
So, I'd do the following:
Remove the var contactSelection
Add the delegate before pushing the new contactSelection object
Change the delegate method signature to this:
protocol ContactSelectionDelegate {
func updateInvitedUsers(contactSelection:ContactSelectionViewController)
}
Change your delegate calls to this:
self.delegate?.updateInvitedUsers(self)