Swift refactor project to MVVM-C - ios

So i am trying to refactor an existing project from MMVM and to add coordinator.
i have the following classes:
protocol Coordinator {
func start()
}
class BaseCoordinator: Coordinator {
private var presenter: UINavigationController
private var genreViewController: ViewController?
private var viewModel = GenreViewModel()
init(presenter: UINavigationController) {
self.presenter = presenter
}
func start() {
let genreViewController = ViewController()
genreViewController.viewModel = viewModel
self.genreViewController = genreViewController
presenter.pushViewController(genreViewController, animated: true)
}
}
class AppCoordinator: Coordinator {
private let window: UIWindow
private let rootViewController: UINavigationController
private var genereListCoordinator: BaseCoordinator?
init(window: UIWindow) {
self.window = window
rootViewController = UINavigationController()
rootViewController.navigationBar.prefersLargeTitles = true
genereListCoordinator = BaseCoordinator(presenter: rootViewController)
}
func start() {
window.rootViewController = rootViewController
genereListCoordinator?.start()
window.makeKeyAndVisible()
}
}
In appDelegate i do as below:
var window: UIWindow?
var applicationCoordinator: AppCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let window = UIWindow(frame: UIScreen.main.bounds)
let appCordinator = AppCoordinator(window: window)
self.window = window
self.applicationCoordinator = appCordinator
appCordinator.start()
return true
}
VC is:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
var viewModel: GenreViewModel!
override func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.itemSize = CGSize(width: self.collectionView.bounds.width, height: 50)
}
self.collectionView.delegate = self
self.collectionView.dataSource = self
collectionView.backgroundView = UIImageView(image: UIImage(named: "131249-dark-grey-low-poly-abstract-background-design-vector.jpg"))
self.viewModel.delegate = self
self.getData()
}
func getData() {
MBProgressHUD.showAdded(to: self.view, animated: true)
viewModel.getGenres()
}
}
extension ViewController: GenreViewModelDelegate {
func didfinish(succsess: Bool) {
MBProgressHUD.hide(for: self.view, animated: true)
if succsess {
self.collectionView.reloadData()
} else {
let action = UIAlertAction(title: "Try again", style: .default, handler: { (action) in
self.getData()
})
Alerts.showAlert(vc: self, action: action)
}
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 100, height: 100)
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: GenreCollectionViewCell.reuseIdentifier, for: indexPath) as? GenreCollectionViewCell else {
return UICollectionViewCell()
}
let cellViewModel = viewModel.cellViewModel(index: indexPath.row)
cell.viewModel = cellViewModel
return cell
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cellViewModel = viewModel.cellViewModel(index: indexPath.row)
viewModel.didSelectGenre(index: (cellViewModel?.id)!)
}
}
VM is :
protocol GenreViewModelDelegate: class {
func didfinish(succsess: Bool)
}
protocol GenreListCoordinatorDelegate: class {
func movieListDidGenre(id: String)
}
class GenreViewModel {
weak var coordinatorDelegate: GenreListCoordinatorDelegate?
var networking = Networking()
var genresModels = [Genres]()
weak var delegate: GenreViewModelDelegate?
func getGenres() {
self.networking.preformNetwokTask(endPoint: TheMoviedbApi.genre, type: Genre.self, success: { [weak self] (response) in
print(response)
if let genres = response.genres {
self?.genresModels = genres
self?.delegate?.didfinish(succsess: true)
} else {
self?.delegate?.didfinish(succsess: false)
}
}) {
self.delegate?.didfinish(succsess: false)
}
}
var count: Int {
return genresModels.count
}
public func cellViewModel(index: Int) -> GenreCollectionViewCellModel? {
let genreCollectionViewCellModel = GenreCollectionViewCellModel(genre: genresModels[index])
return genreCollectionViewCellModel
}
public func didSelectGenre(index: String) {
coordinatorDelegate?.movieListDidGenre(id: index)
}
}
The problem is that when i am trying to inject the viewModel to the ViewController and the push it in the start function it wont work-when the viewDidLoad invoked the viewModel in the VC is nil.

