Dynamic number of ChildViewControllers for XLPagerTabStrip - ios

I am using XLPagerTabStrip to create a category based reading app in Swift 4. I learnt static number of ViewControllers can be easily created using the following function.
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { }
However, the number of categories in my case depends on server response, which may change as per the need. I tried to create dynamic number of tabs by creating view controllers based on the name of categories I parsed from the json response. This is the method I did a hit and trial.
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
var childrenVC = [UIViewController]()
for eachCategory in postCategories {
print(eachCategory)
let newVC = self.storyboard?.instantiateViewController(withIdentifier: "FirstTVC") as? FirstTVC
newVC?.childName = eachCategory.name
childrenVC.append(newVC!)
self.reloadPagerTabStripView()
self.reloadInputViews()
}
return childrenVC
}
Yes, it failed. How can I achieve dynamic number of tabs in this case? If not I am also open to any other alternative. I am done with json response thing but stuck in this step. This SO answer and Github Issue didn't help as well.

I had the exact same situation. If you fetch results from the server asynchronously, as you should, you will have a crash since xlpagertabstrip needs at least one child viewcontroller.
The solution i found is to return one dummy viewcontroller at the start of the application, and after fetching data from the server to reloadPagerTabStripView.
In your parent viewcontroller you should make an empty array of your objects which you fetch from the server, for example:
var menuItems = [MenuObject]()
Next, viewControllers(for pagerTabStripController ... ) method should look like this:
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
var children = [UIViewController]()
if menuItems.count == 0 {
let child = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DummyVC") as! DummyVC
children.append(child)
return children
} else {
let menuItemsUrls = menuItems.map{$0.url}
for menuItem in menuItems {
let child = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MainVC") as! TelegrafMainVC
child.name = menuItem.name.uppercased
child.url = menuItem.url
children.append(child)
}
return children
}
}
And after you fetch data from the server, whether using URLSession, or Alamofire, or whatever else, in the function or a closure you should put something like:
self.menuItems = menuItems
DispatchQueue.main.async {
self.reloadPagerTabStripView()
}
Tell me if i need to clarify something, but i believe this will be sufficient to help you.
Cheers

Step 1 : Do this in your initial viewcontroller,
class initialViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
self.loadMainView(viewControllerArray: self.setMainViewTabParameters())
}
func setMainViewTabParameters() -> NSMutableArray {
let viewControllerArray = NSMutableArray()
var tabArray = ["Tab1", "Tab2", "Tab3", "Tab4", "Tab5", "Tab6", "Tab7"]
var tabIndex = NSInteger()
for item in tabArray {
let tabString = tabArray[tabIndex]
let tabview = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "YourTabControllerIdentifier") as! YourTabController
tabview.tabHeadingTitle = tabString as NSString
viewControllerArray.add(tabview)
tabIndex = tabIndex + 1
}
return viewControllerArray
}
func loadMainView(viewControllerArray : NSMutableArray) -> Void {
let mainView = self.storyboard?.instantiateViewController(withIdentifier: "YourMainControllerIdentifier") as! YourMainController
mainView.viewControllerList = viewControllerArray
self.navigationController?.pushViewController(mainView, animated: false)
}
}
Step 2:
class YourMainController: ButtonBarPagerTabStripViewController {
var viewControllerList = NSArray()
var isReload = false
//MARK: - PagerTabStripDataSource
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
guard isReload else {
return viewControllerList as! [UIViewController]
}
var childViewControllers = viewControllerList as! [UIViewController]
for (index, _) in childViewControllers.enumerated(){
let nElements = childViewControllers.count - index
let n = (Int(arc4random()) % nElements) + index
if n != index{
swap(&childViewControllers[index], &childViewControllers[n])
}
}
let nItems = 1 + (arc4random() % 8)
return Array(childViewControllers.prefix(Int(nItems)))
}
override func reloadPagerTabStripView() {
isReload = true
if arc4random() % 2 == 0 {
pagerBehaviour = .progressive(skipIntermediateViewControllers: arc4random() % 2 == 0, elasticIndicatorLimit: arc4random() % 2 == 0 )
} else {
pagerBehaviour = .common(skipIntermediateViewControllers: arc4random() % 2 == 0)
}
super.reloadPagerTabStripView()
}
}
Step 3: Your tab view controller:
class YourTabController: UIViewController, IndicatorInfoProvider {
var tabHeadingTitle = NSString()
// MARK: - Top Tab Bar Method - IndicatorInfoProvider
func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo {
return IndicatorInfo(title: tabHeadingTitle as String)
}
}

