I have two requests. Each of them getting different data. I need to show an indicator when the all of two request is requesting. How i can do this?
this is my first request:
func productList(tableView:UITableView,spinner:UIActivityIndicatorView,index1:Int,index2:Int,index3:Int){
if product.count<=0{
alamoFireManager?.request(.GET, "http://mobile.unimax.kz/api/Default1",parameters: ["type1id":index1,"type2id":index2,"type3id":index3,"name":"","userid":1089])
.responseJSON { response in
guard response.result.error == nil else {
if let httpError = response.result.error {
switch(httpError.code){
case -1009:
let alert = UIAlertView(title: "Ошибка",message: "Нету интернета!!",delegate: nil,cancelButtonTitle: "OK")
alert.show()
break
default:
let alert = UIAlertView(title: "Ошибка",message: "Повторите попытку!!",delegate: nil,cancelButtonTitle: "OK")
alert.show()
break
}
} else { //no errors
let statusCode = (response.response?.statusCode)!
print(statusCode)
}
spinner.stopAnimating()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
return
}
if let value = response.result.value {
// handle the results as JSON, without a bunch of nested if loops
let product = JSON(value)
for (_,subJson):(String, JSON) in product {
let img:NSData
if let src=subJson["sphoto"].string{
if src.containsString("jpg"){
let range = src.startIndex.advancedBy(2)..<src.endIndex
let substring = src[range]
var urlString = "http://admin.unimax.kz/\(substring)"
urlString = urlString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
if let dataFromURL=NSData(contentsOfURL: NSURL(string: urlString)!){
img=dataFromURL
}
else{
img=NSData(contentsOfURL: NSURL(string: "http://zhaksy-adam.kz/Images/domalak.png")!)!
}
}
else{
img=NSData(contentsOfURL: NSURL(string: "http://zhaksy-adam.kz/Images/domalak.png")!)!
}
}
else{
img=NSData(contentsOfURL: NSURL(string: "http://zhaksy-adam.kz/Images/domalak.png")!)!
}
//Do something you want
let id=subJson["id"].int!
let name=subJson["name"].string!
let price=subJson["price"].int!
let description=subJson["description"].rawString()
self.product.append(Product(id:id,title: name, img: UIImage(data: img), price: price,desc:description!))
}
spinner.stopAnimating()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
tableView.reloadData()
}
}
}
else{
spinner.stopAnimating()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
tableView.reloadData()
}
}
and this is my second request:
func makeGetFav(userID:Int,completionHandler: (responseObject:JSON) -> ()) {
alamoFireManager?.request(.GET, "http://mobile.unimax.kz/api/Klientapi/?authid=\(userID)")
.responseJSON {response in
guard response.result.error == nil else {
if let httpError = response.result.error {
switch(httpError.code){
case -1009:
let alert = UIAlertView(title: "Ошибка",message: "Нету интернета!!",delegate: nil,cancelButtonTitle: "OK")
alert.show()
break
default:
let alert = UIAlertView(title: "Ошибка",message: "Повторите попытку!!",delegate: nil,cancelButtonTitle: "OK")
alert.show()
break
}
} else { //no errors
let statusCode = (response.response?.statusCode)!
print(statusCode)
}
return
}
completionHandler(responseObject: JSON(response.result.value!))
}
}
func getFavs(userID:Int,tableView:UITableView,spinner:UIActivityIndicatorView){
getFavRequets(userID){(responseObject) in
if responseObject != nil{
self.favs.removeAll()
self.localDB.clearFav()
for (_,subJson):(String, JSON) in responseObject {
self.favs.append(FavModel(id: subJson["id"].int!, title: subJson["name"].string!, price: subJson["price"].int!))
}
spinner.stopAnimating()
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
tableView.reloadData()
}
}
}
there are i call it all:
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
indicator.startAnimating()
localDB.getUserInfo()
getRequests.productList(tableView, spinner: indicator, index1: catalog1Index, index2: catalog2Index, index3: catalog3Index)
if localDB.user.count>0{
getRequests.getFavs(localDB.user[0].id, tableView: tableView, spinner: indicator)
}
localDB.checkCart(tableView, tabCtrl: tabBarController!)
You can control it using a singleton to start and stop it according to the number of running requests:
class NetworkActivityIndicator: NSObject {
static let sharedInstance = NetworkActivityIndicator()
private override init() {
}
var count = 0 {
didSet {
self.updateIndicator()
}
}
private func updateIndicator() {
if count > 0 {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
} else {
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
}
The you simply call NetworkActivityIndicator.sharedInstance.count += 1 just before the request and NetworkActivityIndicator.sharedInstance.count += 1 when you get the response
the easiest way is to add two variables to your class that indicate whether the associated request is complete, and then to create a function that stops the spinner only if both the variables indicate the calls are complete.
if you want to use the class for more than one ViewController then I suggest adding a struct-enum combo to organise the variables that are indicating which requests are currently underway.
eg
class GetRequests {
var productsLoaded = false
var favoritesLoaded = false
func stopSpinnerIfNeeded(spinner: UIActivityIndicatorView) {
if productsLoaded && favoritesLoaded {
spinner.stopAnimating()
spinner.hidden = true
}
}
func productList(tableView:UITableView,spinner:UIActivityIndicatorView,index1:Int,index2:Int,index3:Int){
defer {
productsLoaded = true
stopSpinnerIfNeeded(spinner)
}
if product.count<=0{
alamoFireManager?.request(.GET, "http://mobile.unimax.kz/api/Default1",parameters: ["type1id":index1,"type2id":index2,"type3id":index3,"name":"","userid":1089])
.responseJSON { response in
guard response.result.error == nil else {
if let httpError = response.result.error {
switch(httpError.code){
case -1009:
let alert = UIAlertView(title: "Ошибка",message: "Нету интернета!!",delegate: nil,cancelButtonTitle: "OK")
alert.show()
break
default:
let alert = UIAlertView(title: "Ошибка",message: "Повторите попытку!!",delegate: nil,cancelButtonTitle: "OK")
alert.show()
break
}
} else { //no errors
let statusCode = (response.response?.statusCode)!
print(statusCode)
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
return
}
if let value = response.result.value {
// handle the results as JSON, without a bunch of nested if loops
let product = JSON(value)
for (_,subJson):(String, JSON) in product {
let img:NSData
if let src=subJson["sphoto"].string{
if src.containsString("jpg"){
let range = src.startIndex.advancedBy(2)..<src.endIndex
let substring = src[range]
var urlString = "http://admin.unimax.kz/\(substring)"
urlString = urlString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!
if let dataFromURL=NSData(contentsOfURL: NSURL(string: urlString)!){
img=dataFromURL
}
else{
img=NSData(contentsOfURL: NSURL(string: "http://zhaksy-adam.kz/Images/domalak.png")!)!
}
}
else{
img=NSData(contentsOfURL: NSURL(string: "http://zhaksy-adam.kz/Images/domalak.png")!)!
}
}
else{
img=NSData(contentsOfURL: NSURL(string: "http://zhaksy-adam.kz/Images/domalak.png")!)!
}
//Do something you want
let id=subJson["id"].int!
let name=subJson["name"].string!
let price=subJson["price"].int!
let description=subJson["description"].rawString()
self.product.append(Product(id:id,title: name, img: UIImage(data: img), price: price,desc:description!))
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
tableView.reloadData()
}
}
}
else{
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
tableView.reloadData()
}
}
func getFavs(userID:Int,tableView:UITableView,spinner:UIActivityIndicatorView){
getFavRequets(userID){(responseObject) in
if responseObject != nil{
self.favs.removeAll()
self.localDB.clearFav()
for (_,subJson):(String, JSON) in responseObject {
self.favs.append(FavModel(id: subJson["id"].int!, title: subJson["name"].string!, price: subJson["price"].int!))
}
favoritesLoaded = true
stopSpinnerIfNeeded(spinner)
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
tableView.reloadData()
}
}
}
A very fast way to do it:
// global var
var isIndicatorActive : Bool = false
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
indicator.startAnimating()
self.isIndicatorActive = true
In the line before each alamoFireManager?.request you call :
if isIndicatorActive == false {
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
indicator.startAnimating()
self.isIndicatorActive = true
}
And after each line spinner.stopAnimating() add:
self.isIndicatorActive = false
I suggest you to using single network activity indicator and keep a counter which keep track of network activity within the application. I know my answer is more in text, but looking at your code it seems you can implement the following.
0 indicates no activity indicator.
As soon as a new activity starts, increment the counter do a check if counter is greater than 0 then show the indicator.
Decrement the counter when the activity task finishes. On decrement keep a check if counter is 0 then set the indicator visibility to false.
PS: Don't forget to increment/decrement in sync blocks. You may use objc_sync_enter(..) and objc_sync_exit(..) methods for this.
Thx
thanks everybody for help. I solve it like this:
func makeReuest1(){
if localDB.user.count>0{
getRequests.getFavs(localDB.user[0].id)
}
makeRequest2()
}
func makeRequest2(){
getRequests.productList(tableView, spinner: indicator, index1: catalog1Index, index2: catalog2Index, index3: catalog3Index)
}
Related
I don't quite understand what I am doing wrong since I am very new to MVVM. It worked in MVC architecture. I've setup my VM and am able to get the first set of results and even then that's not working properly. I get 4 results instead of 10 which is what LOADLIMIT is set as. I was able to get it to work in an MVC architecture without any issues. The VM function which triggers the query is called multiple (3) times instead of just once i.e. even prior to scrolling.
Here is my VM:
enum FetchRestaurant {
case success
case error
case location
case end
}
class ListViewModel {
let restaurant: [Restaurant]?
let db = Firestore.firestore()
var restaurantArray = [Restaurant]()
var lastDocument: DocumentSnapshot?
var currentLocation: CLLocation?
typealias fetchRestaurantCallback = (_ restaurants: [Restaurant]?, _ message: String?, _ status: FetchRestaurant) -> Void
var restaurantFetched: fetchRestaurantCallback?
var fetchRestaurant: FetchRestaurant?
init(restaurant: [Restaurant]) {
self.restaurant = restaurant
}
func fetchRestaurantCallback (callback: #escaping fetchRestaurantCallback) {
self.restaurantFetched = callback
}
func fetchRestaurants(address: String) {
print("address received: \(address)")
getLocation(from: address) { location in
if let location = location {
self.currentLocation = location
self.queryGenerator(at: location)
} else {
self.restaurantFetched?(nil, nil, .location)
}
}
}
func queryGenerator(at location: CLLocation) {
var query: Query!
if restaurantArray.isEmpty {
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).limit(to: Constants.Mealplan.LOADLIMIT)
} else {
print("last document:\(String(describing: lastDocument?.documentID))")
query = db.collection("Restaurant_Data").whereField("distributionType", isLessThanOrEqualTo: 2).start(afterDocument: lastDocument!).limit(to: Constants.Mealplan.LOADLIMIT)
}
batchFetch(query: query)
}
func batchFetch(query: Query) {
query.getDocuments { (querySnapshot, error) in
if let error = error {
self.restaurantFetched?(nil, error.localizedDescription, .error)
} else if querySnapshot!.isEmpty {
self.restaurantFetched?(nil, nil, .end)
} else if !querySnapshot!.isEmpty {
let queriedRestaurants = querySnapshot?.documents.compactMap { querySnapshot -> Restaurant? in
return try? querySnapshot.data(as: Restaurant.self)
}
guard let restaurants = queriedRestaurants,
let currentLocation = self.currentLocation else {
self.restaurantFetched?(nil, nil, .end)
return }
self.restaurantArray.append(contentsOf: self.applicableRestaurants(allQueriedRestaurants: restaurants, location: currentLocation))
DispatchQueue.main.asyncAfter(deadline: .now(), execute: {
self.restaurantFetched?(self.restaurantArray, nil, .success)
})
self.lastDocument = querySnapshot!.documents.last
}
}
}
func getLocation(from address: String, completionHandler: #escaping (_ location: CLLocation?) -> Void) {
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(address) { (placemarks, error) in
guard let placemarks = placemarks,
let location = placemarks.first?.location else {
completionHandler(nil)
return
}
completionHandler(location)
}
}
}
And in the VC viewDidLoad:
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
I would really appreciate links or resources for implementing MVVM in Swift for a Firestore back-end. I'm coming up short on searches here and on Google. Even tried medium.
EDIT
class ListViewController: UITableViewController {
lazy var loadingShimmer: UIImageView = {
let image = UIImage(named: "shimmer_background")
let imageview = UIImageView(image: image)
imageview.contentMode = .top
imageview.translatesAutoresizingMaskIntoConstraints = false
return imageview
}()
var restaurantArray = [Restaurant]()
var planDictionary = [String: Any]()
var fetchMore = false
var reachedEnd = false
let leadingScreensForBatching: CGFloat = 5.0
var searchController = UISearchController(searchResultsController: nil)
var currentAddress : String?
var listViewModel = ListViewModel(restaurant: [Restaurant]())
override func viewDidLoad() {
super.viewDidLoad()
setupTable()
}
override func viewWillAppear(_ animated: Bool) {
clearsSelectionOnViewWillAppear = false
}
func setupTable() {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Restaurant", style: .plain, target: nil, action: nil)
tableView.register(RestaurantCell.self, forCellReuseIdentifier: "Cell")
tableView.delegate = self
tableView.dataSource = self
let navigationBarHeight: CGFloat = self.navigationController!.navigationBar.frame.height
tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: -navigationBarHeight, right: 0)
tableView.separatorStyle = .none
tableView.showsVerticalScrollIndicator = false
tableView.addSubview(loadingShimmer)
loadingShimmer.topAnchor.constraint(equalTo: tableView.safeAreaLayoutGuide.topAnchor).isActive = true
loadingShimmer.leadingAnchor.constraint(equalTo: tableView.leadingAnchor).isActive = true
loadingShimmer.trailingAnchor.constraint(equalTo: tableView.trailingAnchor).isActive = true
loadingShimmer.startShimmering()
initialSetup()
}
func initialSetup() {
let addressOne = planDictionary["addressOne"] as! String + ", "
let city = planDictionary["city"] as! String + ", "
let postalCode = planDictionary["postalCode"] as! String
currentAddress = addressOne + city + postalCode
setupSearch()
listViewModel.fetchRestaurantCallback { (restaurants, error, result) in
switch result {
case .success :
self.loadingShimmer.stopShimmering()
self.loadingShimmer.removeFromSuperview()
guard let fetchedRestaurants = restaurants else { return }
self.restaurantArray.append(contentsOf: fetchedRestaurants)
self.tableView.reloadData()
self.fetchMore = false
case .location :
self.showAlert(alertTitle: "No businesses nearby", message: "Try going back and changing the address")
case .error :
guard let error = error else { return }
self.showAlert(alertTitle: "Error", message: error)
case .end :
self.fetchMore = false
self.reachedEnd = true
}
}
if let currentAddress = currentAddress {
listViewModel.fetchRestaurants(address: currentAddress)
}
}
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching {
print("\(fetchMore), \(reachedEnd)")
if !fetchMore && !reachedEnd {
if let address = self.currentAddress {
print("address sent: \(address)")
listViewModel.fetchRestaurants(address: address)
}
}
}
}
}
That you're only getting back 4 results instead of 10 is not due to a faulty query or get-document request—those are coded properly. You're either losing documents when you parse them (some are failing Restaurant initialization), Constants.Mealplan.LOADLIMIT is wrong, or there aren't more than 4 documents in the collection itself that satisfy the query.
That the query is executed 3 times instead of once is also not due to anything in this code—viewDidLoad is only called once and geocodeAddressString only returns once. You're making a fetch request elsewhere that we can't see.
In the batchFetch method, you have a guard that returns out of the function without ever calling its completion handler. This will leave the UI in a state of limbo. I'd recommend always calling the completion handler no matter why the function returns.
You never manage the document cursor. If the get-document return has less documents than the load limit, then nil the last-document cursor. This way, when you attempt to get the next page of documents, guard against a nil cursor and see if there is even more to fetch.
There's no need to pass in an empty array and have your function fill it; simply construct and return an array of results within ListViewModel itself.
We can't see how you trigger pagination. Is it through a scroll delegate when the user reaches the bottom or through a button tap, for example? If it's through a scroll delegate, then I'd disable that for now and see how many returns you get—I suspect one, instead of 3.
What is the particular reason you've ditched MVC for MVVM here? With MVC, you can get pagination up with just a few lines of code. I think MVVM is overkill for iOS applications and would advise against using it unless you have a compelling reason.
I sent my data from my API call to my InfoController viewDidLoad. There, I was able to safely store it in a skillName constant, and also printed it, receiving all the information by console.
The problem comes when I try to assign this variable to my skillLabel.
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
guard var skillName = names as? String else { return }
self.pokemon?.skillName = skillName
self.allNames = skillName
print(self.allNames)
}
}
There, when I print allNames, the console shows all the data I need. This is how the data looks like: Data Example
And the computed property where I wanna use this data looks is:
var pokemon: Pokemon? {
didSet {
guard let id = pokemon?.id else { return }
guard let data = pokemon?.image else { return }
navigationItem.title = pokemon?.name?.capitalized
infoLabel.text = pokemon?.description
infoView.pokemon = pokemon
if id == pokemon?.id {
imageView.image = UIImage(data: data)
infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: "\(allNames)")
}
}
}
PD: allNames is a String variable I have at InfoController class-level.
This is how my app looks when run:
PokeApp
My goal is to get that details param to show the skillName data, but it returns nil, idk why. Any advice?
EDIT1: My func that fetches the Pokemon data from my service class is this one:
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
DispatchQueue.main.async {
self.pokemon? = poke
guard let skills = poke.abilities else { return }
for skill in skills {
guard let ability = skill.ability else { return }
guard var names = ability.name!.capitalized as? String else { return }
self.pokemon?.skillName = names
handler(names)
}
}
}
}
EDIT2: InfoView class looks like:
class InfoView: UIView {
// MARK: - Properties
var delegate: InfoViewDelegate?
// This whole block assigns the attributes that will be shown at the InfoView pop-up
// It makes the positioning of every element possible
var pokemon: Pokemon? {
didSet {
guard let pokemon = self.pokemon else { return }
guard let type = pokemon.type else { return }
guard let defense = pokemon.defense else { return }
guard let attack = pokemon.attack else { return }
guard let id = pokemon.id else { return }
guard let height = pokemon.height else { return }
guard let weight = pokemon.weight else { return }
guard let data = pokemon.image else { return }
if id == pokemon.id {
imageView.image = UIImage(data: data)
}
nameLabel.text = pokemon.name?.capitalized
configureLabel(label: typeLabel, title: "Type", details: type)
configureLabel(label: pokedexIdLabel, title: "Pokedex Id", details: "\(id)")
configureLabel(label: heightLabel, title: "Height", details: "\(height)")
configureLabel(label: defenseLabel, title: "Defense", details: "\(defense)")
configureLabel(label: weightLabel, title: "Weight", details: "\(weight)")
configureLabel(label: attackLabel, title: "Base Attack", details: "\(attack)")
}
}
let skillLabel: UILabel = {
let label = UILabel()
return label
}()
let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
return iv
}()
. . .
}
infoView.configureLabel is this:
func configureLabel(label: UILabel, title: String, details: String) {
let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(title): ", attributes: [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: Colors.softRed!]))
attributedText.append(NSAttributedString(string: "\(details)", attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.gray]))
label.attributedText = attributedText
}
EDIT 3: Structures design
struct Pokemon: Codable {
var results: [Species]?
var abilities: [Ability]?
var id, attack, defense: Int?
var name, type: String?
...
}
struct Ability: Codable {
let ability: Species?
}
struct Species: Codable {
let name: String?
let url: String?
}
Jump to the Edit2 paragraph for the final answer!
Initial Answer:
I looks like you UI does not get updated after the controller fetches all the data.
Since all of you UI configuration code is inside the var pokemon / didSet, it's a good idea to extract it to a separate method.
private func updateView(with pokemon: Pokemon?, details: String?) {
guard let id = pokemon?.id, let data = pokemon?.image else { return }
navigationItem.title = pokemon?.name?.capitalized
infoLabel.text = pokemon?.description
infoView.pokemon = pokemon
if id == pokemon?.id {
imageView.image = UIImage(data: data)
infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: details ?? "")
}
}
and now you can easily call in the the didSet
var pokemon: Pokemon? {
didSet { updateView(with: pokemon, details: allNames) }
}
and fetchPokemons completion aswell
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
guard var skillName = names as? String else { return }
self.pokemon?.skillName = skillName
self.allNames = skillName
print(self.allNames)
DispatchQueue.main.async {
self.updateView(with: self.pokemon, details: self.allNames)
}
}
}
It's super important to do any UI setup on the main queue.
Edit:
The fetch function may be causing the problems! you are calling handler multiple times:
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
DispatchQueue.main.async {
self.pokemon? = poke
guard let skills = poke.abilities else { return }
let names = skills.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
handler(names)
}
}
}
Edit2:
After looking at your codebase there are a couple of things you need to change:
1. fetchPokemons implementation
the handler of controller.service.fetchPokes gets called for every pokemon so we need to check if the fetched one is the current (self.pokemon) and then call the handler with properly formated skills.
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
guard poke.id == self.pokemon?.id else { return }
self.pokemon? = poke
let names = poke.abilities?.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
handler(names ?? "-")
}
}
2. update viewDidLoad()
now simply pass the names value to the label.
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
self.pokemon?.skillName = names
self.infoView.configureLabel(label: self.infoView.skillLabel, title: "Skills", details: names)
}
}
3. Refactor var pokemon: Pokemon? didSet observer
var pokemon: Pokemon? {
didSet {
guard let pokemon = pokemon, let data = pokemon.image else { return }
navigationItem.title = pokemon.name?.capitalized
infoLabel.text = pokemon.description!
infoView.pokemon = pokemon
imageView.image = UIImage(data: data)
}
}
I want to delete a post, if the post has equal/more then 5 dislikes. I implemented the counter part and this works already. But the post will not be deleted even if I have 6 dislikes like on this post below:
Here you can see my database.
This is the code, I want to delete the posts after 5 dislikes:
// Dislike Button
func addTapGestureToDislikeImageView() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleDidTapDislike))
dislikeImageView.addGestureRecognizer(tapGesture)
dislikeImageView.isUserInteractionEnabled = true
}
#objc func handleDidTapDislike() {
guard let postId = post?.id else { return }
PostApi.shared.incrementDislikes(postId: postId, onSuccess: { (post) in
self.updateDislike(post: post)
self.post?.dislikes = post.dislikes
self.post?.isDisliked = post.isDisliked
self.post?.dislikeCount = post.dislikeCount
}, onError: { (errorMessage) in
ProgressHUD.showError(errorMessage)
})
}
func updateDislike(post: PostModel) {
if post.isDisliked == false || post.dislikes == nil {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-50")
} else {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-filled-50")
}
guard let count = post.dislikeCount else { return }
if count >= 5 {
deletePost()
}
}
func deletePost() {
// Remove the post from the DB
let ref = Database.database().reference()
ref.child("posts").child((post?.id)!).removeValue { error,ref in
if error != nil {
print(error!.localizedDescription)
}
}
}
Here I increment Dislikes:
func incrementDislikes(postId id: String, onSuccess: #escaping (PostModel) -> Void, onError: #escaping (_ errorMessage: String?) -> Void) {
let postRef = REF_POSTS.child(id)
postRef.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
if var post = currentData.value as? [String : AnyObject], let uid = UserApi.shared.CURRENT_USER_ID {
var dislikes: Dictionary<String, Bool>
dislikes = post["dislikes"] as? [String : Bool] ?? [:]
var dislikeCount = post["dislikeCount"] as? Int ?? 0
if let _ = dislikes[uid] {
// Unstar the post and remove self from stars
dislikeCount -= 1
dislikes.removeValue(forKey: uid)
} else {
// Star the post and add self to stars
dislikeCount += 1
dislikes[uid] = true
}
post["dislikeCount"] = dislikeCount as AnyObject?
post["dislikes"] = dislikes as AnyObject?
// Set value and report transaction success
currentData.value = post
return TransactionResult.success(withValue: currentData)
}
return TransactionResult.success(withValue: currentData)
}) { (error, committed, snapshot) in
if let error = error {
onError(error.localizedDescription)
}
guard let dic = snapshot?.value as? [String: Any] else { return }
guard let postId = snapshot?.key else { return }
let updatePost = PostModel(dictionary: dic, key: postId)
onSuccess(updatePost)
}
}
Thanks for your help!
I think you need to share more information, or any error you see, or debug it and test where the action stops. But try this one maybe it will work.
#objc func handleDidTapDislike() {
guard let postId = post?.id else { return }
PostApi.shared.incrementDislikes(postId: postId, onSuccess: { (post) in
self.updateDislike(post: post)
self.post?.dislikes = post.dislikes
self.post?.isDisliked = post.isDisliked
self.post?.dislikeCount = post.dislikeCount
if post.dislikeCount > 4 {
deletePost()
}
}, onError: { (errorMessage) in
ProgressHUD.showError(errorMessage)
})}
and then edit updateDislike() function
func updateDislike(post: PostModel) {
if post.isDisliked == false || post.dislikes == nil {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-50")
} else {
dislikeImageView.image = UIImage(named: "icons8-gefaellt-nicht-filled-50")
}
}
Im working with NSURLSession. I have an array with restaurants and i'm requesting the dishes for every restaurant in the array to the api. The dataTask works,i'm just having a real hard time trying to call a method only when the all dataTasks are finished.
self.findAllDishesOfRestaurants(self.restaurantsNearMe) { (result) -> Void in
if result.count != 0 {
self.updateDataSourceAndReloadTableView(result, term: "protein")
} else {
print("not ready yet")
}
}
the self.updateDataSourceAndREloadTableView never gets called, regardless of my completion block. Here is my findAllDishesOfRestaurants function
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
self.allDishes.addObjectsFromArray(dishesArray as [AnyObject])
print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray)
})
completion(result:allDishesArray)
}
}
}
And here is my the function where i perform the dataTasks.
func getDishesByRestaurantName(restaurant:Resturant, completion:(result:NSArray) ->Void) {
var restaurantNameFormatted = String()
if let name = restaurant.name {
for charachter in name.characters {
var newString = String()
var sameCharacter:Character!
if charachter == " " {
newString = "%20"
restaurantNameFormatted = restaurantNameFormatted + newString
} else {
sameCharacter = charachter
restaurantNameFormatted.append(sameCharacter)
}
// print(restaurantNameFormatted)
}
}
var urlString:String!
//not to myself, when using string with format, we need to igone all the % marks arent ours to replace with a string, otherwise they will be expecting to be replaced by a value
urlString = String(format:"https://api.nutritionix.com/v1_1/search/%#?results=0%%3A20&cal_min=0&cal_max=50000&fields=*&appId=XXXXXXXXXappKey=XXXXXXXXXXXXXXXXXXXXXXXXXXXX",restaurantNameFormatted)
let URL = NSURL(string:urlString)
let restaurantDishesArray = NSMutableArray()
let session = NSURLSession.sharedSession()
let dataTask = session.dataTaskWithURL(URL!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
do {
let anyObjectFromResponse:AnyObject = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments)
if let asNSDictionary = anyObjectFromResponse as? NSDictionary {
let hitsArray = asNSDictionary.valueForKey("hits") as? [AnyObject]
for newDictionary in hitsArray! as! [NSDictionary]{
let fieldsDictionary = newDictionary.valueForKey("fields") as? NSDictionary
let newDish = Dish.init(dictionary:fieldsDictionary!, restaurant: restaurant)
restaurantDishesArray.addObject(newDish)
}
}
completion(result:restaurantDishesArray)
} catch let error as NSError {
print("failed to connec to api")
print(error.localizedDescription)
}
}
dataTask.resume()
}
Like i said before, I need to wait until the fun findAllDishesOfRestaurants is done. I tried writing my completion blocks but I'm not sure I'm doing it right. Any help is greatly appreciated. Thank
The problem is that you are calling the completion method in findAllDishesOfRestaurants before al tasks are complete. In fact, you are calling it once for each restaurant in the list, which is probably not what you want.
My recommendation would be for you to look into NSOperationQueue for two reasons:
It will let you limit the number of concurrent requests to the server, so your server does not get flooded with requests.
It will let you easily control when all operations are complete.
However, if you are looking for a quick fix, what you need is to use GCD groups dispatch_group_create, dispatch_group_enter, dispatch_group_leave, and dispatch_group_notify as follows.
func findAllDishesOfRestaurants(restaurants:NSArray, completion:(result: NSArray) -> Void) {
let group = dispatch_group_create() // Create GCD group
let allDishesArray:NSMutableArray = NSMutableArray()
for restaurant in restaurants as! [Resturant] {
let currentRestaurant:Resturant? = restaurant
if currentRestaurant == nil {
print("restaurant is nil")
} else {
dispatch_group_enter(group) // Enter group for this restaurant
self.getDishesByRestaurantName(restaurant, completion: { (result) -> Void in
if let dishesArray:NSArray = result {
restaurant.dishes = dishesArray
print(restaurant.dishes?.count)
allDishesArray.addObjectsFromArray(dishesArray as [AnyObject])
// self.allDishes.addObjectsFromArray(dishesArray as [AnyObject]) <-- do not do this
// print(self.allDishes.count)
}
else {
print("not dishes found")
}
// completion(result:allDishesArray) <-- No need for this, remove
dispatch_group_leave(group) // Leave group, marking this restaurant as complete
})
// completion(result:allDishesArray) <-- Do not call here either
}
}
// Wait for all groups to complete
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
completion(result:allDishesArray)
}
}
I'm simply trying to add a '!' when there is a new item and to have it removed once the tabbar is tapped. I'm at a loss of where to put the code in the following.
Essentially when there is a notification or a new conversation created, or an update to an existing conversation, I'd like a ! to pop up on the tabbar badge, and once the user taps said tabbar item the ! goes away.
func conversationViewController(viewController: ATLConversationViewController, didSendMessage message: LYRMessage) {
println("Message sent!")
}
func conversationViewController(viewController: ATLConversationViewController, didFailSendingMessage message: LYRMessage, error: NSError?) {
println("Message failed to sent with error: \(error)")
}
func conversationViewController(viewController: ATLConversationViewController, didSelectMessage message: LYRMessage) {
println("Message selected")
}
// MARK - ATLConversationViewControllerDataSource methods
func conversationViewController(conversationViewController: ATLConversationViewController, participantForIdentifier participantIdentifier: String) -> ATLParticipant? {
if (participantIdentifier == PFUser.currentUser()!.objectId!) {
return PFUser.currentUser()!
}
let user: PFUser? = UserManager.sharedManager.cachedUserForUserID(participantIdentifier)
if (user == nil) {
UserManager.sharedManager.queryAndCacheUsersWithIDs([participantIdentifier]) { (participants: NSArray?, error: NSError?) -> Void in
if (participants?.count > 0 && error == nil) {
//self.addressBarController.reloadView()
// TODO: Need a good way to refresh all the messages for the refreshed participants...
self.reloadCellsForMessagesSentByParticipantWithIdentifier(participantIdentifier)
} else {
println("Error querying for users: \(error)")
}
}
}
return user
}
func conversationViewController(conversationViewController: ATLConversationViewController, attributedStringForDisplayOfDate date: NSDate) -> NSAttributedString? {
let attributes: NSDictionary = [ NSFontAttributeName : UIFont.systemFontOfSize(14), NSForegroundColorAttributeName : UIColor.grayColor() ]
return NSAttributedString(string: self.dateFormatter.stringFromDate(date), attributes: attributes as? [String : AnyObject])
}
func conversationViewController(conversationViewController: ATLConversationViewController, attributedStringForDisplayOfRecipientStatus recipientStatus: [NSObject:AnyObject]) -> NSAttributedString? {
if (recipientStatus.count == 0) {
return nil
}
let mergedStatuses: NSMutableAttributedString = NSMutableAttributedString()
let recipientStatusDict = recipientStatus as NSDictionary
let allKeys = recipientStatusDict.allKeys as NSArray
allKeys.enumerateObjectsUsingBlock { participant, _, _ in
let participantAsString = participant as! String
if (participantAsString == self.layerClient.authenticatedUserID) {
return
}
let checkmark: String = "✔︎"
var textColor: UIColor = UIColor.lightGrayColor()
let status: LYRRecipientStatus! = LYRRecipientStatus(rawValue: recipientStatusDict[participantAsString]!.unsignedIntegerValue)
switch status! {
case .Sent:
textColor = UIColor.lightGrayColor()
case .Delivered:
textColor = UIColor.orangeColor()
case .Read:
textColor = UIColor.greenColor()
default:
textColor = UIColor.lightGrayColor()
}
let statusString: NSAttributedString = NSAttributedString(string: checkmark, attributes: [NSForegroundColorAttributeName: textColor])
mergedStatuses.appendAttributedString(statusString)
}
return mergedStatuses;
}
// MARK - ATLAddressBarViewController Delegate methods methods
// MARK - ATLParticipantTableViewController Delegate Methods
func participantTableViewController(participantTableViewController: ATLParticipantTableViewController, didSelectParticipant participant: ATLParticipant) {
println("participant: \(participant)")
self.addressBarController.selectParticipant(participant)
println("selectedParticipants: \(self.addressBarController.selectedParticipants)")
self.navigationController!.dismissViewControllerAnimated(true, completion: nil)
}
func participantTableViewController(participantTableViewController: ATLParticipantTableViewController, didSearchWithString searchText: String, completion: ((Set<NSObject>!) -> Void)?) {
UserManager.sharedManager.queryForUserWithName(searchText) { (participants, error) in
if (error == nil) {
if let callback = completion {
callback(NSSet(array: participants as! [AnyObject]) as Set<NSObject>)
}
} else {
println("Error search for participants: \(error)")
}
}
}
}�
Solved https://stackoverflow.com/a/29837976/2303865
Let's say that you have queried the number of messages into a variable called counts then to show the this count into the first tabBarItem.
var arrayOfTabBar = self.tabBarController?.tabBar.items as NSArray!
let tabItem = arrayOfTabBar.objectAtIndex(1) as! UITabBarItem
tabItem.badgeValue = counts