With the same code, I managed to make it work, the viewModel property is populated on my side.
// Coordinator
protocol Coordinator {
func start()
}
class BaseCoordinator: Coordinator {
private var presenter: UINavigationController
private var genreViewController: ViewController?
private var viewModel = ViewModel()
init(presenter: UINavigationController) {
self.presenter = presenter
}
func start() {
let genreViewController = ViewController()
genreViewController.viewModel = viewModel
self.genreViewController = genreViewController
presenter.pushViewController(genreViewController, animated: true)
}
}
// ViewController + ViewModel
struct ViewModel { }
class ViewController: UIViewController {
var viewModel: ViewModel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .white
}
}
class AppCoordinator: Coordinator {
private let window: UIWindow
private let rootViewController: UINavigationController
private var genereListCoordinator: BaseCoordinator?
init(window: UIWindow) {
self.window = window
rootViewController = UINavigationController()
rootViewController.navigationBar.prefersLargeTitles = true
genereListCoordinator = BaseCoordinator(presenter: rootViewController)
}
func start() {
window.rootViewController = rootViewController
window.makeKeyAndVisible()
genereListCoordinator?.start()
}
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var applicationCoordinator: AppCoordinator?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let window = UIWindow()
let appCordinator = AppCoordinator(window: window)
self.window = window
self.applicationCoordinator = appCordinator
appCordinator.start()
return true
}
}
Often the issues with Coordinator pattern is to retaining the navigation stack which would not fire viewDidLoad or don't display screen at all for instance. In your case, if only viewModel is missing, I believe it comes from a ViewController constructor issue or an override.
I can see an #IBOutlet in your ViewController which makes me think you are using storyboard / xib file. However, the BaseCoordinator only use a init(), could it be the issue? If using Storyboard, you should try with instantiateViewController(...).
On another note, you should look into retaining all stack of coordinators to handle a full navigation and avoid retaining viewModel or other properties within coordinator when needed. If UINavigationController retain child UIViewController, you wouldn't need to as well.

Related

Debugger contradicting itself when describing UINavigationController

