Create menu dynamically but using same view controller [PagingMenuController] - ios

I am trying to create ButtonBar based paging menu but it will be dynamic because the data (title for no. of menu) created will come from the server. Unfortunately, there is no such example created for this situation. I know it's really stupid to request such example because I have no idea for this case. I just want to use this library as new because it seems more smooth then other libraries that I used. So, I really need help with an example project is appreciated. And I want to use the same view controller for each different tab like UITableViewController for showing data for each tab.
Any Help with that??? An example will be appreciated...Thanks..

My solution is to make a synchronous call to the server to fetch the data that i need to populate the menu. It is possible that there is an asynchronous solution but i can't quite figure it out, since my app depends on completely on the menu items.
fileprivate func parseMenuItems() {
self.menuItems = [MenuObject]()
let url = URL(string: MENU_URL)
do {
let data = try Data(contentsOf: url!)
let json = JSON(data: data)
for (_, subJson) in json["items"] {
guard let name = subJson["name"].string else {return}
guard let url = subJson["url"].string else {return}
let menuItem = MenuObject(name: name, url: url)
self.menuItems.append(menuItem)
}
} catch {
print(error)
}
}
For parsing I am using SwiftyJson, but that is irrelevant here.
This function (parseMenuItems()) is called before super.viewDidLoad().
Next step is to populate view controllers with menuItems data:
override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
var children = [UIViewController]()
for menuItem in menuItems! {
let child = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "TestViewController") as! TestViewController
child.name = menuItem.name
child.url = menuItem.url
children.append(child)
}
}
return children
}
Hope it helps :)

Related

Passing data from child (I think?) of one Tab Bar View Controller to Collection View of another TabBarVC? (Programmatically)

