I'm trying to wrap my head around what could be causing an error I'm seeing. Here's how the general flow looks: when we first hit this feature, we set our tableHeaderView to a custom segmentedcontrol view, then make a service call to fetch some data, when that is complete, we reload the table. This data is all cached, so when the user leaves and comes back, no service call is made, is simply returns back the cached data. However, when coming back, it doesn't seem to recognize that a header view is on the table, and instead places the tableView at the same origin as its header view, so the first cell is covered by the headerView. Here's some relevant methods:
override func viewDidLoad() {
super.viewDidLoad()
self.useAdaptableFeedbackFooter = true
self.tableView.registerReusableCell(DCManagePaymentsTransactionCell.self)
self.tableView.tableHeaderView = self.segmentedControlView
self.segmentedControlView.selectedSegmentIndex = self.selectedSegment.rawValue
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// TODO: Remove once we have other segments implemented
makeServiceCall()
}
private lazy var segmentedControlView: DMSegmentedControlTableHeaderView = {
let view = DMSegmentedControlTableHeaderView.loadableViewFromNib()
let segmentTitles = [DCManagePaymentsString.Summary.localized, DCManagePaymentsString.History.localized, DCManagePaymentsString.Pending.localized]
view.segmentTitles = segmentTitles
view.delegate = self
return view
}()
private func makeServiceCall() {
self.businessService.getData {
(result: DMServiceResult<DCPendingPayments>) -> Void in
switch result {
case .Success (let data):
if data.isHAMode {
self.data.payment = data.pendingPayments.first
self.tableView.dataSource = self.HADataSource
self.tableView.delegate = self.HADataSource
} else {
self.data.pendingPayments = data.pendingPayments
self.tableView.dataSource = self.dataSource
self.tableView.delegate = self.dataSource
}
self.tableView.reloadData()
case .Failure(let errorInfo):
self.navigationController?.handleError(errorInfo, withCompletion: nil)
}
}
}
The only difference I can see is how quickly we eventually call reloadData(). When I set an explicit reloadData() call in viewDidAppear it sets the tableView up correctly, but there's a jerky animation to that.
It seems that I may be setting my table up too fast for it to recognize that there's a header view? Although it doesn't make sense that that's what's causing this. Any ideas or suggestions? I can post more code if necessary.
Edit: I've added two screenshots, first is what the header looks like on first load, second is after you return and it just gets cached data (the Pending Payments is my first cell, and it gets completely covered by the header segmented control in the cached data load):
Related
When the callback for the TaskListDataSource gets called it reloads both the todayVC and the reviewVC because they are UITableViewControllers. However the plannerVC is not and the tableview property is an outlet.
#IBOutlet weak var tableView: UITableView!
Why is it that when the callback runs it crashes saying it is nil. If I am somehow able to scroll across in the page view however and and view the plannerVC it will never crash as the tableview has been loaded into memory. But why doesn't it do it initially?
override func viewDidLoad() {
super.viewDidLoad()
let taskListDataSource = TaskListDataSource {
self.todayVC.tableView.reloadData()
self.plannerVC.tableView.reloadData()
self.reviewVC.tableView.reloadData()
}
todayVC = storyboard!.instantiateViewController(identifier: "TodayViewController", creator: { coder in
return TodayViewController(coder: coder, taskListDataSource: taskListDataSource)
})
plannerVC = storyboard!.instantiateViewController(identifier: "PlannerViewController", creator: { coder in
return PlannerViewController(coder: coder, taskListDataSource: taskListDataSource)
})
reviewVC = storyboard!.instantiateViewController(identifier: "ReviewViewController", creator: { coder in
return ReviewViewController(coder: coder, taskListDataSource: taskListDataSource)
})
addVC = storyboard!.instantiateViewController(identifier: "AddViewController")
setViewControllers([todayVC], direction: .forward, animated: false)
dataSource = self
print(plannerVC.tableView) // Console is printing nil
}
When you call instantiateInitialViewController(creator:), the UIViewController is initiated, but its view (and all subviews, including then all the IBOutlet) aren't loaded in memory.
So when, you try to do self.someIBoutlet (in your case self.plannerVC.tableView.reloadData(), it crashes.
A solution, would be to force the view to load, with loadViewIfNeeded().
Since loading the view can be heavy, it's usually used when the ViewController will be shown shortly after (for instance, in a didSet of some property that access outlet in it, because it will be shown on screen in a few instants, so the view will be loaded anyway, just a few moment after).
Since you are loading 3 UIViewController, could it be that you aren't showing them, but prematurely loading them?
If that's the case, you might rethink your app architecture (all your UIViewController don't need to be initialized and in memory, and less to have their view loaded).
Still, you can check beforehand if the view has been loaded, and that you can access the outlets with isViewLoaded.
I'dd add for that a method in PlannerVC:
func refreshData() {
guard isViewLoaded else { return }
tableView.reloadData()
}
Side note, it could be a protocol (and even more, complexe, like adding var tableView { get set }, and have a default implementation of refreshData(), but that's going further, not necessary)...
protocol Refreshable {
func refreshData()
}
let taskListDataSource = TaskListDataSource {
self.todayVC.refreshData()
self.plannerVC.refreshData()
self.reviewVC.refreshData()
}
Side note, I would check if there isn't memory retain cycles, I would have use a [weak self] in the closure of TaskListDataSource, and also would have made it a property of the VC.
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'm trying to build a weather app wherein I fetch the weather data using Alamofire. The fetching of data is working fine, however the UILabel which is used to display the fetched values displays the data only for a second after which it disappears. The labels are not part of the tableview, they sit above in UIView of the top part of the screen. The updateMainUI() is the method where I assign labels to the corresponding values to be displayed on screen.
I have gone through solutions and they suggest using Dispatch async, however Im unable to solve this seemingly simple issue.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tableView.dataSource = self
tableView.delegate = self
currentWeather = CurrentWeather()
DispatchQueue.main.async {
self.currentWeather.downloadWeatherDetails {
self.updateMainUI()
}
}
}
The updateMainUI() Method is as follows:
func updateMainUI() {
dateLabel.text = currentWeather.date
print(currentWeather.date)
currentTempLabel.text = "\(currentWeather.currentTemp)"
currentWeatherTypeLabel.text = currentWeather.weatherType
locationLabel.text = currentWeather.cityName
currentWeatherImage.image = UIImage(named: currentWeather.weatherType)}
Instead of this
DispatchQueue.main.async{
self.currentWeather.downloadWeatherDetails {
self.updateMainUI()
}
}
try this
currentWeather.downloadWeatherDetails{
DispatchQueue.main.async{
self.updateMainUI()
}
}
I wonder how I can show a VC after the remote data has loaded. I am not using a tableView but a normal VC.
My code look like this:
viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
loadItemData(id)
}
func loadItemData(aId: Int) {
Service.getItem(aId) { (JSON) -> () in
self.iData = JSON
self.configureData(self.iData)
}
}
func configureData(iData: JSON) {
if let type = iData["item_type"].int {
if let == 1 {
someButton.hidden = true
}
}
if let title = iData["item_title"].string {
titleLabel.text = title
}
}
What happens is that my VC first loads with the button visible and with my text label containing "dummy text" from storyboard, then when the data has loaded the button will hide and the text label will change.
So my question now is how I can hide my VC or show some loading indicator until the data has loaded.
Also worth saying this is the 2nd view. My apps start with a tableView and when you click on a cell you end up in this VC. So I could also load the data when the cell gets clicked then pass it to this VC.
Using the activity indicator is better, here's how to do it
First make sure you add Activity Indicator in your VC
override func viewDidLoad() {
super.viewDidLoad()
self.myActivityIndicator.startAnimating()
loadItemData(id)
}
func loadItemData(aId: Int) {
Service.getItem(aId) { (JSON) -> () in
self.iData = JSON
self.configureData(self.iData)
self.myActivityIndicator.stopAnimating()
}
}
Rather than try to hide the VC (try to load the data and pass it), it will makes the apps feel not responsive, because you have to wait the data loaded and then the VC will show.
I am working with swift in Xcode 7. I am totally new to Swift, Xcode, and Firebase. I would like to have three UITableViewControllers in my iOS app. The first two TableView controllers will need dynamic content and the third TableView controller will need static content. I would like for the second and third TableView controllers to display data based on what is pressed on the previous TableView controller. All of the data will come from my Firebase. I have no idea where to start. Please point me in the right direction! Thank you!
This question is broad, in that it asks how to do three different tasks.
I think you'll be better off getting answers if you only ask for one thing at a time.
I can help you with populating a UITableViewController with Firebase.
class MyTableViewController: UITableViewController {
// your firebase reference as a property
var ref: Firebase!
// your data source, you can replace this with your own model if you wish
var items = [FDataSnapshot]()
override func viewDidLoad() {
super.viewDidLoad()
// initialize the ref in viewDidLoad
ref = Firebase(url: "<my-firebase-app>/items")
}
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// listen for update with the .Value event
ref.observeEventType(.Value) { (snapshot: FDataSnapshot!) in
var newItems = [FDataSnapshot]()
// loop through the children and append them to the new array
for item in snapshot.children {
newItems.append(item as! FDataSnapshot)
}
// replace the old array
self.items = newItems
// reload the UITableView
self.tableView.reloadData()
}
}
}
This technique uses the .Value event, you can also use .ChildAdded, but then you have to keep track of .ChildChanged, '.ChildRemoved', and .ChildMoved, which can get pretty complicated.
The FirebaseUI library for iOS handles this pretty easily.
dataSource = FirebaseTableViewDataSource(ref: self.firebaseRef, cellReuseIdentifier: "<YOUR-REUSE-IDENTIFIER>", view: self.tableView)
dataSource.populateCellWithBlock { (cell: UITableViewCell, obj: NSObject) -> Void in
let snap = obj as! FDataSnapshot
// Populate cell as you see fit, like as below
cell.textLabel?.text = snap.key as String
}
I do it slightly different when I have a UITableViewController, especially for those that can push to another detail view / or show a modal view over the top.
Having the setObserver in ViewDidAppear works well. However, I didnt like the fact that when I looked into a cells detail view and subsequently popped that view, I was fetching from Firebase and reloading the table again, despite the possibility of no changes being made.
This way the observer is added in viewDidLoad, and is only removed when itself is popped from the Nav Controller stack. The tableview is not reloaded unnecessarily when the viewAppears.
var myRef:FIRDatabaseReference = "" // your reference
override func viewDidLoad() {
super.viewDidLoad()
setObserver()
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
// only called when popped from the Nav Controller stack
// if I push to another detail view my observer will remain active
if isBeingDismissed() || isMovingFromParentViewController() {
myRef.removeAllObservers()
}
}
func setObserver() {
myRef.observeEventType(.Value, withBlock: { snapshot in
var newThings = [MyThing]()
for s in snapshot.children {
let ss = s as! FIRDataSnapshot
let new = MyThing(snap: ss) // convert my snapshot into my type
newThings.append(new)
}
self.things = newThings.sort{ $0.name < $1.name) } // some sort
self.tableView.reloadData()
})
}
I also use .ChildChanged .ChildDeleted and .ChildAdded in UITableViews. They work really well and allow you use UITableView animations. Its a little more code, but nothing too difficult.
You can use .ChildChanged to get the initial data one item at a time, then it will monitor for changes after that.
If you want all your data at once in the initial load you will need .Value, I suggest you use observeSingleEventOfType for your first load of the tableview. Just note that if you also have .ChildAdded observer you will also get an initial set of data when that observer is added - so you need to deal with those items (i.e. don't add them to your data set) otherwise your items will appear twice on the initial load.