IGListSectionController's didUpdate and cellForItem always re-called, even though isEqual == true - swift

Trying to implement the IGListKit library, I'm running into the issue that my cells are updated unnecessarily. I'm using a singleton adapter.dataSource with one section per row in the table.
Minimum example:
import IGListKit
class ContentItem: ListDiffable {
weak var item: Content?
weak var section: ContentSectionController?
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
return true
}
init(item: Content?) {
self.item = item
}
}
class ContentSectionController: ListSectionController {
weak var object: ContentItem?
override func didUpdate(to object: Any) {
self.object = object as? ContentItem
self.object?.section = self
// should only be called on updates
}
override func sizeForItem(at index: Int) -> CGSize {
guard let content = object?.item else {
return CGSize(width: 0, height: 0)
}
// calculate height
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
let cell = collectionContext!.dequeueReusableCellFromStoryboard(withIdentifier: "ContentCell", for: self, at: index)
(cell as? ContentCell)?.item = object // didSet will update cell
return cell
}
override init() {
super.init()
self.workingRangeDelegate = self
}
}
extension ContentSectionController: ListWorkingRangeDelegate {
func listAdapter(_ listAdapter: ListAdapter, sectionControllerWillEnterWorkingRange sectionController: ListSectionController) {
// prepare
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerDidExitWorkingRange sectionController: ListSectionController) {
return
}
}
class ContentDataSource: NSObject {
static let sharedInstance = ContentDataSource()
var items: [ContentItem] {
return Content.displayItems.map { ContentItem(item: $0) }
}
}
extension ContentDataSource: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
return items
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return ContentSectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
}
/// VC ///
class ContentViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let updater = ListAdapterUpdater()
adapter = ListAdapter(updater: updater, viewController: self, workingRangeSize: 2)
adapter.collectionView = collectionView
adapter.dataSource = ContentDataSource.sharedInstance
}
var adapter: ListAdapter!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
adapter.performUpdates(animated: true)
}
// ...
}
On every view appear I call adapter.performUpdates(animated: true), which should never update the cells since isEqual is overridden with true. Nonetheless, all cells' didUpdate is triggered, calling cellForItem again too.