I'm building a tab bar controller based application programmatically. I'm trying to figure out that how to send data from what I take to be the child of one tab bar view controller (an additional presented VC on top of viewControllers[1]), to a collection view of another one of the tab bar VCs at viewControllers[0]). I've read a bunch of entries here but none seem to be working for me or I'm not understanding them.
Essentially, within the VC in viewControllers[1], I've implemented a custom, full screen camera by implementing this code within the main TabVC.
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if viewControllers?.firstIndex(of: viewController) == 1 {
present(cameraVC, animated: true, completion: nil)
}
return true
}
Then within that VC, I'm capturing the image and stacking a new editVC on top of that using present(editVC) to show the photo captured as well as some edit tools. I say "stacking" even though I don't know if these actually become a part of a hierarchy when presented like this? Could use some clarity on that. From that editingVC, I'm implementing the edits and saving the files to the documentsDirectory. All that works fine and the image successfully saves. But where I'm stuck is getting the path's address for the photo I've saved back to my collection view VC at viewControllers[0] so that I can populate its cells with the photo(s).
I've tried creating a delegate but I realized I can't set viewControllers[0] as a delegate for the (1st or maybe 2nd?) child of viewControllers[1]. (I'm unclear whether the code above essentially makes the cameraVC a child in an of itself of viewControllers[1].) Or can I/is it recommended?
Then I tried creating a state class containing an array of URLs (for the paths of the saved files) that both editVC could write to, and the viewControllers[0] collection view could read from. But I can't seem to get the editVC to update that array. I suspect I am inadvertently creating multiple instances of that "state". class from each of my different VCs?
I have so much code I'm not certain what to post but hopefully this is relevant:
In the state class:
class StatePaths {
var urls = [URL]()
}
In the editVC
// declaration at the beginning of the class
var statePaths: StatePaths?
// In the save func:
let imageName = UUID().uuidString
let imagePath = getDocumentsDirectory().appendingPathComponent(imageName)
if let jpegData = image.jpegData(compressionQuality: 0.8) {
try? jpegData.write(to: imagePath)
self.statePaths.urls.append(imagePath)
self.dismiss(animated: true)
}
In the collection view of viewControllers[0]
var statePaths = StatePaths()
// In the cellForItem func:
let path = statePaths[indexPath.item]
if let image = UIImage(contentsOfFile: path.path) {
cell.imageView.image = image
}
I also implemented some print statements that show the images are being saved, but that they are not populating the statePaths class. I also implemented collectionView.reloadData() on ViewWillAppear of my collection VC.
Any help whether on these approaches or any others would be greatly appreciated.
I suspect I am inadvertently creating multiple instances of that
"state". class from each of my different VCs?
Yes, "statePaths" are multiple instances and no need to use it. You need to fetch the saved images at your viewControllers[0].
Also, the more clear way is to decouple this code to a separate model or service.
For example:
struct ImageStorage {
static var documentsDirectory: String {
return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
}
static var imagesDirectory: String {
let imagesDirectory = documentsDirectory + "/images/"
if !FileManager.default.fileExists(atPath: imagesDirectory) {
try? FileManager.default.createDirectory(atPath: imagesDirectory, withIntermediateDirectories: true, attributes: nil)
}
return imagesDirectory
}
static var images: [UIImage] {
guard let paths = try? FileManager.default.contentsOfDirectory(atPath: imagesDirectory) else {
return []
}
let images = paths.compactMap { UIImage(contentsOfFile: imagesDirectory + $0) }
return images
}
static func save(_ image: UIImage) {
let imageName = UUID().uuidString
let imagePath = imagesDirectory + imageName
let imageUrl = URL(fileURLWithPath: imagePath)
guard let jpegData = image.jpegData(compressionQuality: 0.8) else {
return
}
try? jpegData.write(to: imageUrl)
}
}
Then use it in viewController:
ImageStorage.save(image)
ImageStorage.images
Upd. To question about passing data: It depends.
In some cases you can pass data to viewController by using delegates, tabBarViewController stack, NotificationCenter etc.
But in other cases easier to use singleton for temporarily storing the data.
As I can see the link between viewControllers is not so obvious in your case. So the singleton is preferred.
As example:
class StatePaths {
static let shared = StatePaths()
private init() {}
var urls = [URL]()
}
StatePaths.shared.urls = urls
let urls = StatePaths.shared.urls

Adding element to UITableView

I am trying to add an element into the array cart.
After executing this function more than once on different items, I see from the print statement that the count never increases and always stay at 1. Furthermore, the new element replaces the old element rather than appending to the array.
Additionally, I don't know how to display the data into the table view of cartTableViewController. Whenever I navigate to cartTableViewController, no elements show up.
#IBAction func addToCart(_ sender: UIBarButtonItem) {
let mainStoryBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let cartTableViewController = mainStoryBoard.instantiateViewController(withIdentifier: "CartTableView") as? CartTableViewController {
var cart = cartTableViewController.cartItems
let itemToAdd = CartItem(product: product!, quantity: quantity)
print(itemToAdd!.product.itemName) // the new item replaces the existing one
cart.append(itemToAdd!)
print(cart.count) // count is always 1
print(quantityLabel.text! + " " + (product?.itemName)! + " added")
}
}
Try this may help you,
var cart: [CartItem]!
override func viewDidLoad() {
super.viewDidLoad()
let mainStoryBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let cartTableViewController = mainStoryBoard.instantiateViewController(withIdentifier: "CartTableView") as? CartTableViewController {
cart = cartTableViewController.cartItems
}
}
#IBAction func addToCart(_ sender: UIBarButtonItem) {
if cart != nil {
let itemToAdd = CartItem(product: product!, quantity: quantity)
print(itemToAdd!.product.itemName) // the new item replaces the existing one
cart.append(itemToAdd!)
print(cart.count) // count is always 1
print(quantityLabel.text! + " " + (product?.itemName)! + " added")
}
}
I can clearly say that you have a problem with the below line because the value of cartTableViewController.cartItems always nil or empty
let cartTableViewController = mainStoryBoard.instantiateViewController(withIdentifier: "CartTableView") as? CartTableViewController
var cart = cartTableViewController.cartItems
Every time, You have to use old instead of the initialized new object.
You can fix your problem defining a global array of the cart which is
accessible throughout app.
Code:
let cartTableViewController = mainStoryBoard.instantiateViewController(withIdentifier: "CartTableView") as? CartTableViewController
var cart = aryCart //Your cart global object
print(cart.count) // Updated Count
Update:
Just define a variable outside of class then it will automatically accessible throughout application.
var aryCart = [CartItem]()
class CartVC:UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func addToCart(_ sender: UIBarButtonItem) {
let mainStoryBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
if let cartTableViewController = mainStoryBoard.instantiateViewController(withIdentifier: "CartTableView") as? CartTableViewController {
let itemToAdd = CartItem(product: product!, quantity: quantity)
print(itemToAdd!.product.itemName) // the new item replaces the existing one
aryCart.append(itemToAdd!)
print(aryCart.count) // count is always 1
print(quantityLabel.text! + " " + (product?.itemName)! + " added")
}
}
}
The new array you're creating only exists within the scope of the function. When assigning cart to cartTableViewController.cartItems, this only copies the values from cartItems into cart, it doesn't assign the pointer. For example:
var cartItems = ["First element"]
var cart = cartItems
cart.append("Second Element")
print(cartItems)
print(cart)
This would print:
["First element"]
["First element", "Second Element"]
It looks like you're adding data to an array in another ViewController. I would generally advise you to update state through some other mechanism than the instance variable in the other view controller. There are many philosophies about how to structure code in the best way, one solution for you could be to use the MVVM pattern. This article covers it: https://medium.com/#azamsharp/mvvm-in-ios-from-net-perspective-580eb7f4f129
A quick way to "clean up" a little bit would be to have an intermediate object where you store the cartItems data, which you have a reference to in both the current view controller and CartTableView, such as CartItemStore:
class CartItemStore {
static let shared = CartItemStore()
private let cartItems: [CartItem]
// you can add logic for reading previously added cart items from cache in order to persist it.
// The same goes when adding new cartItems
private init() {
self.cartItems = []
}
func getItem(_ index: Int) -> CartItem? {
//Check whether the index passed in is valid
if cartItems.indices.contains(index) {
return cartItems[index]
} else {
return nil
}
}
func addItem(_ item: CartItem) {
cartItems.append(item)
}
func numberOfItems() -> Int {
return cartItems.count
}
}
Then you can use your singleton store in the following way:
// This inside the current view controller
private let cartItemStore = CartItemStore.shared
#IBAction func addToCart(_ sender: UIBarButtonItem) {
...
let itemToAdd = CartItem(product: product!, quantity: quantity)
cartItemStore.addItem(itemToAdd)
print(cartItemStore.numberOfItems())
}
Then you can read items from the other view controller as well by accessing the singleton.
Singletons should generally be avoided since it can be hard to test your application. Singletons allow state to be updated wherever, which can create a nightmare if you abuse it good enough. I used it here to give you a quick way to accomplish something that is a little less coupled, but if you will sit with this code base for a while you might want to rethink this. Feel free to look into MVVM and how to manage state with it, it will make things easier, in the long run, to have a structured way of managing state in your application :) Hope this is to any help of you.