How is it possible that the navigation controller shown in the screenshot is described in the memory debugger as having 1 children (of type ContactsTableViewController) and at the same time in the lldb prompt is described as having 0 elements?
If you want more context or the code to reproduce it, please look at the code shown in: here
UPDATE:
I haven't run any other commands between the time I captured the memory graph and the time I ran the po command.
The minimal reproducible example is the following playground snippet ( I only have seen this strange behaviour when using a UIViewControllerRepresentable.Coordinator, that's why the minimal example is a bit complex):
import PlaygroundSupport
import SwiftUI
import UIKit
protocol Coordinator {
var nav: UINavigationController { get }
func start()
}
final class FlowCoordinator: Coordinator {
struct Dependencies {
let nav: UINavigationController
let screen1Factory: () -> UIViewController
let screen2Factory: () -> UIViewController
}
var nav: UINavigationController
let screen1Factory: () -> UIViewController
let screen2Factory: () -> UIViewController
init(dependencies: FlowCoordinator.Dependencies) {
self.nav = dependencies.nav
self.screen1Factory = dependencies.screen1Factory
self.screen2Factory = dependencies.screen2Factory
}
func start() {
let vc = screen1Factory() as! ViewController
vc.flowCoordinator = self
nav.pushViewController(vc, animated: false)
print("FlowCoordinator.started: flowCoordinator.nav: ", nav, "VCs count: ", nav.viewControllers.count)
}
func screen2() {
print("Pushing screen2")
let vc = screen2Factory() as! ViewController
vc.flowCoordinator = self
nav.pushViewController(vc, animated: false)
print("Coordinator.viewController.flowCoordinator.nav: ", "VCs count: ",
nav.viewControllers.count)
}
}
final class ViewController: UITableViewController {
weak var flowCoordinator: FlowCoordinator?
// Array of cities to display in the table view
let cities = ["New York", "Paris", "Tokyo"]
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CityCell")
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of rows in the section
return cities.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CityCell", for: indexPath)
// Configure the cell...
cell.textLabel?.text = cities[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
flowCoordinator?.screen2()
}
}
final class AppDependencies {
lazy var flowCoordinator: FlowCoordinator = {
let navigationController = UINavigationController()
navigationController.navigationBar.prefersLargeTitles = true
let flowCoordinatorDependencies = FlowCoordinator.Dependencies(
nav: navigationController,
screen1Factory: makeViewController1,
screen2Factory: makeViewController2)
return FlowCoordinator(dependencies: flowCoordinatorDependencies)
}()
func makeViewController1() -> UIViewController {
let vc = ViewController()
vc.title = "View 1"
return vc
}
func makeViewController2() -> UIViewController {
let vc = ViewController()
vc.title = "View 2"
return vc
}
}
[![enter image description here][1]][1]
struct View1: UIViewControllerRepresentable {
let flowCoordinator: FlowCoordinator
func makeUIViewController(context: Context) -> ViewController {
return context.coordinator.viewController
}
func updateUIViewController(_ uiViewController: ViewController, context: Context) {
}
}
extension View1 {
final class Coordinator: NSObject, UITableViewDelegate {
var viewController: ViewController
var flowCoordinator: FlowCoordinator
init(flowCoordinator: FlowCoordinator) {
self.flowCoordinator = flowCoordinator
self.flowCoordinator.start()
self.viewController = self.flowCoordinator.nav.viewControllers.first as! ViewController
super.init()
viewController.tableView.delegate = self
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("View1.tableView(_:didSelectRowAt:indexPath)")
print("Coordinator.viewController.flowCoordinator.nav: ", viewController.flowCoordinator?.nav ?? "nil", "VCs count: ",
viewController.flowCoordinator?.nav.viewControllers.count ?? "nil")
viewController.flowCoordinator?.screen2()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(flowCoordinator: flowCoordinator)
}
}
struct ContentView: View {
let deps = AppDependencies()
var body: some View {
View1(flowCoordinator: deps.flowCoordinator)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
let deps = AppDependencies()
//deps.flowCoordinator.start()
let nav = deps.flowCoordinator.nav
//let host = nav
let host = UIHostingController(rootView: View1(flowCoordinator: deps.flowCoordinator) )
PlaygroundPage.current.liveView = host

Reusable UITableViewController

I want to avoid duplicate rewriting same code and create reusable UITableViewController.
I have ExerciseViewController with 3 buttons. Each button push a UITableViewController on the navigation stack. There are three UITableViewControllers: 1) CategoryUITableVC, 2) EquipmentUITableVC, 3) MusclesUITableVC.
All of these three view controllers have almost exactly same layout - cells with labels and accessory buttons. The only difference is that first view controller has got image next to title. Is it worth doing one reusable VC and instantiate it 3 times or maybe better solution create 3 separated VC (but It will be just rewriting almost same code).
I use coordinator pattern.
class ExerciseCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
...
//unnecessary code to show
...
// *HERE I INSTANTIATE VIEW CONTROLLERS, I PRESENT THEM MODALLY BUT I WANT TO HAVE NAVIGATION BAR, SO I NEED TO CREATE NEW NAVIGATION CONTROLLERS*
lazy var equipmentVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var equipmentNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: equipmentVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
lazy var categoryVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var categoryNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: categoryVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
lazy var muscleVC: ReusableTableViewController = {
let vc = AppStoryboard.Main.instantiate(ReusableTableViewController.self)
vc.delegate = self
return vc
}()
lazy var muscleNavController: UINavigationController = {
let navController = UINavigationController(rootViewController: muscleVC)
navController.navigationItem.largeTitleDisplayMode = .always
return navController
}()
}
extension ExerciseCoordinator: CustomExerciseDelegate {
func selectCategory() {
navigationController.viewControllers.last?.present(categoryNavController, animated: true, completion: nil)
categoryVC.dataType = .category
}
func selectEquipment() {
navigationController.viewControllers.last?.present(equipmentNavController, animated: true, completion: nil)
equipmentVC.dataType = .equipment
}
func selectMuscles() {
navigationController.viewControllers.last?.present(muscleNavController, animated: true, completion: nil)
muscleVC.dataType = .muscle
}
}
I assign data type to know from where it comes from (CategoryVC/EquipmentVC/MuscleVC) when I will dismiss UITableVC.
Here it is my reusable UITableViewController:
import UIKit
import RealmSwift
class ExerciseCategoryTableViewController: UITableViewController {
var delegate: ExerciseSelectionCriteriaDelegate?
//I use it delegate to send data back after dismiss view
var dataType: DataType?
var data = [String]() {
didSet {
DispatchQueue.main.async {
self.tableView.reloadData() }}
}
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
func getData() {
if dataType == .category {
let allCategories = RealmService.shared.realm.objects(Category.self)
data = allCategories.compactMap({$0.category})
} else if dataType == .equipment {
let allEquipment = RealmService.shared.realm.objects(Equipment.self)
data = allEquipment.compactMap({$0.equipment})
} else {
let allMuscles = RealmService.shared.realm.objects(Muscles.self)
data = allMuscles.compactMap({$0.muscles})
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// below is my own shortcut for dequeue cell, it works
let cell: ExerciseSelectionTableViewCell = tableView.dequeueResuableCell(for: indexPath)
cell.category.text = data[indexPath.row]
if let image = UIImage(named: "\(data[indexPath.row].lowercased())") {
cell.categoryImage.image = image
cell.categoryImage.isHidden = false
} else {
cell.categoryImage.isHidden = true
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 70
}
#IBAction func closeViewController(_ sender: UIBarButtonItem) {
closeViewController()
}
#IBAction func saveSelectedCategories(_ sender: UIBarButtonItem) {
saveSelectedData()
}
func saveSelectedData() {
let selectedIndexes = tableView.indexPathsForSelectedRows
if let selectedData = selectedIndexes?.compactMap({data[$0.row]}) {
dismiss(animated: true) {
self.delegate?.selectedFields(for: selectedData, dataType: self.dataType)
}
} else {
dismiss(animated: true) {
self.delegate?.selectedFields(for: nil, dataType: self.dataType)
}
}
}
func closeViewController() {
guard let _ = tableView.indexPathsForSelectedRows else { return dismiss(animated: true, completion: nil)}
Alert.showAlertWithCompletion(on: self, with: "TEXT", message: "TEXT") { _ in
self.dismiss(animated: true, completion: nil)
}
}
}
I will be thankful if you tell me if this approach is correct or maybe better solution are separated view controllers or create protocol with default implementation?