Try this
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
var childrenVC: [UIViewController] = []
for eachCategory in postCategories {
let newVC = UIStoryboard(name: "Main (YOUR-STORYBOARD-NAME)", bundle: nil).instantiateViewController(withIdentifier: "FirstTVC") as? FirstTVC
newVC?.childName = eachCategory.name
childrenVC.append(newVC!)
}
return childrenVC
}

Related

Swift - passing data from VC to detail VC with Firestore

I'm totally new to Swift well I have a View Controller, where a uitableview of data is being fetched from the Firestore and I want to send this data from View Controller to detail View Controller. I mean, when a cell in View Controller is clicked, detail View Controller shows such as name, description from Firestore.. is there anyone to help me?
here's HospitalViewController.Swift :
class HospitalViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
struct HospitalData {
var Name: String = ""
var Image: String = ""
var Region: String = ""
func getDic() -> [String:String] {
let dic = [
"Name": self.Name,
"Image": self.Image,
"Region": self.Region
]
return dic
}
}
var hospitalArray: Array<HospitalData> = []
#IBOutlet weak var hospitalTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.isNavigationBarHidden = true
hospitalTableView.delegate = self
hospitalTableView.dataSource = self
}
#IBAction func onBtnRead(_ sender: UIButton) {
getValueFromList()
}
func getValueFromList() {
hospitalArray.removeAll()
let db = Firestore.firestore()
db.collection("Hospital").getDocuments() {
(querySnapshot, err) in
if let error = err {
print("fail", error)
}else{
print("success")
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
let dataDic = document.data() as NSDictionary
let Name = dataDic["Name"] as? String ?? ""
print("Name:", Name)
let Image = dataDic["Image"] as? String ?? ""
print("Image:", Image)
let Region = dataDic["Region"] as? String ?? ""
print("Region:", Region)
var hospital = HospitalData()
hospital.Name = Name
hospital.Image = Image
hospital.Region = Region
self.hospitalArray.append(hospital)
}
self.hospitalTableView.reloadData()
}
}
}
func setValueIntoList() {
var hospital = HospitalData()
hospital.Name = "SUN Hospital"
hospital.Image = "hospital.png"
hospital.Region = "Seoul"
let dic = hospital.getDic()
let db = Firestore.firestore()
var ref: DocumentReference? = nil
ref = db.collection("Hospital").addDocument(data: dic) {
err in
if let error = err {
print("fail", error)
}else{
print("success", ref!.documentID)
}
}
}
// mark: datasource
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.hospitalArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = hospitalTableView.dequeueReusableCell(withIdentifier: "hosptialTableViewCell", for: indexPath) as! HospitalTableViewCell
let hospitalStruct = self.hospitalArray[indexPath.row]
cell.labelName.text = hospitalStruct.Name
cell.labelRegion.text = hospitalStruct.Region
cell.hospitalImageView.image = UIImage(named: "hospital.png")
return cell
}
To navigate from one viewcontroller to others you can use the basic push navigation as bellow
let viewController = UIStoryboard.init(name: "YOUR_STORYBOARD", bundle: Bundle.main).instantiateViewController(withIdentifier: "YOUR_VC_IDENTIFIER") as? YOUR_VC
self.navigationController?.pushViewController(viewController, animated: true)
Now to pass data from one view to the viewcontroller you would like to move you have to declare a variable on the viewcontroller you would like to move.
i.e
You need to have a variable in your detail vc.
hospital_detail_vc.swift
var hospitalData: HospitalData?
Now you need to pass the data/variable as below through the navigation in your TableView's method didSelectRowAt as below.
let viewController = UIStoryboard.init(name: "YOUR_STORYBOARD", bundle: Bundle.main).instantiateViewController(withIdentifier: "YOUR_VC_IDENTIFIER") as? YOUR_VC
viewController.hospitalData = self.hospitalArray[indexPath.row]
self.navigationController?.pushViewController(viewController, animated: true)
Happy coding :)

