How to search by API OpenWeatherApi city? - swift

I created these requests, I don’t know if I asked them correctly. If someone can tell, they will be grateful. Here is my code. I need to search for a city in the search and so that the weather is displayed in the table. Please help and tell me how to set and where to fix it. Or did I set the functions and methods incorrectly? error
"The data couldn’t be read because it is missing."
import UIKit
class SearchViewController: UIViewController,UISearchResultsUpdating, UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var searchBar: UISearchBar!
#IBOutlet weak var tableView: UITableView!
var filteredCity: [OpenWeather] = []
// var searchCity: [OpenWeather]=[]
let wetherApi = WeatherManager()
var cityWeather = [OpenWeather]()
let netSer = NetworkService()
let searchController = UISearchController()
var isSearchBarEmpty: Bool {
return searchController.searchBar.text?.isEmpty ?? true
}
override func viewDidLoad() {
super.viewDidLoad()
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController
tableView.delegate = self
tableView.dataSource = self
wetherApi.fetchCurrentWeather(city: "London")
}
func updateSearchResults(for searchController: UISearchController) {
self.filteredCity = self.cityWeather.filter { (city:OpenWeather) -> Bool in
if city.city.lowercased().contains(self.searchController.searchBar.text!.lowercased()){
return true
}else {
return false
}
}
//Update the results TableView
self.tableView.reloadData()
}
#objc func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cityWeather.count
}
#objc(tableView:cellForRowAtIndexPath:) internal func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CityTableViewCell
cell.cityTemp.text = cityWeather[indexPath.row].city
cell.cityTemp.text = "\(cityWeather[indexPath.row].main.tempCelsius)"
return cell
}
}
My structure weather:
import Foundation
import UIKit
struct OpenWeather: Codable {
let coord:Coordinate
let city:String
let weathertwo:[WeatherTwo]
let main: Main
}
struct Coordinate: Codable {
let lan:Float
let lot:Float
enum CodingKeys: String, CodingKey {
case lan = "lat"
case lot = "lon"
}
}
struct WeatherTwo: Codable {
let main: String
let description: String
}
struct Main: Codable {
private let temp: Double
var tempCelsius: Double {
get {
return (temp - 32) / 1.8
}
}
private let temp_min: Double
var tempCelsiusmin: Double {
get {
return (temp - 32) / 1.8
}
}
private let temp_max: Double
var tempCelsiusmax: Double {
get {
return (temp - 32) / 1.8
}
}
let pressure: Int
let humidity: Int
}
My code Api codable download:
struct WeatherManager {
//Deliberately deleted "q=London" within the url, because it needs to be customizable with a different city
func fetchCurrentWeather(city: String){
let URL_API = "https://api.openweathermap.org/data/2.5/weather?q="
let CITY_ID = city
//let URL_API_KEY = "<api_key>"
let session = URLSession(configuration: .default)
let urlString = URL_API + CITY_ID + "&appid=cfe547d810fc4ad95e8f24187c6b08da"
guard let url = URL(string: urlString) else {
print("Error building URL")
return
}
let task = session.dataTask(with: url) { (data, response, error) in
DispatchQueue.main.async {
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data, let response = response as? HTTPURLResponse else {
print("Invalid data or response")
return
}
do {
if response.statusCode == 200 {
let items = try JSONDecoder().decode(OpenWeather.self, from: data)
print(items)
} else {
print("Response wasn't 200. It was: " + "\n\(response.statusCode)")
}
} catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}

I see a few reasons this happens.
If you look at the response the API gives you, you will find that:
The keys weathertwo and city do not exist in the response, yet they are present in your OpenWeather struct. You need to make them optional or provide your default values by overriding the init if you don't always expect this data to be present
Have a look at the coord object in the response
"coord": {
"lon": -0.1257,
"lat": 51.5085
}
Compare the spelling of keys with your coord struct:
struct Coordinate: Codable {
let lan:Float
let long:Float
}
If you want to use different variable names, you need to look at using CodingKeys
However, the simplest fix I believe is changing your OpenWeather struct to this:
struct OpenWeather: Codable {
let coord: Coordinate
let city: String?
let weathertwo: [WeatherTwo]?
let main: Main
}
and fixing coords to this:
struct Coordinate: Codable {
let lat: Float
let lon: Float
}
should get rid of the error and give you the data you need as I can see data from the Open Weather API printed on the console:
OpenWeather(coord: TestApp.Coordinate(lat: 25.2582, lon: 55.3047), city: nil, weathertwo: nil, main: TestApp.Main(temp: 300.38, temp_min: 299.31, temp_max: 301.29, pressure: 1008, humidity: 54))
There are other improvements that could be made such as using camel case for the variables with CodingKeys but that is something you can decide later.
Give this a go and see if it resolves your issue.
Update
As you rightly pointed out, you asked how to get the list of cities supported by Open Weather.
I had a quick look, it does not look like they have an API to get a list of all the cities. Their API seems to support you sending a city name and it will send back to you the weather data for that city.
However, they provide their list of cities here
I think the file history.city.list.json.gz has all the city data which might need to be included in your app or somewhere online if you wish.
Have a look.

Related

How to load data in the collectionview from JSON to Swift by using Alamofire and Kingfisher

I need to get the data from already made JSON file to the Swift. By using MVVM desing I wrote this code in the Repo class
func loadFoods() {
AF.request("http://example.com/foods/getAllFoods.php",method: .get).response { response in
if let data = response.data {
do{
let result = try JSONDecoder().decode(ProductsResponse.self, from: data)
if let list = result.foods {
self.foodList.onNext(list)
}
}catch{
print(error.localizedDescription)
}
}
}
}
Here's the code from View Model class:
class HomeViewModel {
var foodList = BehaviorSubject <[Foods]>(value: [Foods]())
var frepo = FoodsDaoRepository()
init() {
loadFoods()
foodList = frepo.foodList
}
func loadFoods() {
frepo.loadFoods()
}
func loadPersons(){
prepo.loadPersons()
}
and I wrote this code in the ViewContoller class:
override func viewDidLoad() {
super.viewDidLoad()
searchTextField.delegate = self
collectionView.delegate = self
collectionView.dataSource = self
let _ = viewModel.foodList.subscribe(onNext: { list in
self.foodsList = list
DispatchQueue.main.async {
self.collectionView.reloadData()
}
})
override func viewWillAppear(_ animated: Bool) {
viewModel.loadFoods()
}
Foodlist variable takes the data from Food class:
class Foods: Codable {
var id : Int?
var name : String?
var image : String?
var price : Int?
var category : String?
init(id: Int?, name: String?, image: String?, price: Int?, category: String?) {
self.id = id
self.name = name
self.image = image
self.price = price
self.category = category
}
}
But it didn't help to get the data from JSON to the CollectionView. It just shows empty collection view cells.
Also how can I get an image by using Kingfisher and Alamofire?
I tried to explain the problem and wrote a code to expand my question

How to get value from the first 3 rows in TableView using swift?

I retrieve data from MySql via PHP file to get users information and scores to load them in a table. I need to get the value of the first 3 users and put them in a Label outside the Table, it is like game leaders list. I attached an image to explain the idea.
Here is the structure code:
import Foundation
protocol HomeModelProtocol: AnyObject {
func itemsDownloaded(items: NSArray)
}
class HomeModel: NSObject, URLSessionDataDelegate {
weak var delegate: HomeModelProtocol!
let urlPath = "https://mywebsite.com/folder/callUserList.php" //this will be changed to the path where service.php lives
func downloadItems() {
let url: URL = URL(string: urlPath)!
let defaultSession = Foundation.URLSession(configuration: URLSessionConfiguration.default)
let task = defaultSession.dataTask(with: url) { (data, response, error) in
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON(data!)
}
}
task.resume()
}
func parseJSON(_ data:Data) {
var jsonResult = NSArray()
do{
jsonResult = try JSONSerialization.jsonObject(with: data, options:JSONSerialization.ReadingOptions.allowFragments) as! NSArray
} catch let error as NSError {
print(error)
}
var jsonElement = NSDictionary()
let users = NSMutableArray()
for i in 0 ..< jsonResult.count
{
jsonElement = jsonResult[i] as! NSDictionary
let user = UsersModel()
//the following insures none of the JsonElement values are nil through optional binding
if let name = jsonElement["name"] as? String,
let email = jsonElement["email"] as? String,
let phoneNumber = jsonElement["phone"] as? String,
let userImage = jsonElement["image"] as? String
{
user.name = name
user.email = email
user.phoneNumber = phoneNumber
user.userImage = userImage
}
users.add(user)
}
DispatchQueue.main.async(execute: { () -> Void in
self.delegate.itemsDownloaded(items: users)
})
}
}
Here is the model:
import Foundation
class UsersModel: NSObject {
//properties
var name: String?
var email: String?
var phoneNumber: String?
var userImage: String?
//empty constructor
override init()
{
}
//construct with #name, #address, #latitude, and #longitude parameters
init(name: String, email: String, phoneNumber: String, userImage: String) {
self.name = name
self.email = email
self.phoneNumber = phoneNumber
self.userImage = userImage
}
//prints object's current state
override var description: String {
return "Name: \(String(describing: name)), Email: \(String(describing: email)), Phone Number: \(String(describing: phoneNumber)), User Image: \(String(describing: userImage))"
}
}
Here is the code in the TableView controller:
var feedItems: NSArray = NSArray()
override func viewDidLoad() {
super.viewDidLoad()
let homeModel = HomeModel()
homeModel.delegate = self
homeModel.downloadItems()
}
func itemsDownloaded(items: NSArray) {
feedItems = items
self.listTableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of feed items
return feedItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Retrieve cell
let cellIdentifier: String = "BasicCell"
let myCell: WinnerTableCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as! WinnerTableCell
// Get the location to be shown
let item: UsersModel = feedItems[indexPath.row] as! UsersModel
// Get references to labels of cell
myCell.lbTextName!.text = item.name
return myCell
}
The data shows in the Table but I have no idea how to fill the 3 label with the 3 first users from the Table.
How can I get these values from the table and pass it to a label in the same ViewController?
Thanks
When adding this code:
if feedItems.count >= 3 {
lblFirstWinner.text = feedItems[0].name // 1st winner
lblSecondWinner.text = feedItems[1].name // 2nd winner
lblThirdWinner.text = feedItems[2].name // 3rd winner
}
it shows error: Value of type 'Any' has no member 'name'
Change itemsDownloaded method as
func itemsDownloaded(items: NSArray) {
feedItems = items
self.listTableView.reloadData()
for (index, user) in items.enumerated() {
let user = user as! UserModel
switch index {
case 0: // 1st winner
lblFirstWinner.text = user.name
case 1: // 2nd winner
lblSecondWinner.text = user.name
case 2: // 3rd winner
lblThirdWinner.text = user.name
}
}
}
OR
Change your HomeModelProtocol method and feedItems type to [UsersModel]
protocol HomeModelProtocol: AnyObject {
func itemsDownloaded(items: [UsersModel]) // Changed
}
var feedItems =[UsersModel]() // Changed
override func viewDidLoad() {
super.viewDidLoad()
let homeModel = HomeModel()
homeModel.delegate = self
homeModel.downloadItems()
}
func itemsDownloaded(items: [UsersModel]) {
feedItems = items
self.listTableView.reloadData()
if feedItems.count >= 3 {
lblFirstWinner.text = feedItems[0].name // 1st winner
lblSecondWinner.text = feedItems[1].name // 2nd winner
lblThirdWinner.text = feedItems[2].name // 3rd winner
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// Return the number of feed items
return feedItems.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Retrieve cell
let cellIdentifier: String = "BasicCell"
let myCell: WinnerTableCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) as! WinnerTableCell
// Get references to labels of cell
myCell.lbTextName!.text = feedItems[indexPath.row].name // Changed
return myCell
}
Just you need to add a few lines in the below function and your solution will be done.
func itemsDownloaded(items: NSArray) {
feedItems = items
self.listTableView.reloadData()
if feedItems.count >= 3 {
lblFirstWinner.text = feedItems[0].name // 1st winner
lblSecondWinner.text = feedItems[1].name // 2nd winner
lblThirdWinner.text = feedItems[2].name // 3rd winner
}
}
Let me know... is it working for you? and please also refer to #vadian comment on your question.

Why is my Firestore data not displaying as table view?

I am currently working on a project with Xcode (User Interface: Storyboard) and Google Firebase. I cannot get my app to display the information like this:
Desired output
The way I got this picture was by starting a test project selecting SwiftUI as my User Interface, and only having ONE single view controller. In my app a user will only arrive to this page after they have logged in and click a button that takes them to the table view.
My app currently prints the result at the bottom of Xcode:
Current output
import UIKit
import FirebaseFirestore
class AssetViewController: UIViewController {
#IBOutlet weak var assetList: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let db = Firestore.firestore()
db.collection("assets").getDocuments { (snapshot, error) in
if error != nil {
print("error")
} else {
for document in (snapshot?.documents)! {
if let brand = document.data() ["brand"] as? String {
if let description = document.data() ["description"] as? String {
if let date = document.data() ["date"] as? String {
if let cost = document.data() ["cost"] as? String {
if let serial = document.data() ["serial"] as? String {
if let warranty = document.data() ["warranty"] as? String {
if let warranty_cost = document.data() ["warranty_cost"] as? String {
print("\(document.documentID) => \(document.data())") }
}
}
}
}
}
}
}
}
}
}
}
I have the following class:
import Foundation
import FirebaseFirestore
class AssetsViewModel: ObservableObject {
#Published var assets = [Asset] ()
private var db = Firestore.firestore()
func fetchData() {
db.collection("assets").addSnapshotListener { (QuerySnapshot, error) in
guard let documents = QuerySnapshot?.documents else {
print("No assets in here")
return
}
self.assets = documents.map { (QueryDocumentSnapshot) -> Asset in
let data = QueryDocumentSnapshot.data()
let brand = data["brand"] as? String ?? ""
let description = data["description"] as? String ?? ""
let date = data["date"] as? String ?? ""
let cost = data["cost"] as? String ?? ""
let serial = data["serial"] as? String ?? ""
let warranty = data["warranty"] as? String ?? ""
let warranty_cost = data["warranty_cost"] as? String ?? ""
return Asset(brand: brand, description: description, date: date, cost: cost, serial: serial, warranty: warranty, warranty_cost: warranty_cost)
}
}
}
}
And I have the following structure:
import Foundation
struct Asset: Identifiable {
var id: String = UUID().uuidString
var brand: String
var description: String
var date: String
var cost: String
var serial: String
var warranty: String
var warranty_cost: String
}
My main goal is to display the information like the first picture. I would appreciate any help given.
This is the code that I used to display the first picture:
import SwiftUI
struct ContentView: View {
#ObservedObject private var viewModel = AssetsViewModel()
var body: some View {
NavigationView {
List(viewModel.assets) { asset in
VStack(alignment: .leading) {
Text(asset.brand)
.font(.headline)
Text(asset.description)
.font(.subheadline)
Text(asset.date)
.font(.subheadline)
Text(asset.cost)
.font(.subheadline)
Text(asset.serial)
.font(.subheadline)
Text(asset.warranty)
.font(.subheadline)
Text(asset.warranty_cost)
.font(.subheadline)
}
}
.navigationBarTitle("Assets")
.onAppear() {
self.viewModel.fetchData()
}
}
}
}
struct AssetViewSwiftUIView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The post below helped me. The error I was getting was because Swift did not like me naming the ViewTable "assetList". Once I changed the name to "tableView" and changed in the code, it worked well. I added extra code to make the cells auto adjust from here: why UITableViewAutomaticDimension not working?.
Thank you very much for your help!
You need something like this:
import Foundation
import UIKit
import FirebaseFirestore
class myTableCell: UITableViewCell {
#IBOutlet weak var brand: UILabel!
#IBOutlet weak var description: UILabel!
#IBOutlet weak var date: UILabel!
#IBOutlet weak var cost: UILabel!
#IBOutlet weak var serial: UILabel!
#IBOutlet weak var warranty: UILabel!
#IBOutlet weak var warrantyCost: UILabel!
}
class IndexAssets {
var brand = ""
var description = ""
var date = ""
var cost = ""
var serial = ""
var warranty = ""
var warrantyCost = ""
init(brand: String, description: String, date: String, cost: String, serial: String, warranty: String, warrantyCost: String)
{
self.brand = brand
self.description = description
self.date = date
self.cost = cost
self.serial = serial
self.warranty = warranty
self.warrantyCost = warrantyCost
}
}
class AssetViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var assetList: UITableView!
var dataArray: [IndexAssets] = [IndexAssets]()
override func viewDidLoad() {
super.viewDidLoad()
downloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! myTableCells
cell.brand?.text = dataArray[indexPath.row].brand
cell.description?.text = dataArray[indexPath.row].description
cell.date?.text = dataArray[indexPath.row].date
cell.cost?.text = dataArray[indexPath.row].cost
cell.serial?.text = dataArray[indexPath.row].serial
cell.warranty?.text = dataArray[indexPath.row].warranty
cell.warrantyCost?.text = dataArray[indexPath.row].warrantyCost
return cell
}
func downloadData()
{
let db = Firestore.firestore()
db.collection("assets").getDocuments { (snapshot, error) in
if error != nil
{
print("error")
}
else
{
for document in (snapshot?.documents)!
{
let brand = document.data() ["brand"] as? String
let description = document.data() ["description"] as? String
let date = document.data() ["date"] as? String
let cost = document.data() ["cost"] as? String
let serial = document.data() ["serial"] as? String
let warranty = document.data() ["warranty"] as? String
let warrantyCost = document.data() ["warranty_cost"] as? String
if let brand = brand, let description = description, let date = date, let cost = cost, let serial = serial, let warranty = warranty, let warrantyCost = warrantyCost
{
let insert = IndexAssets(brand: brand, description: description, date: date, cost: cost, serial: serial, warranty: warranty, warrantyCost: warrantyCost)
self.dataArray.append(insert)
}
}
self.assetList.reloadData()
}}
}
}
Also, remember in Storyboard to:
Assign "cell" to your Dynamic Cell identifier
Select myTableCell as Class for that cell
Drag labels from the class to connect them to storyboard
Drag your table to your View Controller (Yellow Icon on top) and select DataSource and Delegate.

How i implement my JSON API Request into a UITableViewCell?

I have a problem with my current Project. First of all, i like to implement a JSON API Request that allows me to get a title off a URL. The Problem: I want to display the JSON data into a UITableViewCell.
But Xcode throws following Error:
Cannot assign value of type 'FirstViewController.Title' to type
'String?'
Maybe there is more wrong in my code, because i'm just a beginner at Swift/Xcode
I already tried this:
cell.textLabel?.text = course.title as? String
But i got warning message as follows:
Cast from 'FirstViewController.Title' to unrelated type 'String' always fails
This is my code sample:
var courses = [Course]()
let cell = "ItemCell"
override func viewDidLoad() {
super.viewDidLoad()
fetchJSON()
}
struct Course: Codable {
let title: Title
enum CodingKeys: String, CodingKey {
case title
case links = "_links"
}
}
struct Links: Codable {
}
struct Title: Codable {
let rendered: String
}
fileprivate func fetchJSON() {
let urlString = "ExampleURL"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, err) in
DispatchQueue.main.async {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let result = try JSONDecoder().decode(Course.self, from: data)
self.tableView.reloadData()
} catch let jsonErr {
print("Failed to decode:", jsonErr)
}
}
}.resume()
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return courses.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .value1, reuseIdentifier: "ItemCell")
let course = courses[indexPath.row]
cell.textLabel?.text = course.title as? String // Cast from 'FirstViewController.Title' to unrelated type 'String' always fails
return cell
}
I just want to get WordPress posts into a UITableView - UITableViewCell.
Maybe you can tell me if its the wrong way i tried it but i don't really know how i solve this problem
Thank you in advance
Assign the var before the reload
let res = try JSONDecoder().decode(Course.self, from: data)
courses.append(res)
DispatchQueue.main.async {
self.tableView.reloadData()
}
And set it to the string value
cell.textLabel?.text = course.title.rendered
courses = try JSONDecoder().decode([Course].self, from: data)
print(courses)