duplicate data showing in UITableView after UIViewController being dismissed

Problems :
UITableview populate duplicate data even after removing the array when it dismiss
Expected output:
UITableview populate data based on itenary items in array
Actual output:
UITableView populates the correct amount output when the user selects the first location in DiscoverVC but when the user selects another location, the tableview append the itenary data that the user-selected previouly.
Summary:
I have 3VC in my project, first vc (DiscoverVC), will call api to populate data in UICollectionView, I implement UICollectionView Delegate to move to another screen with segue, in prepare segue I pass data from the first vc to second vc (ItenaryVC), in second vc i have 2 view inside it. One normal vc and the second is floating panel (ItenaryFP). When second vc loads up it will make API calls base on Location object that had been pass from first vc and pass the data to the third vc which is floating panel (ItenaryFP) through delegate in ItenaryVC.
PS; I use custom cell for the tableview and have, I also already try to remove the array from viewWillAppear and viewDidDissapear but it's still not working
GIF of how the issues occurs
Here is a summary of my code
DiscoverVC.swift
class DiscoverVC : UIViewController {
//MARK:- IBOutlets
#IBOutlet weak var collectionView: UICollectionView!
private var locationResult = [Location]()
private var selectedAtRow : Int!
//MARK:- Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
renderView()
getLocations()
}
private func renderView() {
collectionView.register(UINib(nibName: R.nib.discoverCell.name, bundle: nil), forCellWithReuseIdentifier: R.reuseIdentifier.discoverCell.identifier)
collectionView.delegate = self
collectionView.dataSource = self
}
private func getLocations(location : String = "locations") {
NetworkManager.shared.getLocations(for: location) { [weak self] location in
switch location {
case .success(let locations):
self?.updateDiscoverUI(with: locations)
case .failure(let error):
print(error.rawValue)
}
}
}
private func updateDiscoverUI(with locations : [Location]) {
DispatchQueue.main.async { [weak self] in
self?.locationResult.append(contentsOf: locations)
self?.collectionView.reloadData()
}
}
}
//MARK:- Delegate
extension DiscoverVC : UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedAtRow = indexPath.row
self.performSegue(withIdentifier: R.segue.discoverVC.goToDetails, sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destinationVC = segue.destination as? ItenaryVC else { return}
// Passing location object to ItenaryVC
destinationVC.locationName = locationResult[selectedAtRow]
destinationVC.imageURL = locationResult[selectedAtRow].image
// Remove tab bar when push to other vc
destinationVC.hidesBottomBarWhenPushed = true
}
}
ItenaryVC.swift
protocol ItenaryVCDelegate : AnyObject {
func didSendItenaryData(_ itenaryVC : ItenaryVC, with itenary : [[Days]])
func didSendLocationData(_ itenaryVC : ItenaryVC, with location : Location)
}
class ItenaryVC: UIViewController {
#IBOutlet weak var backgroundImage: UIImageView!
var fpc : FloatingPanelController!
var imageURL : URL?
var locationName: Location?
weak var delegate : ItenaryVCDelegate?
//MARK:- Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
setupCard()
setupView()
}
override func viewWillAppear(_ animated: Bool) {
// Call API for data
getItenaries(at: locationName!.itenaryName)
print("ItenaryVC Appear")
}
override func viewWillDisappear(_ animated: Bool) {
locationName = nil
}
}
//MARK:- Network Request
extension ItenaryVC {
func getItenaries(at itenaries : String = "Melaka"){
print("itenaries : \(itenaries)")
NetworkManager.shared.getItenaries(for: itenaries) { [weak self] itenary in
switch itenary {
case .success(let itenary):
// print(itenary)
DispatchQueue.main.async {
// Passing data to itenaryFP
self?.delegate?.didSendItenaryData(self! , with: itenary)
}
print(itenaries.count)
case .failure(let error):
print(error.rawValue)
}
}
}
}
//MARK:- Private methods
extension ItenaryVC {
private func setupView() {
backgroundImage.downloaded(from: imageURL!)
backgroundImage.contentMode = .scaleAspectFill
// Passing data to itenaryFP
delegate?.didSendLocationData(self, with: locationName!)
}
private func setupCard() {
guard let itenaryFlotingPanelVC = storyboard?.instantiateViewController(identifier: "itenaryPanel") as? ItenaryFP else { return}
// Initliase delegate to Floating Panel, create strong reference to Panel
self.delegate = itenaryFlotingPanelVC
fpc = FloatingPanelController()
fpc.set(contentViewController: itenaryFlotingPanelVC)
fpc.addPanel(toParent: self)
fpc.delegate = self
fpc.layout = self
}
}
ItenaryFP.swift
class ItenaryFP: UIViewController{
var itenaries = [[Days]]()
var location : Location?
//MARK:- : Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
print("ItrenaryFP viewDidLoad, itenaries : \(itenaries.count), location : \(location)")
renderView()
}
override func viewWillAppear(_ animated: Bool) {
itenaries.removeAll()
itenaryTableView.reloadData()
}
override func viewWillDisappear(_ animated: Bool) {
DispatchQueue.main.async {
self.itenaries.removeAll()
print("ItenarFP dissapear, itenaries :\(self.itenaries.count), location : \(self.location)")
self.location = nil
self.itenaryTableView.reloadData()
}
}
private func renderView() {
itenaryTableView.register(UINib(nibName: R.nib.itenaryCell.name, bundle: nil), forCellReuseIdentifier: R.nib.itenaryCell.identifier)
itenaryTableView.dataSource = self
itenaryTableView.delegate = self
locDescHC.constant = locDesc.contentSize.height
}
}
//MARK:- Data source
extension ItenaryFP : UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return itenaries.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return itenaries[section].count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell : ItenaryCell = itenaryTableView.dequeueReusableCell(withIdentifier: R.nib.itenaryCell.identifier, for: indexPath) as! ItenaryCell
let listOfItenaries = itenaries[indexPath.section][indexPath.row]
cell.cellContent(for: listOfItenaries)
return cell
}
}
//MARK:- ItenaryVC Delegate
extension ItenaryFP : ItenaryVCDelegate {
func didSendLocationData(_ itenaryVC: ItenaryVC, with location: Location) {
DispatchQueue.main.async {
self.locationLabel.text = location.locationName
self.locDesc.text = location.description
self.sloganLabel.text = location.slogan
}
}
func didSendItenaryData(_ itenaryVC: ItenaryVC, with itenary: [[Days]]) {
DispatchQueue.main.async {
self.itenaries.append(contentsOf: itenary)
self.itenaryTableView.reloadData()
print("itenary \(self.itenaries.count)")
}
}
}
Use:
self.itenaries = itenary
Instead of:
self.itenaries.append(contentsOf: itenary)