IGListKit requires both diffIdentifier and isEqual to be implemented with the IGListDiffable protocol in order to compare the identity/equality of two objects. (You're missing the diff identifier in your model).
My understanding is that under the hood, ListKit checks to see if the two diff identifiers of the objects are equal, if they are THEN it moves on to comparing them with isEqual.
Resources:
IGListKit Best Practices
IGListDiffable Protocol Reference

Related

Swift convert Design Pattern Mvc to Mvvm how can I transfer code block?

I am at the stage of learning new swift and I designed my application as mvc design pattern. I went on an adventure to learn mvvm :D.
There are parts that I still don't understand. I learned that I need to transfer without using UIKit in the ViewModel part, but I couldn't figure out how to transfer it. I have to find the way to it. I have 10 Viewcontroller pages and I want to make them all according to mvvm.
I'm trying to convert my design from MVC to MVVM but i am getting this error how can i solve it?
BreedsViewController
import UIKit
import ProgressHUD
protocol BreedsViewControllerInterface: AnyObject {
func prepareCollectionView()
}
final class BreedsViewController: UIViewController {
#IBOutlet weak var categoryCollectionView: UICollectionView!
// main storyboard collection View adding (dataSource, delegate)
#IBOutlet weak var popularCollectionView: UICollectionView!
// main storyboard collection View adding (dataSource, delegate)
#IBOutlet weak var specialsCollectionView: UICollectionView!
// main storyboard collection View adding (dataSource, delegate)
private lazy var viewModel = BreedsVM()
// data, move mvvm
var categories: [DogCategory] = []
var populars: [Breed] = []
var downCategories:[Breed] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.view = self
viewModel.viewDidLoad()
}
private func registerCell() {
categoryCollectionView.register(UINib(nibName: CategoryCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: CategoryCollectionViewCell.identifier)
popularCollectionView.register(UINib(nibName: DogPortraitCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogPortraitCollectionViewCell.identifier)
specialsCollectionView.register(UINib(nibName: DogLandscapeCollectionViewCell.identifier, bundle: nil), forCellWithReuseIdentifier: DogLandscapeCollectionViewCell.identifier)
}
}
extension BreedsViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch collectionView {
case categoryCollectionView:
return categories.count
case popularCollectionView:
return populars.count
case specialsCollectionView:
return downCategories.count
default: return 0
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch collectionView {
case categoryCollectionView:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CategoryCollectionViewCell.identifier, for: indexPath) as! CategoryCollectionViewCell
cell.setup(category: categories[indexPath.row])
return cell
case popularCollectionView:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogPortraitCollectionViewCell.identifier, for: indexPath) as! DogPortraitCollectionViewCell
cell.setup(breed: populars[indexPath.row])
return cell
case specialsCollectionView:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DogLandscapeCollectionViewCell.identifier, for: indexPath) as! DogLandscapeCollectionViewCell
cell.setup(breed: downCategories[indexPath.row])
return cell
default: return UICollectionViewCell()
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == categoryCollectionView {
let controller = ListDogsViewController.instantiate()
controller.category = categories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
} else {
let controller = FavoriteDetailViewController.instantiate()
controller.breed = collectionView == popularCollectionView ? populars[indexPath.row] : downCategories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
}
}
}
extension BreedsViewController: BreedsViewControllerInterface {
func prepareCollectionView() {
registerCell()
ProgressHUD.show()
NetworkService.shared.fetchAllCategories { [weak self] (result) in
switch result {
case.success(let allBreed):
ProgressHUD.dismiss()
self?.categories = allBreed.categories ?? []
self?.populars = allBreed.populars ?? []
self?.downCategories = allBreed.downCategories ?? []
self?.categoryCollectionView.reloadData()
self?.popularCollectionView.reloadData()
self?.specialsCollectionView.reloadData()
case.failure(let error):
ProgressHUD.showError(error.localizedDescription)
}
}
}
}
BreedsVM
import Foundation
protocol BreedsVMInterface {
var view: BreedsViewControllerInterface? { get set }
func viewDidLoad()
func didSelectItemAt(indexPath: IndexPath)
}
final class BreedsVM {
weak var view: BreedsViewControllerInterface?
}
extension BreedsVM: BreedsVMInterface {
func didSelectItemAt(indexPath: IndexPath) {
}
func viewDidLoad() {
view?.prepareCollectionView()
}
}
For example, I want to apply didselectItemAt according to Mvvm. When I want to do this, I get the following error. How can I solve it?
Changed BreedsViewController
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
viewModel.didSelectItemAt(indexPath: indexPath)
}
Changed BreedsVM
import Foundation
protocol BreedsVMInterface {
var view: BreedsViewControllerInterface? { get set }
func viewDidLoad()
func didSelectItemAt(indexPath: IndexPath)
}
final class BreedsVM {
weak var view: BreedsViewControllerInterface?
var categories: [DogCategory] = []
var populars: [Breed] = []
var downCategories:[Breed] = []
}
extension BreedsVM: BreedsVMInterface {
func didSelectItemAt(indexPath: IndexPath) {
if collectionView == categoryCollectionView {
let controller = ListDogsViewController.instantiate()
controller.category = categories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
} else {
let controller = FavoriteDetailViewController.instantiate()
controller.breed = collectionView == popularCollectionView ? populars[indexPath.row] : downCategories[indexPath.row]
navigationController?.pushViewController(controller, animated: true)
}
}
func viewDidLoad() {
view?.prepareCollectionView()
}
}
BreedsVM's warnings and errors
Cannot find 'categoryCollectionView' in scope Cannot find 'collectionView' in scope Cannot find 'popularCollectionView' in scope
When we move from MVC to any other architecture, we do so to achieve the separation of business logic and UI Logic so for example in MVVM, the ViewModel shouldn't know anything about the UI and also the ViewController should be dumb just makes UI stuff ( changing color, show and hide UI elements, .. ) and also in MVVM, the connection should be from one side the ViewController, the ViewController should have an instance from the ViewModel but the ViewModel should have any reference from the ViewController, but how we achieve the changing of the UI after processing some logic? by binding, and this can be done through number of ways, for example: Combine or RxSwift or even closures, but for simplicity we can start by making the binding using closures so let's take an example:
// ViewModel
class BreedsViewModel {
// MARK: - Closures
var fetchCategoriesSucceeded: ( (_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed]) -> Void )?
var fetchCategoriesFailed: ( (_ errorMessage: String) -> Void )?
// MARK: - Fetch Categories API
func fetchCategories(){
// Also this should be injected to the ViewModel instead of using it as a singleton, read more about dependency injection
NetworkService.shared.fetchAllCategories { [weak self] (result) in
switch result {
case.success(let allBreed):
self?.fetchCategoriesSucceeded?(allBreed.categories, allBreed.populars, allBreed.downCategories)
case.failure(let error):
self?.fetchCategoriesFailed?(error.localizedDescription)
}
}
}
}
// ViewController
class BreedsViewController: UIViewController {
var viewModel = BreedsViewModel() // This should be injected to the view controller
private var categories: [DogCategory] = []
private var populars: [Breed] = []
private var downCategories:[Breed] = []
override func viewDidLoad() {
super.viewDidLoad()
bindViewModel()
fetchCategories()
}
private func fetchCategories(){
// ProgressHUD.show()
viewModel.fetchCategories()
}
private func bindViewModel() {
viewModel.fetchCategoriesSucceeded = { [weak self] categories, populars, downCategories in
// ProgressHUD.dismiss()
self?.categories = categories
self?.populars = populars
self?.downCategories = downCategories
// collectionView.reloadData()
}
viewModel.fetchCategoriesFailed = { [weak self] errorMessage in
// ProgressHUD.showError(errorMessage)
}
}
}
As you can see now, the ViewModel doesn't know anything about the UI, just getting the data from the API then notify the ViewController through the closure and when the ViewController notified, it should update the UI.
I can see also what you are trying to achive is more related to MVP, there are a Presenter and a ViewController, the Presenter will have a weak reference from the ViewController and update the view controller through a delegate
// Presenter
protocol BreedsPresenterDelegate: AnyObject {
func fetchCategoriesSucceeded(_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed])
func fetchCategoriesFailed(_ errorMessage: String)
}
class BreedsPresenter {
weak var delegate: BreedsPresenterDelegate?
func fetchCategories(){
NetworkService.shared.fetchAllCategories { [weak self] (result) in
switch result {
case.success(let allBreed):
self?.delegate?.fetchCategoriesSucceeded(allBreed.categories, allBreed.populars, allBreed.downCategories)
case.failure(let error):
self?.delegate?.fetchCategoriesFailed(error.localizedDescription)
}
}
}
}
// ViewController
class BreedsViewController: UIViewController {
var presenter = BreedsPresenter() // This should be injected to the view controller
private var categories: [DogCategory] = []
private var populars: [Breed] = []
private var downCategories:[Breed] = []
override func viewDidLoad() {
super.viewDidLoad()
presenter.delegate = self
fetchCategories()
}
private func fetchCategories(){
// ProgressHUD.show()
presenter.fetchCategories()
}
}
extension BreedsViewController: BreedsPresenterDelegate {
func fetchCategoriesSucceeded(_ categories: [DogCategory], _ populars: [Breed], _ downCategories: [Breed]) {
// ProgressHUD.dismiss()
self.categories = categories
self.populars = populars
self.downCategories = downCategories
// collectionView.reloadData()
}
func fetchCategoriesFailed(_ errorMessage: String) {
// ProgressHUD.showError(errorMessage)
}
}
I hope this helps.