Dynamically assign ViewController to Navigate

In my case I have UITableView and have View all button for the listing of all the items in separate screens. So I added target for UIButton action method in cellForRowAt. Now what I am doing in action method:
#IBAction func btnViewAllOffer(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tblOfferView)
let indexPath = self.tblOfferView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
if let type = self.homeData[indexPath!.section].type {
if type == HomeDataType.SponserProduct.rawValue {
let vc1 = self.storyboard?.instantiateViewController(withIdentifier: "ViewController1") as! ViewController1
if let title = self.homeData[indexPath!.section].title {
vc1.title = title
}
self.navigationController?.pushViewController(vc1, animated: true)
} else if type == HomeDataType.Offer.rawValue {
let vc2 = self.storyboard?.instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
if let title = self.homeData[indexPath!.section].title {
vc2.title = title
}
self.navigationController?.pushViewController(vc2, animated: true)
} else if type == HomeDataType.BestSeller.rawValue {
let vc3 = self.storyboard?.instantiateViewController(withIdentifier: "ViewController3") as! ViewController3
if let title = self.homeData[indexPath!.section].title {
vc3.title = title
}
self.navigationController?.pushViewController(vc3, animated: true)
}
}
}
}
What I need, is there any way I can minimize the code and assign viewcontrollers dynamically so there is no need to instantiate each view controller and push them everytime?
Something like:
var vc = UIViewController()
if let type = self.homeData[indexPath!.section].type {
if type == HomeDataType.SponserProduct.rawValue {
vc = ViewController1()
}
else if type == HomeDataType.Offer.rawValue {
vc = ViewController2()
} else if type == HomeDataType.BestSeller.rawValue {
vc = ViewController3()
}
}
self.navigationController?.pushViewController(vc, animated: true)
Use a protocol (SimilarViewController) to define the common properties like title:
protocol SimilarViewController {
var title: String? { get set }
}
class ViewController1: UIViewController, SimilarViewController {
var title: String?
}
class ViewController2: UIViewController, SimilarViewController {
var title: String?
}
class ViewController3: UIViewController, SimilarViewController {
var title: String?
}
#IBAction func btnViewAllOffer(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tblOfferView)
let indexPath = self.tblOfferView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
if let type = self.homeData[indexPath!.section].type {
var vcGeneric: SimilarViewController?
if type == HomeDataType.SponserProduct.rawValue {
vcGeneric = self.storyboard?.instantiateViewController(withIdentifier: "ViewController1") as! ViewController1
} else if type == HomeDataType.Offer.rawValue {
vcGeneric = self.storyboard?.instantiateViewController(withIdentifier: "ViewController2") as! ViewController2
} else if type == HomeDataType.BestSeller.rawValue {
vcGeneric = self.storyboard?.instantiateViewController(withIdentifier: "ViewController3") as! ViewController3
}
if let title = self.homeData[indexPath!.section].title {
vcGeneric?.title = title
}
if let vcGeneric = vcGeneric as? UIViewController {
self.navigationController?.pushViewController(vcGeneric, animated: true)
}
}
}
}
1: create a struct and assign the value to it.
struct TitleDetails {
static var title : String = ""
}
2: create an extension of viewController and use it to avoid code repetition.
extension UIViewController {
func pushVC(_ vcName : String) {
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: vcname)
self.navigationController?.pushViewController(vc, animated: true)
}
}
3: now you can call it directly as,
TitleDetails.title = yourTitleValue
self.pushVC("ViewController1")
and in your ViewDidLoad() method of your destination view controller,
self.title = TitleDetails.title
Create BaseViewController and derived other ViewController from BaseViewController
class BaseViewController: UIViewController {
var viewTitle = ""
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func pushVC(_ vcName : String) {
let vc = UIStoryboard.init(name: "Main", bundle: Bundle.main).instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(vc, animated: true)
}
}
And use below code on ViewController you need like:
#IBAction func btnViewAllOffer(_ sender: UIButton) {
let buttonPosition = sender.convert(CGPoint.zero, to: self.tblOfferView)
let indexPath = self.tblOfferView.indexPathForRow(at: buttonPosition)
if indexPath != nil {
if let type = self.homeData[indexPath!.section].type {
self.viewTitle = self.homeData[indexPath!.section].title
if type == HomeDataType.SponserProduct.rawValue {
self.pushVC("ViewController1")
} else if type == HomeDataType.Offer.rawValue {
self.pushVC("ViewController2")
} else if type == HomeDataType.BestSeller.rawValue {
self.pushVC("ViewController3")
}
}
}
}