Swift: How to update data in Container View without storyboard

My project is created programmatically without using storyboard. And it is like Apple Music's miniPlayer, when clicking a row in tableView, will update the data of miniPlayer(which is in containerView).
I see some examples with storyboard and segue like below code: call child viewController's method in parent viewController to update data by using protocol & delegate.
But I don't use storyboard, so what is the alternative code to prepare()?
protocol ContentDelegate {
func updateContent(id: Int)
}
class ParentViewController: UIViewController {
var delegate: ContentDelegate?
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "containerController") {
let containerVC = segue.destination as! ChildContainerViewController
self.delegate = containerVC
}
}
}
class ChildContainerViewController: UIViewController, ContentDelegate {
func updateContent(id: Int) {
// your code
}
}
My Code: add container view in the root view controller(UITabViewController).
class ViewController: UITabBarController {
// mini player
var miniPlayer: MiniPlayerViewController?
// container view
var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// set tabBar and other stuff
...
configureContainer()
}
func configureContainer() {
// add container
containerView = UIView()
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
containerView.heightAnchor.constraint(equalToConstant: 64.0)
])
// add child view controller view to container
miniPlayer = MiniPlayerViewController()
guard let miniPlayer = miniPlayer else { return }
addChild(miniPlayer)
miniPlayer.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(miniPlayer.view)
// Create and activate the constraints for the child’s view.
guard let miniPlayerView = miniPlayer.view else { return }
NSLayoutConstraint.activate([
miniPlayerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
miniPlayerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
miniPlayerView.topAnchor.constraint(equalTo: containerView.topAnchor),
miniPlayerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
miniPlayer.didMove(toParent: self)
}
}
I want to trigger the update when clicking the row in parentView.
protocol ContentDelegate {
func configure(songs: [Song]?, at index: Int)
}
class SongsListViewController: UIViewController {
private var tableView: UITableView!
var delegate: ContentDelegate?
// MARK: - data source
var songs = [Song]()
. . .
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let index = indexPath.row
let vc = MiniPlayerViewController()
self.delegate = vc
self.delegate?.configure(songs: songs, at: index)
// present(vc, animated: true)
}
The update method in child view.
extension MiniPlayerViewController {
func configure(songs: [Song]?, at index: Int) {
if let songs = songs {
let song = songs[index]
songTitle.text = song.title
thumbImage.image = song.artwork?.image
} else {
// placeholder fake info
songTitle.text = "你在终点等我"
thumbImage.image = UIImage(named: "Wang Fei")
}
}
}
There is more than one approach to this...
First approach - no custom delegate:
Use the subclassed UITabBarController as an "intermediary". Give it a func such as:
func configure(songs: [Song]?, at index: Int) -> Void {
miniPlayer.configure(songs: songs, at: index)
}
then, in your "Select Song" view controller (one of the tabs):
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tbc = self.tabBarController as? CustomTabBarController else {
return
}
let index = indexPath.row
tbc.configure(songs: songs, at: index)
}
Second approach - using a custom delegate:
protocol ContentDelegate {
func configure(songs: [Song]?, at index: Int)
}
Make sure your "mini player" controller conforms to the delegate:
class MiniPlayerViewController: UIViewController, ContentDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// add UI elements, any other setup code
}
}
extension MiniPlayerViewController {
func configure(songs: [Song]?, at index: Int) {
if let songs = songs {
let song = songs[index % songs.count]
songTitle.text = song.title
thumbImage.image = song.artwork
} else {
// placeholder fake info
songTitle.text = "你在终点等我"
thumbImage.image = UIImage(named: "Wang Fei")
}
}
}
Give your "Select Song" view controller (and any other of the tab controllers) a delegate var:
class SelectSongViewController: UIViewController {
var delegate: ContentDelegate?
// everything else
}
then, in your subclassed UITabBarController:
override func viewDidLoad() {
super.viewDidLoad()
configureContainer()
if let vc = viewControllers?.first as? SelectSongViewController {
vc.delegate = miniPlayer
}
}
now your "Select Song" view controller can call the delegate func:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tbc = self.tabBarController as? CustomTabBarController else {
return
}
let index = indexPath.row
delegate?.configure(songs: songs, at: index)
}