Swift async await: how to use with several non async delegates?

I have created this simple example, it is a UITextField with an autocompletion ability, displaying a table view showing asynchronous data that evolves as the user types in the text field.
TextField
import UIKit
protocol TextFieldDelegate {
func autocompletedComponents(
_ textField: TextField,
_ components: #escaping ([String]) -> Void
)
}
class TextField: UITextField {
var components: [String] = []
var tableView = UITableView(frame: .zero)
var autocompletionDelegate: TextFieldDelegate? { didSet { setupUI() } }
// Actions
#objc private func didUpdateText() {
autocompletionDelegate?.autocompletedComponents(self) { [weak self] components in
guard let weakSelf = self else {
return
}
weakSelf.components = components
weakSelf.tableView.reloadData()
weakSelf.updateUI()
}
}
// Event
override func becomeFirstResponder() -> Bool {
tableView.isHidden = false
return super.becomeFirstResponder()
}
override func resignFirstResponder() -> Bool {
tableView.isHidden = true
return super.resignFirstResponder()
}
// Init
override init(frame: CGRect) {
super.init(frame: frame)
internalInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
internalInit()
}
private func internalInit() {
addTarget(
self,
action: #selector(didUpdateText),
for: .editingChanged
)
}
// UI
private func setupUI() {
tableView.dataSource = self
tableView.delegate = self
tableView.showsVerticalScrollIndicator = false
tableView.removeFromSuperview()
superview?.addSubview(tableView)
}
private func updateUI() {
let tableViewHeight = Double(min(5, max(0, components.count))) * 44.0
tableView.frame = CGRect(
origin: CGPoint(
x: frame.origin.x,
y: frame.origin.y + frame.size.height
),
size: CGSize(
width: frame.size.width,
height: tableViewHeight
)
)
}
}
extension TextField: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
cell.textLabel?.text = components[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
44.0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
components.count
}
}
ViewController
import MapKit
import UIKit
class ViewController: UIViewController {
private var completion: (([MKLocalSearchCompletion]) -> Void)?
private var searchCompleter = MKLocalSearchCompleter()
#IBOutlet weak var textField: TextField!
override func viewDidLoad() {
super.viewDidLoad()
searchCompleter.delegate = self
textField.autocompletionDelegate = self
}
}
extension ViewController: TextFieldDelegate {
func autocompletedComponents(
_ textField: TextField,
_ components: #escaping ([String]) -> Void
) {
if completion == nil {
completion = { results in
components(results.map { $0.title })
}
}
searchCompleter.queryFragment = textField.text ?? ""
}
}
extension ViewController: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
completion?(completer.results)
}
}
In this example the view controller uses data from MapKit. Now I would like to get rid of the #escaping blocks and replace them with the new async/await syntax.
I have started rewriting the TextField code:
protocol TextFieldDelegate {
func autocompletedComponents(
_ textField: TextField
) async -> [String]
}
#objc private func didUpdateText() {
Task {
let autocompletedComponents = await autocompletionDelegate?.autocompletedComponents(self) ?? []
components = autocompletedComponents
tableView.reloadData()
updateUI()
}
}
However I am stuck in the ViewController, because I don't know what to do with the completion block I was using until now.
Thank you for your help
Here's an implementation using Combine's PassThroughSubject to send the array from MKLocalSearchCompleterDelegate to your autocompletedComponents function which then returns that array to be used in TextField
In ViewController:
class ViewController: UIViewController {
private var searchCompleter = MKLocalSearchCompleter()
var cancellables = Set<AnyCancellable>()
var publisher = PassthroughSubject<[String], Never>()
#IBOutlet weak var textField: TextField!
override func viewDidLoad() {
super.viewDidLoad()
searchCompleter.delegate = self
textField.autocompletionDelegate = self
}
}
extension ViewController: TextFieldDelegate {
func autocompletedComponents(
_ textField: TextField
) async -> [String] {
searchCompleter.queryFragment = textField.text ?? ""
return await withCheckedContinuation { continuation in
publisher
.sink { array in
continuation.resume(returning: array)
}.store(in: &cancellables)
}
}
}
extension ViewController: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
publisher.send(completer.results.map({$0.title}))
}
}
Or you could use your completion closure it would look like this:
func autocompletedComponents(
_ textField: TextField,
_ components: #escaping ([String]) -> Void
) {
return await withCheckedContinuation { continuation in
if completion == nil {
completion = { results in
continuation.resume(returning: results.map { $0.title })
}
}
searchCompleter.queryFragment = textField.text ?? ""
}
}

