How to pass data to a sub view from ContentView in SwiftUI - ios

Forgive me if this doesn't make sense, I am a total beginner at Swift. I am creating a recipe app that pulls data from an API and lists it out in navigation links. When the user clicks on the recipe I want it to move to sub view and display information from the API such as recipe name, image, ingredients, and have a button with a link to the webpage.
I was able to get the data pulled into the list with navigation links. However, now I do not know how to go about setting up the recipe details sub view with all of the information I listed above.
This is where I call the API:
class RecipeService {
func getRecipes(_ completion: #escaping (Result<[Recipe], Error>) -> ()) {
let url = URL(string: "http://www.recipepuppy.com/api")!
URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error {
return completion(.failure(error))
}
guard let data = data else {
return completion(.failure(NSError(domain: "", code: -1, userInfo: nil)))
}
do {
let response = try JSONDecoder().decode(RecipesResponses.self, from: data)
completion(.success(response.results))
} catch {
completion(.failure(error))
}
}.resume()
}
}
This is where I take in the recipe responses:
struct RecipesResponses: Codable {
let title: String
let version: Double
let href: String
let results: [Recipe]
}
struct Recipe: Codable {
let title, href, ingredients, thumbnail: String
var detail: URL {
URL(string: href)!
}
var thumb: URL {
URL(string: thumbnail)!
}
}
This is my recipe ViewModel:
class RecipeViewModel: ObservableObject {
#Published var recipes = [Recipe]()
#Published var isLoading = false
private let service = RecipeService()
init() {
loadData()
}
private func loadData() {
isLoading = true
service.getRecipes{ [weak self] result in
DispatchQueue.main.async {
self?.isLoading = false
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let recipes):
self?.recipes = recipes
}
}
}
}
}
This is my view where I list out the API responses:
struct ListView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
NavigationView {
List(viewModel.recipes, id: \.href) { recipe in
NavigationLink (destination: RecipeDetailView()) {
HStack{
CachedImageView(recipe.thumb)
.mask(Circle())
.frame(width: 80)
VStack(alignment: .leading) {
Text(recipe.title)
.font(.largeTitle)
.foregroundColor(.black)
.padding()
}
}
.buttonStyle(PlainButtonStyle())
}
}
.navigationBarTitle(Text("All Recipes"))
}
}
}
struct ListView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
This is the view where I would like to list out the recipe details and link to the webpage. This is where I am struggling to be able to pull the API data into:
struct RecipeDetailView: View {
#ObservedObject var viewModel = RecipeViewModel()
var body: some View {
Text("Detail View")
}
}
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView()
}
}
Images of app

You can change RecipeDetailView to accept a Recipe as a parameter:
struct RecipeDetailView: View {
var recipe : Recipe
var body: some View {
Text(recipe.title)
Link("Webpage", destination: recipe.detail)
//etc
}
}
Then, in your NavigationLink, pass the Recipe to it:
NavigationLink(destination: RecipeDetailView(recipe: recipe)) {
One thing I'd warn you about is force unwrapping the URLs in Recipe using ! -- you should know that if you ever get an invalid/malformed URL, this style of unwrapping will crash the app.
Update, to show you what the preview might look like:
struct RecipeDetailView_Previews: PreviewProvider {
static var previews: some View {
RecipeDetailView(recipe: Recipe(title: "Recipe name", href: "https://google.com", ingredients: "Stuff", thumbnail: "https://linktoimage.com"))
}
}

Related

Where to make an API call to populate a view with data

I am trying to create a view that displays results from an API call, however I keep on running into multiple errors.
My question is basically where is the best place to make such an API call.
Right now I am "trying" to load the data in the "init" method of the view like below.
struct LandingView: View {
#StateObject var viewRouter: ViewRouter
#State var user1: User
#State var products: [Product] = []
init(_ viewRouter : ViewRouter, user: User) {
self.user1 = user
_viewRouter = StateObject(wrappedValue: viewRouter)
ProductAPI().getAllProducts { productArr in
self.products = productArr
}
}
var body: some View {
tabViewUnique(prodArrParam: products)
}
}
I keep on getting an "escaping closure mutating self" error, and while I could reconfigure the code to stop the error,I am sure that there is a better way of doing what I want.
Thanks
struct ContentView: View {
#State var results = [TaskEntry]()
var body: some View {
List(results, id: \.id) { item in
VStack(alignment: .leading) {
Text(item.title)
}
// this one onAppear you can use it
}.onAppear(perform: loadData)
}
func loadData() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos") else {
print("Your API end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode([TaskEntry].self, from: data) {
DispatchQueue.main.async {
self.results = response
}
return
}
}
}.resume()
}
}
In .onAppear you can make api calls