UISearchController in iOS 9 - TextField from UISearchBar clipping out of navigation item

I am trying to get a smooth Searchbar on navigation item on iOS 9, which means I can't use navigationItem.searchController property since its only iOS 11 only.
class SearchContainerViewController: UITableViewController {
let dataSource = ["1", "2", "3", "4", "5"]
override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.count
}
override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.textLabel?.text = dataSource[indexPath.row]
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
dismiss(animated: true, completion: nil)
}
}
class SearchViewController: UISearchController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
class MyViewController : UIViewController, UISearchResultsUpdating, UISearchBarDelegate {
lazy var searchButton = UIBarButtonItem(title: "Search", style: UIBarButtonItem.Style.plain, target: self, action: #selector(showSearchBar))
var searchViewController: SearchViewController = {
let container = SearchContainerViewController()
let searchController = SearchViewController(searchResultsController: container)
return searchController
}()
override func viewDidLoad() {
super.viewDidLoad()
setupSearchController()
setupSearchButton()
}
func setupSearchController() {
searchViewController.searchResultsUpdater = self
searchViewController.searchBar.delegate = self
searchViewController.dimsBackgroundDuringPresentation = false
searchViewController.hidesNavigationBarDuringPresentation = false
searchViewController.searchBar.searchBarStyle = .minimal
searchViewController.searchBar.showsCancelButton = true
definesPresentationContext = true
}
#objc func showSearchBar() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView = self.searchViewController.searchBar
self.navigationItem.rightBarButtonItem = nil
self.searchViewController.searchBar.becomeFirstResponder()
}
}
func setupSearchButton() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView = nil
self.navigationItem.rightBarButtonItem = self.searchButton
}
}
// MARK: Conforms to UISearchResultUpdating
public func updateSearchResults(for searchController: UISearchController) { }
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
setupSearchButton()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
view.layoutSubviews()
}
}
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
let newWindow = UIWindow(frame: UIScreen.main.bounds)
let mainViewController = MyViewController()
let navigationController = UINavigationController(rootViewController: mainViewController)
newWindow.backgroundColor = .white
newWindow.rootViewController = navigationController
newWindow.makeKeyAndVisible()
window = newWindow
return true
}
}
Though the result is kinda disappointing since the textview with StatusBar is clipping out of the navigation item context, there is anything im doing wrong and could've done better?
Appreciate your support.
In before people down voting the question for no reasons, I am going to do something different and answer my own question.
The clipping happened because in both situations the height of the navigationItem were different because of they are somewhat stretchable if put big content in titleView (like a searchBar).
I've set the searchBar from the start on the navigationItem and just toogle their isHidden property when it should be done.
#objc private func activateSearch() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView?.isHidden = false
self.navigationItem.rightBarButtonItem = nil
self.searchController.isActive = true
}
}
private func deactivateSearch() {
UIView.animate(withDuration: 0.75) {
self.navigationItem.titleView?.isHidden = true
self.navigationItem.rightBarButtonItem = self.searchButton
self.searchController.isActive = false
}
}

Resources