not able to load data with ViewModel

the tableView dataSource is properly set up in the IB
the viewController identity is properly set as well in the IB
this is my viewModel
class StatusCodeViewModel {
let apiClient = APIClient.shared
var statusCodes: [StatusCode] = []
let identifier = "statusCodeCell"
init() {}
func loadStatusCodes() {
apiClient.execute(service: .statusCode) { statusCodes in
self.statusCodes = statusCodes
}
}
}
and the viewController in which I want to load data
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var viewModel: StatusCodeViewModel? {
didSet {
if viewModel!.statusCodes.count > 0 {
self.tableView.reloadData()
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel = StatusCodeViewModel()
viewModel!.loadStatusCodes()
}
}
extension ViewController : UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let statusCodes = viewModel!.statusCodes as? [StatusCode] {
return statusCodes.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: viewModel!.identifier)
cell?.textLabel!.text = viewModel!.statusCodes[indexPath.row].title
return cell!
}
}
the data count is 0 and no data is shown in the tableView
You have did set on view model which will occur on initialisation.
You will have to implement some kind of callback when the api returns the call - easiest way would be protocol.
protocol StatusCodeViewModelDelegate {
func callFinished()
}
class StatusCodeViewModel {
let apiClient = APIClient.shared
var statusCodes: [StatusCode] = []
let identifier = "statusCodeCell"
var delegate : StatusCodeViewModelDelegate?
init() {}
func loadStatusCodes() {
apiClient.execute(service: .statusCode) { statusCodes in
self.statusCodes = statusCodes
delegate?.callFinished()
}
}
}
Then in your viewController:
override func viewDidLoad() {
super.viewDidLoad()
viewModel = StatusCodeViewModel()
viewModel.delegate = self
viewModel!.loadStatusCodes()
}
func callFinished() {
self.tableView.reloadData()
}
Don't forget to extend for delegate you just made:
class ViewController: UIViewController, StatusCodeViewModelDelegate {
Or, as #rmaddy suggested, in View model change loadStatusCodes to:
func loadStatusCodes(completion: #escaping () -> Void) {
apiClient.execute(service: .statusCode) { statusCodes in
self.statusCodes = statusCodes
}
}
Then, in the viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
viewModel = StatusCodeViewModel()
viewModel!.loadStatusCodes {
self.tableView.reloadData()
}
}
//This would do !
func loadStatusCodes(completion: #escaping () -> Void) {
apiClient.execute(service: .statusCode) { statusCodes in
self.statusCodes = statusCodes
completion()
}
}
// And in ViewController:
override func viewDidLoad() {
super.viewDidLoad()
viewModel = StatusCodeViewModel()
viewModel?.loadStatusCodes() {
self.tableView.reloadData()
}
}