Encoding to JSON format is not encoding the toggled boolean value in Swift

I am making an app that has information about different woods, herbs and spices, and a few other things. I am including the ability to save their favorite item to a favorites list, so I have a heart button that the user can press to add it to the favorites. Pressing the button toggles the isFavorite property of the item and then leaving the page calls a method that encodes the data to save it to the user's device. The problem that I am running into is that it is not encoding the updated value of the isFavorite property. It is still encoding the value as false, so the favorites list is not persisting after closing and reopening the app.
Here is my Wood.swift code, this file sets up the structure for Wood items. I also included the test data that I was using to make sure that it displayed properly in the Wood extension:
import Foundation
struct Wood: Identifiable, Codable {
var id = UUID()
var mainInformation: WoodMainInformation
var preparation: [Preparation]
var isFavorite = false
init(mainInformation: WoodMainInformation, preparation: [Preparation]) {
self.mainInformation = mainInformation
self.preparation = preparation
}
}
struct WoodMainInformation: Codable {
var category: WoodCategory
var description: String
var medicinalUses: [String]
var magicalUses: [String]
var growZone: [String]
var lightLevel: String
var moistureLevel: String
var isPerennial: Bool
var isEdible: Bool
}
enum WoodCategory: String, CaseIterable, Codable {
case oak = "Oak"
case pine = "Pine"
case cedar = "Cedar"
case ash = "Ash"
case rowan = "Rowan"
case willow = "Willow"
case birch = "Birch"
}
enum Preparation: String, Codable {
case talisman = "Talisman"
case satchet = "Satchet"
case tincture = "Tincture"
case salve = "Salve"
case tea = "Tea"
case ointment = "Ointment"
case incense = "Incense"
}
extension Wood {
static let woodTypes: [Wood] = [
Wood(mainInformation: WoodMainInformation(category: .oak,
description: "A type of wood",
medicinalUses: ["Healthy", "Killer"],
magicalUses: ["Spells", "Other Witchy Stuff"],
growZone: ["6A", "5B"],
lightLevel: "Full Sun",
moistureLevel: "Once a day",
isPerennial: false,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet]),
Wood(mainInformation: WoodMainInformation(category: .pine,
description: "Another type of wood",
medicinalUses: ["Healthy"],
magicalUses: ["Spells"],
growZone: ["11G", "14F"],
lightLevel: "Full Moon",
moistureLevel: "Twice an hour",
isPerennial: true,
isEdible: true),
preparation: [Preparation.incense, Preparation.satchet])
]
}
Here is my WoodData.swift file, this file contains methods that allow the app to display the correct wood in the list of woods, as well as encode, and decode the woods:
import Foundation
class WoodData: ObservableObject {
#Published var woods = Wood.woodTypes
var favoriteWoods: [Wood] {
woods.filter { $0.isFavorite }
}
func woods(for category: WoodCategory) -> [Wood] {
var filteredWoods = [Wood]()
for wood in woods {
if wood.mainInformation.category == category {
filteredWoods.append(wood)
}
}
return filteredWoods
}
func woods(for category: [WoodCategory]) -> [Wood] {
var filteredWoods = [Wood]()
filteredWoods = woods
return filteredWoods
}
func index(of wood: Wood) -> Int? {
for i in woods.indices {
if woods[i].id == wood.id {
return i
}
}
return nil
}
private var dataFileURL: URL {
do {
let documentsDirectory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
return documentsDirectory.appendingPathComponent("evergreenData")
}
catch {
fatalError("An error occurred while getting the url: \(error)")
}
}
func saveWoods() {
if let encodedData = try? JSONEncoder().encode(woods) {
do {
try encodedData.write(to: dataFileURL)
let string = String(data: encodedData, encoding: .utf8)
print(string)
}
catch {
fatalError("An error occurred while saving woods: \(error)")
}
}
}
func loadWoods() {
guard let data = try? Data(contentsOf: dataFileURL) else { return }
do {
let savedWoods = try JSONDecoder().decode([Wood].self, from: data)
woods = savedWoods
}
catch {
fatalError("An error occurred while loading woods: \(error)")
}
}
}
Finally, this is my WoodsDetailView.swift file, this file displays the information for the wood that was selected, as well as calls the method that encodes the wood data:
import SwiftUI
struct WoodsDetailView: View {
#Binding var wood: Wood
#State private var woodsData = WoodData()
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses.indices, id: \.self) { index in
let medicinalUse = wood.mainInformation.medicinalUses[index]
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses.indices, id: \.self) { index in
let magicalUse = wood.mainInformation.magicalUses[index]
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone.indices, id: \.self) { index in
let zone = wood.mainInformation.growZone[index]
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
wood.isFavorite.toggle()
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
#State static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: $wood)
}
}
This is my MainTabView.swift file:
import SwiftUI
struct MainTabView: View {
#StateObject var woodData = WoodData()
var body: some View {
TabView {
NavigationView {
List {
WoodsListView(viewStyle: .allCategories(WoodCategory.allCases))
}
}
.tabItem { Label("Main", systemImage: "list.dash")}
NavigationView {
List {
WoodsListView(viewStyle: .favorites)
}
.navigationTitle("Favorites")
}.tabItem { Label("Favorites", systemImage: "heart.fill")}
}
.environmentObject(woodData)
.onAppear {
woodData.loadWoods()
}
.preferredColorScheme(.dark)
}
}
struct MainTabView_Previews: PreviewProvider {
static var previews: some View {
MainTabView()
}
}
This is my WoodListView.swift file:
import SwiftUI
struct WoodsListView: View {
#EnvironmentObject private var woodData: WoodData
let viewStyle: ViewStyle
var body: some View {
ForEach(woods) { wood in
NavigationLink(wood.mainInformation.category.rawValue, destination: WoodsDetailView(wood: binding(for: wood)))
}
}
}
extension WoodsListView {
enum ViewStyle {
case favorites
case singleCategory(WoodCategory)
case allCategories([WoodCategory])
}
private var woods: [Wood] {
switch viewStyle {
case let .singleCategory(category):
return woodData.woods(for: category)
case let .allCategories(category):
return woodData.woods(for: category)
case .favorites:
return woodData.favoriteWoods
}
}
func binding(for wood: Wood) -> Binding<Wood> {
guard let index = woodData.index(of: wood) else {
fatalError("Wood not found")
}
return $woodData.woods[index]
}
}
struct WoodsListView_Previews: PreviewProvider {
static var previews: some View {
WoodsListView(viewStyle: .singleCategory(.ash))
.environmentObject(WoodData())
}
}
Any assistance into why it is not encoding the toggled isFavorite property will be greatly appreciated.
Your problem is that structs are value types in Swift. Essentially this means that the instance of Wood that you have in WoodsDetailView is not the same instance that is in your array in your model (WoodData); It is a copy (Technically, the copy is made as soon as you modify the isFavourite property).
In SwiftUI it is important to maintain separation of responsibilities between the view and the model.
Changing the favourite status of a Wood is something the view should ask the model to do.
This is where you have a second issue; In your detail view you are creating a separate instance of your model; You need to refer to a single instance.
You have a good start; you have put your model instance in the environment where views can access it.
First, change the detail view to remove the binding, refer to the model from the environment and ask the model to do the work:
struct WoodsDetailView: View {
var wood: Wood
#EnvironmentObject private var woodsData: WoodData
var body: some View {
VStack {
List {
Section(header: Text("Description")) {
Text(wood.mainInformation.description)
}
Section(header: Text("Preparation Techniques")) {
ForEach(wood.preparation, id: \.self) { technique in
Text(technique.rawValue)
}
}
Section(header: Text("Edible?")) {
if wood.mainInformation.isEdible {
Text("Edible")
}
else {
Text("Not Edible")
}
}
Section(header: Text("Medicinal Uses")) {
ForEach(wood.mainInformation.medicinalUses, id: \.self) { medicinalUse in
Text(medicinalUse)
}
}
Section(header: Text("Magical Uses")) {
ForEach(wood.mainInformation.magicalUses, id: \.self) { magicalUse in
Text(magicalUse)
}
}
Section(header: Text("Grow Zone")) {
ForEach(wood.mainInformation.growZone, id: \.self) { zone in
Text(zone)
}
}
Section(header: Text("Grow It Yourself")) {
Text("Water: \(wood.mainInformation.moistureLevel)")
Text("Needs: \(wood.mainInformation.lightLevel)")
if wood.mainInformation.isPerennial {
Text("Perennial")
}
else {
Text("Annual")
}
}
}
}
.navigationTitle(wood.mainInformation.category.rawValue)
.onDisappear {
woodsData.saveWoods()
}
.toolbar {
ToolbarItem {
HStack {
Button(action: {
self.woodsData.toggleFavorite(for: wood)
}) {
Image(systemName: wood.isFavorite ? "heart.fill" : "heart")
}
}
}
}
}
}
struct WoodsDetailView_Previews: PreviewProvider {
static var wood = Wood.woodTypes[0]
static var previews: some View {
WoodsDetailView(wood: wood)
}
}
I also got rid of the unnecessary use of indices when listing the properties.
Now, add a toggleFavorite function to your WoodData object:
func toggleFavorite(for wood: Wood) {
guard let index = self.woods.firstIndex(where:{ $0.id == wood.id }) else {
return
}
self.woods[index].isFavorite.toggle()
}
You can also remove the index(of wood:Wood) function (which was really just duplicating Array's firstIndex(where:) function) and the binding(for wood:Wood) function.
Now, not only does your code do what you want, but you have hidden the mechanics of toggling a favorite from the view; It simply asks for the favorite status to be toggled and doesn't need to know what this actually involves.

How to setup NavigationLink inside SwiftUI list

I am attempting to set up a SwiftUI weather app. when the user searches for a city name in the textfield then taps the search button, a NavigationLink list item should appear in the list. Then, the user should be able to click the navigation link and re-direct to a detail view. My goal is to have the searched navigation links to populate a list. However, my search cities are not populating in the list, and I'm not sure why. In ContentView, I setup a list with a ForEach function that passes in cityNameList, which is an instance of the WeatherViewModel. My expectation is that Text(city.title) should display as a NavigationLink list item. How should I configure the ContentView or ViewModel to populate the the list with NavigationLink list items? See My code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityWeather, id: \.id) { city in
NavigationLink(destination: DetailView(detail: viewModel)) {
HStack {
Text(city.cityWeather.name)
.font(.system(size: 32))
}
}
}
}
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error)
}
}
task.resume()
}
}
Model
import Foundation
struct WeatherModel: Identifiable, Codable {
var id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
DetailView
import SwiftUI
struct DetailView: View {
var detail: WeatherViewModel
var body: some View {
VStack(spacing: 20) {
Text(detail.cityWeather.name)
.font(.system(size: 32))
Text("\(detail.cityWeather.main.temp)")
.font(.system(size: 44))
Text(detail.cityWeather.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherViewModel.init())
}
}
try something like this example code, works well for me:
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city)) {
HStack {
Text(city.name).font(.system(size: 32))
}
}
}
}
Spacer()
}.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
struct DetailView: View {
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name).font(.system(size: 32))
Text("\(detail.main.temp)").font(.system(size: 44))
Text(detail.firstWeatherInfo()).font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
#Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}