UITableview crashes when scrolling up in Swift

I am having this crash when trying to scroll up my tableview. My array is not nil. Why does it crash every time I try to scroll up? I am trying to display data from Core Data.
Here is my code:
var product = [NSManagedObject]()
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return product.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cellIdentifier = "CheckOutTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! CheckOutTableViewCell
let item = product[indexPath.row]
** It crashes here, every time I try to scroll up my table view
cell.productTitle.text = item.valueForKey("name") as! String
cell.productDescription.text = item.valueForKey("size") as! String
return cell
}
func fetch() {
let moc = DataController().managedObjectContext
let productFetch = NSFetchRequest(entityName: "Product")
do {
let fetchedResults: [NSManagedObject] = try moc.executeFetchRequest(productFetch) as! [Product]
if let results: [NSManagedObject] = fetchedResults {
product = results
print("results:\(results.count)")
}
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
}
Product.swift
import Foundation
import CoreData
class Product: NSManagedObject {
}
Product+CoreDataProperties.swift
import Foundation
import CoreData
extension Product {
#NSManaged var id: String?
#NSManaged var name: String?
#NSManaged var img: String?
#NSManaged var quantity: String?
#NSManaged var size: String?
#NSManaged var price: String?
#NSManaged var promo: String?
}
Crash Screenshot
You should check the size of the array before getting your product, and use optional binding (if let) to check that the value exists in the dictionary:
if indexPath.row < product.count {
let item = product[indexPath.row]
if let name = item.valueForKey("name") as? String {
cell.productTitle.text = name
}
if let size = item.valueForKey("size") as? String {
cell.productDescription.text = size
}
}
To write safer code, it's a good idea to avoid force unwrapping when possible.
In this case, given that you have a Product class for your model, you should store your products as [Product], you can then avoid using valueForKey:
if indexPath.row < product.count {
let item = product[indexPath.row]
if let name = item.name as? String {
cell.productTitle.text = name
}
if let size = item.size as? String {
cell.productDescription.text = size
}
}