cellForItem or sizeForItem not called

I am trying out IGListKit for the first time but i seem to have hit a brick wall early on
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
collectionView.backgroundColor = .white
return collectionView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.collectionView)
let updater = ListAdapterUpdater()
let adapter = ListAdapter(updater: updater, viewController: self, workingRangeSize: 0)
adapter.collectionView = self.collectionView
adapter.dataSource = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
And
extension SocialViewController: ListAdapterDataSource {
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
// this can be anything!
return [ "Foo" as ListDiffable, "Bar" as ListDiffable]
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
return TopNewsSectionController()
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return UIImageView(image: UIImage(named: "swords"))
}
}
class TopNewsSectionController: ListSectionController {
override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
return collectionContext!.dequeueReusableCell(of: TopNewsCollectionViewCell.self, for: self, at: index)
}
}
but my neither cellForItem or sizeForItem is being called
what am i doing wrong?
Try to declare adapter as a class property rather than a method property,
let updater = ListAdapterUpdater()
let adapter = ListAdapter(updater: updater, viewController: self, workingRangeSize: 0)
adapter.collectionView = self.collectionView
to
lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0)
}()
Call adapter.performUpdatesAnimated or adapter.reloadDataWithCompletion (for the first time loading) to update
Your adapter object is destroyed when viewDidLoad returns. Try to declare it as a property of your ViewController.

How do I update the picker view in the original viewController after dismiss a popover in swift 3?

