I found the pattern used by the Google places IOS SDK to be clean and well designed. Basically they follow what is presented on the following apple presentation: Advanced User interface with Collection view (It start slide 46).
This is what is implemented in their GMSAutocompleteTableDataSource.
It us up to the datasource to define the state of the tableview.
We link the tableview.
var googlePlacesDataSource = GMSAutocompleteTableDataSource()
tableView.dataSource = googlePlacesDataSource
tableView.delegate = googlePlacesDataSource
Then every time something change the event is binded to the datasource:
googlePlacesDataSource.sourceTextHasChanged("Newsearch")
The data source perform the query set the table view as loading and then display the result.
I would like to achieve this from my custom Source:
class JourneyTableViewDataSource:NSObject, UITableViewDataSource, UITableViewDelegate{
private var journeys:[JourneyHead]?{
didSet{
-> I want to trigger tableView.reloadData() when this list is populated...
-> How do I do that? I do not have a reference to tableView?
}
}
override init(){
super.init()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
...
}
Any idea?
to update dynamic tableview, you have to add reloadData, reloadData updates datasource.
self.tableview.reloadData()
Your data source shouldn't know anything about your table view. This is the basic of this patter and it gives you a lot of power in terms of reusability.
For your purpose the best solution is to use the property closure:
var onJourneysUpdate: (() -> Void)?
private var journeys: [JourneyHead]? {
didSet {
onJourneysUpdate?()
}
}
And in the point where you set your data source you can set whatever action you would like to execute after updating the datasource's array:
var googlePlacesDataSource = JourneyTableViewDataSource()
tableView.dataSource = googlePlacesDataSource
tableView.delegate = googlePlacesDataSource
googlePlacesDataSource.onJourneysUpdate = { [unowned self] in
self.tableView.reloadData()
}
Related
When a user swipe a cell, it becomes possible to delete it. However, the deletion occurs without animation.
Part of code in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
guard let foodCategoryDetailViewModel = foodCategoryDetailViewModel else { return }
tableView.delegate = nil
tableView.dataSource = nil
foodCategoryDetailViewModel.foodsInSelectedCategory
.bind(to: tableView.rx.items(cellIdentifier: FoodCategoryDetailTableViewCell.cellIdentifier, cellType: FoodCategoryDetailTableViewCell.self))
{ row, food, cell in
cell.foodCategoryDetailCellViewModel = foodCategoryDetailViewModel.cellViewModel(forRow: row)
}.disposed(by: disposeBag)
tableView.rx.itemDeleted.subscribe(onNext: { indexPath in
foodCategoryDetailViewModel.removeFoodFromApplication(atRow: indexPath.row)
}).disposed(by: disposeBag)
}
Part of code in my ViewModel:
class FoodCategoryDetailTableViewViewModel: FoodCategoryDetailTableViewViewModelType {
var foodsInSelectedCategory: BehaviorRelay<[Food]>
func removeFoodFromApplication(atRow row: Int) {
if let food = getFood(atRow: row) {
foodsInSelectedCategory.remove(at: row)
//remove from core data
CoreDataHelper.sharedInstance.removeFoodFromApplication(foodName: food.name!)
}
}
How to animating deleting process from tableView?
In order to animate the deleting process, you need a datasource that is designed to do that. The default datasource in RxSwift is not.
The most popular library for such a thing is RxDataSources which brings in a full blown multi-section system for animating table and collection views, but if you don't want something that elaborate, you can easily write your own.
Here is an example of a simple animatable datasource for RxSwift that uses DifferenceKit to calculate which cells must be animated on/off: (https://github.com/danielt1263/RxMultiCounter/blob/master/RxMultiCounter/RxExtensions/RxSimpleAnimatableDataSource.swift)
I'm using the latest version of Moya with RxSwift and I've encountered a logical issue for which I can't find a solution, at the moment.
Let's say that I have a UITableViewController with a ViewModel that implements the following interface:
protocol ListViewModelType {
var items: Observable<[Item]> { get }
}
The items property is implemented as (using EVReflection):
var items: Observable<[Therapy]> {
get {
let provider = RxMoyaProvider<ItemService>()
return provider
.request(.all)
.map(toArray: Item.self)
}
}
In the viewDidLoad method of the UITableViewController, I have set up a binding between the items property and the tableView via the following code:
self.viewModel.items
.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: cellType)) { row, element, cell in
// cell configuration code
}
.disposed(by: self.disposeBag)
Now, I would like to refresh the content of the UITableView to reflect changes that the user has done through other parts of the app. Considering that the RxMoyaProvider returns an Observable, this should be easily done through another value emitted by the Observable, but I don't know how to communicate to the provider that it should refresh the content from the server and put it into the same Observable.
Am I missing something here? Is there a more recommended way to bind a UITableView to a list of objects coming from a RxMoyaProvider?
You have to reload our Moya request. this is a bit hacky but i guess you get the hint.
let didAppear = Variable(true)
override func viewDidAppear(_ animated: Bool) {
didAppear.value = true
}
override func viewDidLoad(){
self.didAppear.flatMap{ _ in self.viewModel.items}
.bind(to: tableView.rx.items(cellIdentifier: cellIdentifier, cellType: cellType)) { row, element, cell in
// cell configuration code
}
.disposed(by: self.disposeBag)
}
alternatively you could redesign our architecture with a offline-first principle
Quick question, I am using a UISearchController its working perfectly.
But I was wondering if it was possible to show a new view when I select the search bar?
Because when I am searching I do not want to see my tableView/background.
What you are referring to is the presentation context of the UISearchController.
Here is a link to Apple's documentation on definesPresentationContext and the relevant piece of information we care about
this property controls which existing view controller in your view
controller hierarchy is actually covered by the new content
If you are still working off this example UISearchController from before, you are already almost done and just need to look at the following line of code inside of viewDidLoad():
self.definesPresentationContext = true
The default value for this is false. Since it's set to true, we are telling the UITableViewController that it will be covered when the view controller or one of its descendants presents a view controller. In our case, we are covering the UITableViewController with the UISearchController.
To address your question, hiding the tableView/background is as simple as clearing or switching the table's data source when the search bar is active. This is handled in the following bit of code.
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (self.userSearchController.active) {
return self.searchUsers.count
} else {
// return normal data source count
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("userCell") as! UserCell
if (self.userSearchController.active && self.searchUsers.count > indexPath.row) {
// bind data to the search data source
} else {
// bind data to the normal data source
}
return cell
}
When the search bar is dismissed, we want to reload the normal data source which is done with the following:
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
// Clear any search criteria
searchBar.text = ""
// Force reload of table data from normal data source
}
Here's a link to a great article on UISearchControllers and also gives a brief overview of their inner workings and view hierarchy.
For future posts on SO, you should always try to include the relevant code samples so people are able to give the best feedback possible :)
EDIT
I think I misinterpreted your question a bit but the above is still relevant towards the answer. To display a special view when the search results are empty or nothing is typed in, do the following:
1) Add a new UIView as a child of the TableView of your UITableViewController in the storyboard with the desired labels/images. This will be next to any prototype cells you may have.
2) Create and wire up the outlets in your UITableViewController
#IBOutlet var emptyView: UIView!
#IBOutlet weak var emptyViewLabel: UILabel!
3) Hide the view initially in viewDidLoad()
self.emptyView?.hidden = true
4) Create a helper function to update the view
func updateEmptyView() {
if (self.userSearchController.active) {
self.emptyViewLabel.text = "Empty search data source text"
self.emptyView?.hidden = (self.searchUsers.count > 0)
} else {
// Keep the emptyView hidden or update it to use along with the normal data source
//self.emptyViewLabel.text = "Empty normal data source text"
//self.emptyView?.hidden = (self.normalDataSource.count > 0)
}
}
5) Call the updateEmptyView() after you've finished querying
func loadSearchUsers(searchString: String) {
var query = PFUser.query()
// Filter by search string
query.whereKey("username", containsString: searchString)
self.searchActive = true
query.findObjectsInBackgroundWithBlock { (objects: [AnyObject]?, error: NSError?) -> Void in
if (error == nil) {
self.searchUsers.removeAll(keepCapacity: false)
self.searchUsers += objects as! [PFUser]
self.tableView.reloadData()
self.updateEmptyView()
} else {
// Log details of the failure
println("search query error: \(error) \(error!.userInfo!)")
}
self.searchActive = false
}
}
Hope that helps!
I want to update the label in the DetailViewController everytime I selected a tableRow in the MasterViewController. To achieve this, I designed a delegate, which I have in the MasterVC
protocol TestTableViewControllerDelegate {
func selectedRow(selectedCar : Car)
}
class TestTableViewController: UITableViewController {
...
var delegate : TestTableViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = DetailViewController()
The delegate works just fine, (it is implemented correctly in the DetailVC), it can pass values from TestTableVC to DetailVC and also correctly do println(), which prints a new Car.model String to the console every time I select a row in the TTVC.
The DetailVC looks like this (shortened):
class DetailViewController: UIViewController, TestTableViewControllerDelegate {
#IBOutlet var textLabel: UILabel!
var theCar : Car? {
didSet(newCar) {
refreshUI()
}
}
override func viewDidLoad() {
super.viewDidLoad()
refreshUI()
}
func selectedRow(selectedCar : Car) {
theCar = selectedCar
refreshUI()
}
func refreshUI() {
textLabel?.text = theCar!.model
}
}
I can achieve any kind of action with my delegate, expect for refreshing the UI. I have tried numerous ways, this is my latest attempt. Before that, I tried setting the textLabel's text property directly within the delegate method, didn't work. This problem only occurs when working with the UI-elements. I know it has something to do with the view not being loaded yet, but why does my refreshUI() function not work at all?
I am still a beginner, so any tip or help would be much appreciated!
A workaround I've used is to cerate a properly in the delegate and pass the value to it instead of the UI element. When the view loads I update the label's text properly with the value of the delegates property. I would think there's a better way to do this (I'm new to programming) but this is the best soultion I've come up with so far. Will update with sample code soon.
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)