How to set up SwiftUI app to pass inputted value as parameter into URL string

I am attempting to build a basic SwiftUI weather app. The app allows the user to search weather by city name, using the OpenWeatherMap API. I configured the inputted city name from the text field to be injected into name: "" in WeatherModel, inside the fetchWeather() function in the viewModel. I then configured the OpenWeatherMap URL string to take in searchedCity.name as a parameter (see viewModel below). This setup seems to work fine, as I am able to search for weather by city name. However, I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct. In regards to:
let searchedCity = WeatherModel(...
... I am not sure what to do with the CurrentWeather and WeatherInfo inside that instance of WeatherModel. Since I'm only using "searchedCity" to pass the name of the city into the URL, how should "CurrentWeather.init(temp: 123.00)" and "weather: [WeatherInfo.init(description: "")]" be set? Is it correct to implement values for temp and description, such as 123 and ""?
Here is my full code below:
ContentView
import SwiftUI
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
#StateObject var viewModel = WeatherViewModel()
// #State private var textField = ""
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $viewModel.enterCityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather()
viewModel.enterCityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
Text(viewModel.title)
.font(.system(size: 32))
Text(viewModel.temp)
.font(.system(size: 44))
Text(viewModel.descriptionText)
.font(.system(size: 24))
Spacer()
}
.navigationTitle("Weather MVVM")
}.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Model
import Foundation
// Data, Model should mirror the JSON layout
//Codable is the property needed to convert JSON into a struct
struct WeatherModel: Codable {
let name: String
let main: CurrentWeather
let weather: [WeatherInfo]
}
struct CurrentWeather: Codable {
let temp: Float
}
struct WeatherInfo: Codable {
let description: String
}
ViewModel
import Foundation
class WeatherViewModel: ObservableObject {
//everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
#Published var title: String = "-"
#Published var temp: String = "-"
#Published var descriptionText: String = "-"
#Published var enterCityName: String = ""
init() {
fetchWeather()
}
func fetchWeather() {
let searchedCity = WeatherModel(name: enterCityName, main: CurrentWeather.init(temp: 123.00), weather: [WeatherInfo.init(description: "")])
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(searchedCity.name)&units=imperial&appid=<myAPIKey>") else {
return
}
let task = URLSession.shared.dataTask(with: url) { data, _, error in
// get data
guard let data = data, error == nil else {
return
}
//convert data to model
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.title = model.name
self.temp = "\(model.main.temp)"
self.descriptionText = model.weather.first?.description ?? "No Description"
}
}
catch {
print(error)
}
}
task.resume()
}
}
There are many ways to do what you ask, the following code is just one approach. Since you only need the city name to get the result, just use only that in the url string. Also using your WeatherModel in the WeatherViewModel avoids duplicating the data into various intermediate variables.
PS: do not post your secret appid key in your url.
import Foundation
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#StateObject var viewModel = WeatherViewModel()
#State private var cityName = "" // <-- use this to get the city name
var body: some View {
NavigationView {
VStack {
TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
Button(action: {
viewModel.fetchWeather(for: cityName) // <-- let the model fetch the results
cityName = ""
}, label: {
Text("Search")
.padding(10)
.background(Color.green)
.foregroundColor(Color.white)
.cornerRadius(10)
})
// --- display the results ---
Text(viewModel.cityWeather.name).font(.system(size: 32))
Text("\(viewModel.cityWeather.main.temp)").font(.system(size: 44))
Text(viewModel.cityWeather.firstWeatherInfo()).font(.system(size: 24))
Spacer()
}
.navigationTitle("Weather MVVM")
}.navigationViewStyle(.stack)
}
}
class WeatherViewModel: ObservableObject {
// use your WeatherModel that you get from the fetch results
#Published var cityWeather: WeatherModel = WeatherModel()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityWeather = model
}
}
catch {
print(error) // <-- need to deal with errors here
}
}
task.resume()
}
}
struct WeatherModel: Codable {
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Float = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct.e
You should alway avoid to pass fake values to an object/class like Int(123).
Instead you should use nullable structures or classes.
I don't see the need of create a whole WeatherModel instance just to read one property from it, one property that you already have in a viewmodel's enterCityName property. Just use the viewmodel's enterCityName property instead.

How to generate fixed number of views from json API in SwiftUI?

So I am learning how to use SwiftUI with a json api. I am currently generating views through a list and a ForEach Loop. I am wondering how I can make it so that it only generates the first, say 10 elements in the posts array, instead of generating the entire list from the api. Basically I want to use RandomElement() to display 10 random posts from the entire array. I am just beginning here and learning so any help woul dbe appreicated.
Below is the code for my main view that is displaying the list
import SwiftUI
struct postList: View {
//state variable of the posts
#State var posts: [Post] = []
var array = [Post]()
var body: some View {
List {
ForEach(posts) { post in
VStack(alignment: .leading) {
Text(post.title)
.font(.headline)
Text(post.body)
.font(.callout)
}
.padding()
}
}
.onAppear() {
Api().getPosts { (posts) in
self.posts = posts
}
}
}
}
struct postList_Previews: PreviewProvider {
static var previews: some View {
postList()
}
}
Down here is the Data file I use to retrieve the json data
import SwiftUI
struct Post: Codable, Identifiable {
let id = UUID()
var title: String
var body: String
}
class Api {
func getPosts(completion: #escaping ([Post]) -> ()) {
guard let url = URL(string: "http://jsonplaceholder.typicode.com/posts") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
let posts = try! JSONDecoder().decode([Post].self, from: data!)
DispatchQueue.main.async {
completion(posts)
}
}
.resume()
}
}

Resources