I am displaying a popover segue after a button is pressed with the purpose of adding a car brand to my picker view in the original viewController so the user can return and select the new brand just added in that picker view, the new car brand is added to the database sussesfuly but I cannot update the picker view, actually after the popover is dismiss, nothing happened, I already try to add a reloadAllComponents in viewWillAppear, viewDidLoad, viewDidAppear but nothing happened, some one can help me please?
Kindly look at the images below:
Popover:
Button segue:
my code:
OriginalViewController
import UIKit
import CoreData
class VehicleAddViewController: UIViewController, UITextViewDelegate, UITextFieldDelegate, UIPopoverPresentationControllerDelegate, UIPickerViewDataSource, UIPickerViewDelegate {
// MARK: - Model
var managedObjectContext: NSManagedObjectContext? = (UIApplication.shared.delegate as? AppDelegate)?.managedObjectContext
// MARK: - Properties
private final var pickerBrandData: [String]?
private final var pickerBrandResult: String? = nil
override func viewDidLoad() {
super.viewDidLoad()
self.Brand.delegate = self
self.Brand.dataSource = self
}
// MARK: - Outlets
#IBOutlet weak var Brand: UIPickerView
//MARK: Data Sources
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
switch pickerView.tag {
case 1:
if pickerBrandData == nil {
return 0
} else {
return pickerBrandData!.count
}
default:
return 0
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch pickerView.tag {
case 1:
return pickerBrandData?[row]
default:
return nil
}
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
switch pickerView.tag {
case 1:
pickerBrandResult = pickerBrandData?[row]
default:
break
}
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destinationViewController.
switch segue.identifier! {
case "Quick Brand Add Segue":
let DestViewController = segue.destination as? QuickBrandAddViewController
let ReceiveViewController = DestViewController?.contentViewController
if let PopOverD = ReceiveViewController?.popoverPresentationController {
let minimunSize = ReceiveViewController?.view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
ReceiveViewController?.preferredContentSize = CGSize(width: (minimunSize?.width)!, height: (minimunSize?.height)!)
PopOverD.delegate = self
DestViewController?.newSetOfBrands = pickerBrandData
}
break
default:
break
}
// Pass the selected object to the new view controller.
}
}
extension UIViewController {
var contentViewController: UIViewController {
if let navcon = self as? UINavigationController {
return navcon.visibleViewController!
} else {
return self
}
}
}
popover viewController
import UIKit
import CoreData
class QuickBrandAddViewController: UIViewController, UITextFieldDelegate {
// MARK: - Model
var managedObjectContext: NSManagedObjectContext? = (UIApplication.shared.delegate as? AppDelegate)?.managedObjectContext
var newSetOfBrands: [String]?
// MARK: - Properties
private final var activeTextField: UITextField?
// MARK: - Lifecycle methods
override func viewDidLoad() {
super.viewDidLoad()
self.Brand.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Outlets
#IBOutlet weak var Brand: UITextField!
// MARK: - Delegate
func textFieldDidBeginEditing(_ textField: UITextField) {
activeTextField = textField
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if activeTextField != nil {
updateDatabase(description: (activeTextField!.text)!)
}
managedObjectContext?.performAndWait {
self.newSetOfBrands = Car_Brand.fetchBrand(inManagedObjectContext: self.managedObjectContext!)!
}
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: {
self.activeTextField?.resignFirstResponder()
self.view.endEditing(true)
}, completion: nil)
presentingViewController?.dismiss(animated: true, completion: nil)
return false
}
// MARK: - Methods
private func updateDatabase(description: String) {
managedObjectContext?.perform {
_ = Car_Brand.insertNew(brandDescription: description, inManagedObjectContext: self.managedObjectContext!)
do {
try self.managedObjectContext?.save()
} catch let error {
print ("Core Data Error: \(error)")
}
}
printDatabesesStatistics()
}
private func printDatabesesStatistics() {
managedObjectContext?.perform() {
do {
let brandCount = try self.managedObjectContext!.count(for: NSFetchRequest(entityName: "Car_Brand"))
print (brandCount)
} catch let error {
print ("Core Data Error: \(error)")
}
}
}
}
CoreDataClass.swift
import Foundation
import CoreData
public class Car_Brand: NSManagedObject {
class func insertNew (brandDescription: String, inManagedObjectContext context: NSManagedObjectContext) -> Car_Brand? {
let request: NSFetchRequest<Car_Brand> = Car_Brand.fetchRequest()
request.predicate = NSPredicate (format: "brand = %#", brandDescription)
if let brand = (try? context.fetch(request))?.first {
return brand
} else if let brand = NSEntityDescription.insertNewObject(forEntityName: "Car_Brand", into: context) as? Car_Brand {
brand.brand = brandDescription
return brand
}
return nil
}
class func fetchBrand (inManagedObjectContext context: NSManagedObjectContext) -> [String]? {
let request: NSFetchRequest<Car_Brand> = Car_Brand.fetchRequest()
request.propertiesToFetch = ["brand"]
request.sortDescriptors = [NSSortDescriptor(key: "brand", ascending: true)]
if let preBrandList = try? context.fetch(request) as [Car_Brand] {
var brandList: [String] = []
for preBrandList in preBrandList {
brandList.append(preBrandList.brand!)
}
return brandList
} else {
return nil
}
}
}
CoreDataProperties.swift
import Foundation
import CoreData
extension Car_Brand {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Car_Brand> {
return NSFetchRequest<Car_Brand>(entityName: "Car_Brand");
}
#NSManaged public var brand: String?
#NSManaged public var id_brand: NSSet?
#NSManaged public var id_model: NSSet?
}
// MARK: Generated accessors for id_brand
extension Car_Brand {
#objc(addId_brandObject:)
#NSManaged public func addToId_brand(_ value: Vehicle)
#objc(removeId_brandObject:)
#NSManaged public func removeFromId_brand(_ value: Vehicle)
#objc(addId_brand:)
#NSManaged public func addToId_brand(_ values: NSSet)
#objc(removeId_brand:)
#NSManaged public func removeFromId_brand(_ values: NSSet)
}
// MARK: Generated accessors for id_model
extension Car_Brand {
#objc(addId_modelObject:)
#NSManaged public func addToId_model(_ value: Model)
#objc(removeId_modelObject:)
#NSManaged public func removeFromId_model(_ value: Model)
#objc(addId_model:)
#NSManaged public func addToId_model(_ values: NSSet)
#objc(removeId_model:)
#NSManaged public func removeFromId_model(_ values: NSSet)
}
After several tests I found that the key is unwind segue, in that way is possible to perform an action after the user come back to the original segue, so I change the strategy of just dismiss for a unwind segue like this:
in the original viewController I add:
// MARK: - Navigation
#IBAction func updateBrandPicker(segue: UIStoryboardSegue) {
_ = navigationController?.popViewController(animated: true)
var rowPosition = 0;
let previousPickerBrandData = pickerBrandData
// Fetch the new list of Brands
managedObjectContext?.perform {
self.pickerBrandData = Car_Brand.fetchBrand(inManagedObjectContext: self.managedObjectContext!)!
self.Brand.reloadAllComponents()
// Obtain new brand
let newSetBrands: Set = Set(self.pickerBrandData!)
var differencePickerBrandData: Set = Set(newSetBrands)
differencePickerBrandData.subtract(previousPickerBrandData!)
let theNewBrand = Array(differencePickerBrandData)
// Select the new brand in the pickerView
if theNewBrand.count > 0 {
for brandName in self.pickerBrandData! {
if brandName == theNewBrand[0] {
self.Brand.selectRow(rowPosition, inComponent: 0, animated: true)
break
}
rowPosition += 1
}
}
}
}
in the popover viewController I add:
override func viewWillDisappear(_ animated: Bool) {
if Brand != nil {
updateDatabase(description: (Brand!.text)!)
}
self.performSegue(withIdentifier: "updateBrandPicker", sender: Any?.self)
}
// MARK: - Actions
#IBAction func Save(_ sender: UIBarButtonItem) {
presentingViewController?.dismiss(animated: true, completion: nil)
}
// MARK: - Delegate
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: {
self.Brand?.resignFirstResponder()
self.view.endEditing(true)
}, completion: nil)
presentingViewController?.dismiss(animated: true, completion: nil)
return false
}
In the stroyboard just add the unwind segue by Ctrl + dragging to Exit button and edditing the identifiere field check the next images:
And the pickerView should update and select the new registry just added