Cannot transfer JSON value to Label in SecondViewController

I am trying to transfer a JSON value to a label in another view controller, from pressing a button in my Main View Controller. My first view controller is called 'ViewController' and my second one is called 'AdvancedViewController. The code below shows how I get the JSON data, and it works fine, displays the JSON values in labels in my MainViewController, but when I go to send a JSON value to a label in my AdvancedViewController, I press the button, it loads the AdvancedViewController but the label value is not changed? I have assigned the label in my AdvancedViewController and I'm not sure why its not working. I am trying to transfer it to the value 'avc.Label' which is in my advanced view controller
The main label code shows how I get it to work in my MainViewController
Code below:
My Main ViewController:
guard let APIUrl = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=" + text + "&appid=e7b2054dc37b1f464d912c00dd309595&units=Metric") else { return }
//API KEY
URLSession.shared.dataTask(with: APIUrl) { data, response, error in
guard let data = data else { return }
let decoder = JSONDecoder()
//Decoder
do {
let weatherData = try decoder.decode(MyWeather.self, from: data)
if (self.MainLabel != nil)
{
if let gmain = (weatherData.weather?.first?.main) { //using .first because Weather is stored in an array
print(gmain)
DispatchQueue.main.async {
self.MainLabel.text! = String (gmain)
}
}
}
let avc = AdvancedViewController(nibName: "AdvancedViewController", bundle: nil)
if (avc.Label != nil)
{
if let mTest = (weatherData.weather?.first?.myDescription) { //using .first because Weather is stored in an array
DispatchQueue.main.async {
avc.Label.text! = String (mTest)
}
}
}
In AdvancedViewController create variable that store the value of mTest
class AdvancedViewController: ViewController {
var test: String!
override func viewDidLoad(){
super.viewDidLoad()
if let test = test {
myLabel.text = test
}
}
}
let vc = storyboard?.instantiateViewController(withIdentifier: "AdvancedViewController") as! AdvancedViewController
if let mTest = (weatherData.weather?.first?.myDescription) {
vc.test = mTest
}
DispatchQueue.main.async {
present(vc, animated: true, completion: nil)
}
You shouldn't try to manipulate another view controller's views. That violates the OO principle of encapsulation. (And it sometimes just plain doesn't work, as in your case.)
Salman told you what to do to fix it. Add a String property to the other view controller, and then in that view controller's viewWillAppear, install the string value into the desired label (or do whatever is appropriate with the information.)

After retrieving data from Firebase, how to send retrieved data from one view controller to another view controller?

This is my table created in Firebase here. I have a search button. The button action will be at first it will fetch data from firebase and then it will send it to another view controller and will show it in a table view. but main problem is that before fetching all data from Firebase
self.performSegueWithIdentifier("SearchResultPage", sender: self )
triggers and my next view controller shows a empty table view. Here was my effort here.
From this post here I think that my code is not placed well.
Write this line of code in your dataTransfer method in if block after complete for loop
self.performSegueWithIdentifier("SearchResultPage", sender: self )
Try sending the search string as the sender when you are performing the segue, for example:
self.performSegueWithIdentifier("SearchResultPage", sender: "searchString")
Then, in your prepare for segue get the destination view controller and send over the string to the new view controller. something like:
destinationViewController.searchString = sender as! String
When the segue is completed and you have come to the new view controller, you should now have a searchString value that is set, use this value to perform your query and get the relevant data to show in the new table view.
Please note that this is just one solution of many, there are other ways to achieve this.
The reason why you are not able to send data is because, you are trying to send data even before the data could actually be fetched.
Try the following where you are pushing to next view controller only after getting the data
func dataTransfer() {
let BASE_URL_HotelLocation = "https://**************.firebaseio.com/Niloy"
let ref = FIRDatabase.database().referenceFromURL(BASE_URL_HotelLocation)
ref.queryOrderedByChild("location").queryStartingAtValue("uttara").observeEventType(.Value, withBlock: { snapshot in
if let result = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in result {
let downloadURL = child.value!["image"] as! String;
self.storage.referenceForURL(downloadURL).dataWithMaxSize(25 * 1024 * 1024, completion: { (data, error) -> Void in
let downloadImage = UIImage(data: data!)
let h = Hotel()
h.name = child.value!["name"] as! String
print("Object \(count) : ",h.name)
h.deal = child.value!["deal"] as! String
h.description = child.value!["description"] as! String
h.distance = child.value!["distance"] as! String
h.latestBooking = child.value!["latestBooking"] as! String
h.location = child.value!["location"] as! String
h.image = downloadImage!
self.HotelObjectArray.append(h)
})
}
let storyboard = UIStoryboard(name:"yourStoryboardName", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("SearchResultPageIdentifier") as! SearchResultPage
self.navigationController.pushViewController(vc, animated: true)
}
else {
print("no results")
}
})
}

Load data again after segue is done

I would like to know how should I behave if I got several articles in table view controller, which are loaded and parsed from some API. Then I would like to click on some article to view his detail.
Should I load all articles on start up with all data and then pass concrete whole article to next detail table controller or just send article id and in new table controller load again whole one article with passed id?
I think that is much better second method, but have no idea how to do it. Any help? Thanks a lot.
EDIT: added code
MainTableViewController:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let articleId = articles[indexPath.row].Id
let destination = DetailTableViewController()
destination.articleId = articleId
destination.performSegue(withIdentifier: "Main", sender: self)
}
DetailTableViewController:
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 140
// Vyzkoušíme jestli jsme připojeni do internetu
if Helper.hasConnectivity() {
// Zapneme načítací obrazovku
setLoadingScreen()
// Načteme jídla
loadMainArticles()
} else {
promptAlert(title: "Upozornění", message: "Ujistěte se, že Vaše zařízení je připojené k internetu")
}
}
private func loadMainArticles() {
ApiService.sharedInstance.fetchArticle(part: "GetArticle", articleId: "brananske-posviceni--spravna-lidova-zabava-s-pecenou-husou") { (articles: [Article]) in
self.removeLoadingScreen()
self.tableView.separatorStyle = .singleLine
if articles.count == 0 {
self.promptAlert(title: "Upozornění", message: "Žádná data")
}
self.selectedArticle = articles[0]
self.tableView.reloadData()
}
}
ApiService:
func fetchArticle(part: String, articleId: String, completion: #escaping ([Article]) -> ()) {
var Id = ""
var Title = ""
var Content = ""
var Picture = ""
var PublishDate = ""
let url = NSURL(string: "http://xxx")
URLSession.shared.dataTask(with: url! as URL) { (data, response, error) in
if error != nil {
print(error as Any)
return
}
do {
let json = try! JSONSerialization.jsonObject(with: data!, options: .allowFragments)
var articles = [Article]()
for dictionary in json as! [[String: AnyObject]] {
.
.
.
let article = Article(Id: Id, Title: Title, Content: Content, Picture: Picture, PublishDate: PublishDate, Categories: Categories)
articles.append(article!)
}
DispatchQueue.main.async(execute: {
completion(articles)
})
}
}.resume()
}
The problem is that when I do the segue after click on row, then should loadMainArticles. The method is triggered, but always stops on URLSession row and immediately jump to resume() and the completion block is never triggered. Is there any reason why?
This is a choice that should be done according to article data model size.
You should pay attention to which data you need to show on TableView single cell and which on Article detail page.
I suppose you to show:
Title and posted hour in TableView
Title, hour, long description, maybe some image in Detail page
In this case i would reccommend you to download only title and hour for each article item, and then, in detail page, download single item details (avoid to download unused content, since user could not open every item).
Otherwise, if amounts of data are similar, download all informations on first connection and open detail page without making any other network request.
I think that is much better second method, but have no idea how to do it.
You need to retrieve article id from TableView selected cell and pass it to DetailPageViewController using prepareForSegue. In DetailPageViewController ViewDidLoad send article id to your backend api and retrieve article details to show.

Resources