Passing data to ConrainerView from ViewControlller always nil

I have CurrentYearViewController in that I used container to show multiple views. The problem is value that I am sending to ContainerViewController is getting always nil
CurrentYearViewController
//calling this function after getting response, not presenting because it is called from Segmented Control
func refreshCurrentYearListView(dict: NSDictionary) {
let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let containerViewController = mainStoryboard.instantiateViewController(withIdentifier: "containerView") as!
ContainerViewController
containerViewController.dataDictionary = dict
}
ContainerViewController
class ContainerViewController: UIViewController {
#IBOutlet weak var billedValueLbl: UILabel!
var dataDictionary: NSDictionary!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewWillAppear(_ animated: Bool) {
if let data = dataDictionary {
populateJSON(dict: data)
}
}
func populateJSON(dict: NSDictionary) {
let N_A: String = "N/A"
let parser = CommonParser()
let currentYearSalesSummaryDict: NSDictionary = parser.parse(dictionary: dict, key: "currentYearInvoiceSummary")
if currentYearSalesSummaryDict.count > 0 {
billedValueLbl.text = parser.parse(dictionary: currentYearSalesSummaryDict, key: "totalInvoiceAmount", exceptionString: N_A)
}
}

How to make UIPageViewController switch controllers automatically

I was following this great tutorial from Duc Tran about UIPageViewController. I was wondering how would you make the controllers inside your UIPageViewController transition automatically without having the user swipe. This is how the code looks without the delegate and datasource.
class AnimesPageVC: UIPageViewController {
override var navigationOrientation: UIPageViewControllerNavigationOrientation {return .horizontal}
weak var pageViewControllerDelegate: AnimePagesVCDelegate?
var timeInterval: Int?
var images: [UIImage]?
lazy var animes: [UIViewController] = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
var animes = [UIViewController]()
if let images = self.images {
for image in images {
let anime = storyboard.instantiateViewController(withIdentifier: "AnimeImage")
animes.append(anime)
}
}
self.pageViewControllerDelegate?.setUpPageController(numberOfPages: animes.count)
return animes
}()
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
dataSource = self
loadPage(atIndex: 0)
}
func loadPage(atIndex: Int) {
let anime = animes[atIndex]
var direction = UIPageViewControllerNavigationDirection.forward
if let currentAnime = viewControllers?.first {
if let currentIndex = animes.index(of: currentAnime) {
if currentIndex > atIndex {
direction = .reverse
}
}
}
configurePages(viewController: anime)
setViewControllers([anime], direction: direction, animated: true, completion: nil)
}
func configurePages(viewController: UIViewController) {
for (index, animeVC) in animes.enumerated() {
if viewController === animeVC {
if let anime = viewController as? AnimeVC {
anime.image = self.images?[index]
self.pageViewControllerDelegate?.turnPageController(to: index)
}
}
}
}
}
So how would I be able to get that kind of behavior. Would appreciate any help. :)
Add a timer to your view did load and call the same load function with updated index
Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { (_) in
// call your function here
self.loadPage(atIndex: index + 1)
// you have to update your index also.
self.index = self.index + 1
}
This will call your loadPage function each 0.3 sec
keep in mind that my solution is only for one way if you it's only going to next page because I am adding to automatically it will not come back to previous controller for that you have do something like
index = index - 1

UIPageViewController crashing when not using static data

I have a working page view controller system with this ( just going to paste absolutely everything, pretty sure the issue just lies in my firebase observation near the top) :
class IndividualPeopleVC: UIViewController, UIPageViewControllerDataSource {
var pageViewController: UIPageViewController!
var pageTitles : [String]!
var imageURLS: NSArray!
let currentUser = FIRAuth.auth()?.currentUser?.uid
override func viewDidLoad()
{
super.viewDidLoad()
self.pageTitles = []
self.pageTitles = ["1", "2", "3", "4'"]
DataService.ds.REF_INTERESTS.child(passedInterest).child("rooms").child(passedRoom).child("users").observeSingleEventOfType(.Value) { (snapshot: FIRDataSnapshot) in
print(snapshot.value)
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
let name = snap.key
let uid = snap.value as! String
let person = Person(name: name, bio: "", UID: uid)
if person.UID != self.currentUser {
self.pageTitles.append(uid)
}
}
}
self.pageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("PageViewController") as! UIPageViewController
self.pageViewController.dataSource = self
let startVC = self.viewControllerAtIndex(0) as ContentViewController
let viewControllers = NSArray(object: startVC)
self.pageViewController.setViewControllers(viewControllers as? [UIViewController], direction: .Forward, animated: true, completion: nil)
self.pageViewController.view.frame = CGRectMake(0, 30, self.view.frame.width, self.view.frame.size.height - 60)
self.addChildViewController(self.pageViewController)
self.view.addSubview(self.pageViewController.view)
self.pageViewController.didMoveToParentViewController(self)
}
func viewControllerAtIndex(index: Int) -> ContentViewController
{
if ((self.pageTitles.count == 0) || (index >= self.pageTitles.count)) {
return ContentViewController()
}
var vc: ContentViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContentViewController") as! ContentViewController
vc.titleText = self.pageTitles[index] as! String
vc.imageFile = self.pageImages[index] as! String
vc.pageIndex = index
return vc
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController?
{
var vc = viewController as! ContentViewController
var index = vc.pageIndex as Int
if (index == 0 || index == NSNotFound)
{
return nil
}
index--
return self.viewControllerAtIndex(index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var vc = viewController as! ContentViewController
var index = vc.pageIndex as Int
if (index == NSNotFound) {
return nil
}
index++
if (index == self.pageTitles.count){
return nil
}
return self.viewControllerAtIndex(index)
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int{
return self.pageTitles.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int{
return 0
}
}
So basically most of that is just copy-pasted from another source and it works. It creates 4 pages from the pageTitles array that scroll and on another VC I am passing the 1,2,3 and 4 and a label is displaying everything in the array as the scrollview should.
The problem comes when I try to not hard-code the pageTitles and instead pull it from firebase.
When I comment out where I'm hard-coding the pageTitles array, I get an unexpected nil value on my ContentViewController where I'm sending the titles and I do like titleLabel.text = (what was sent over).
I figured maybe guarding it with an if/let to allow firebase time to load stuff in would help, so I did a
if let title = (what was sent over) {
titleLabel.text = title
}
Nothing shows up on the screen when this happens, and then if I scroll left or right I get an error with indexes down in my pagebefore/pageafters.
I feel like this should be super simple and very similar to a tableview? Load in data from firebase, fill an array with the info, use if/lets to allow firebase some time to load in, then update the UI based on the array.
I think your DataService is asynchronous. You have to create your pageviewcontroller once your data is ready. Try this modified viewDidLoad method.
override func viewDidLoad()
{
super.viewDidLoad()
self.pageTitles = []
// self.pageTitles = ["1", "2", "3", "4'"]
DataService.ds.REF_INTERESTS.child(passedInterest).child("rooms").child(passedRoom).child("users").observeSingleEventOfType(.Value) { (snapshot: FIRDataSnapshot) in
print(snapshot.value)
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
let name = snap.key
let uid = snap.value as! String
let person = Person(name: name, bio: "", UID: uid)
if person.UID != self.currentUser {
self.pageTitles.append(uid)
}
}
self.pageViewController = self.storyboard?.instantiateViewControllerWithIdentifier("PageViewController") as! UIPageViewController
self.pageViewController.dataSource = self
let startVC = self.viewControllerAtIndex(0) as ContentViewController
let viewControllers = NSArray(object: startVC)
self.pageViewController.setViewControllers(viewControllers as? [UIViewController], direction: .Forward, animated: true, completion: nil)
self.pageViewController.view.frame = CGRectMake(0, 30, self.view.frame.width, self.view.frame.size.height - 60)
self.addChildViewController(self.pageViewController)
self.view.addSubview(self.pageViewController.view)
self.pageViewController.didMoveToParentViewController(self)
}
}

Resources