Trouble displaying data from Contentful in SwiftUI - ios

I'm trying to display data from Contentful into my SwiftUI app but I'm hitting an issue.
The goal is to display a list of movies and have them tappable. When I select a movie I want to get the data for that movie, so the title & movie trailer for example.
But in my selectable row I'm getting Use of undeclared type 'movie' and in my movies View I'm getting Use of undeclared type 'fetcher'
Here's what I have tried below:
import SwiftUI
import Combine
import Contentful
struct Movie: Codable, Identifiable, FieldKeysQueryable, EntryDecodable {
static let contentTypeId: String = "movies"
// FlatResource Memberes.
let id: String
var updatedAt: Date?
var createdAt: Date?
var localeCode: String?
var title: String
var movieId: String
var movieTrailer: String
enum FieldKeys: String, CodingKey {
case title, movieId, movieTrailer
}
enum CodingKeys: String, CodingKey {
case id = "id"
case title = "title"
case movieId = "movieId"
case movieTrailer = "movieTrailer"
}
}
public class MovieFetcher: ObservableObject {
#Published var movies = [Movie]()
init() {
getArray(id: "movies") { (items) in
items.forEach { (item) in
self.movies.append(Movie(id: item.id, title: item.title, movieId: item.movieId, movieTrailer: item.movieTrailer))
}
}
}
func getArray(id: String, completion: #escaping([Movie]) -> ()) {
let client = Client(spaceId: spaceId, accessToken: accessToken, contentTypeClasses: [Movie.self])
let query = QueryOn<Movie>.where(contentTypeId: "movies")
client.fetchArray(of: Movie.self, matching: query) { (result: Result<ArrayResponse<Movie>>) in
switch result {
case .success(let array):
DispatchQueue.main.async {
completion(array.items)
}
case .error(let error):
print(error)
}
}
}
}
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil
#Binding var show: Bool
var body: some View {
HStack(alignment: .bottom) {
if show {
ScrollView(.horizontal) {
Spacer()
HStack(alignment: .bottom, spacing: 30) {
ForEach(fetcher.movies, id: \.self) { item in
selectableRow(movie: item, selectMovie: self.$selectMovie)
}
}
.frame(minWidth: 0, maxWidth: .infinity)
}
.padding(.leading, 46)
.padding(.bottom, 26)
}
}
}
}
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie?
#State var initialImage = UIImage()
var urlString = "\(urlBase)\(movie.movieId).png?"
var body: some View {
ZStack(alignment: .center) {
if movie == selectedMovie {
Image("")
.resizable()
.frame(width: 187, height: 254)
.overlay(
RoundedRectangle(cornerRadius: 13)
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 182, height: 249)
.onAppear {
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
} else {
Image(uiImage: initialImage)
.resizable()
.cornerRadius(13.0)
.frame(width: 135, height: 179)
.onAppear {
guard let url = URL(string: self.urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
guard let image = UIImage(data: data) else { return }
RunLoop.main.perform {
self.initialImage = image
}
}.resume()
}
}
}
.onTapGesture {
self.selectedMovie = self.movie
}
}
}

I suppose it was intended
struct moviesView : View {
#ObservedObject var fetcher = MovieFetcher()
#State var selectMovie: Movie? = nil // type is Movie !!
...
and here
struct selectableRow : View {
var movie: Movie
#Binding var selectedMovie: Movie? // type is Movie !!
The good practice is to use Capitalized names for types and lowerCased types for variables/properties, following this neither you nor compiler be confused.

Related

Return Duplicate record with Realm

I am using Relam to store the data locally and working fine but when I try to add the new record with navigation link it returns the duplicate record as well . Another problem is when I click the record , I am expecting change the navigation but since it got duplicate record , the first record does not work but the second one it work .
Here is the Model .
import SwiftUI
import RealmSwift
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
init(taskObject: TaskObject) {
self.id = taskObject.id.stringValue
self.title = taskObject.title
self.completed = taskObject.completed
self.completedAt = taskObject.completedAt
}
}
Here is the Persisted Model...
import Foundation
import RealmSwift
class TaskObject: Object {
#Persisted(primaryKey: true) var id: ObjectId
#Persisted var title: String
#Persisted var completed: Bool = false
#Persisted var completedAt: Date = Date()
}
Here is the View Model ..
/
/ 2
final class TaskViewModel: ObservableObject {
// 3
#Published var tasks: [Task] = []
// 4
private var token: NotificationToken?
init() {
setupObserver()
}
deinit {
token?.invalidate()
}
// 5
private func setupObserver() {
do {
let realm = try Realm()
let results = realm.objects(TaskObject.self)
token = results.observe({ [weak self] changes in
// 6
self?.tasks = results.map(Task.init)
.sorted(by: { $0.completedAt > $1.completedAt })
.sorted(by: { !$0.completed && $1.completed })
})
} catch let error {
print(error.localizedDescription)
}
}
// 7
func addTask(title: String) {
let taskObject = TaskObject(value: [
"title": title,
"completed": false
])
do {
let realm = try Realm()
try realm.write {
realm.add(taskObject)
}
} catch let error {
print(error.localizedDescription)
}
}
// 8
func markComplete(id: String, completed: Bool) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.completed = completed
task?.completedAt = Date()
}
} catch let error {
print(error.localizedDescription)
}
}
func remove(id: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
if let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId) {
try realm.write {
realm.delete(task)
}
}
} catch let error {
print(error.localizedDescription)
}
}
func updateTitle(id: String, newTitle: String) {
do {
let realm = try Realm()
let objectId = try ObjectId(string: id)
let task = realm.object(ofType: TaskObject.self, forPrimaryKey: objectId)
try realm.write {
task?.title = newTitle
}
} catch let error {
print(error.localizedDescription)
}
}
}
Here is the code for Content view ...
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
AddTaskView()
TaskListView()
}
.navigationTitle("Todo")
.navigationBarTitleDisplayMode(.automatic)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is the code for Add task view ..
import SwiftUI
struct AddTaskView: View {
#State private var taskTitle: String = ""
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
HStack(spacing: 12) {
TextField("Enter New Task..", text: $taskTitle)
Button(action: handleSubmit) {
Image(systemName: "plus")
}
}
.padding(20)
}
private func handleSubmit() {
viewModel.addTask(title: taskTitle)
taskTitle = ""
}
}
Here is the Task list View ..
struct TaskListView: View {
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(viewModel.tasks, id: \.id) { task in
TaskRowView(task: task)
Divider().padding(.leading, 20)
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task)
}.animation(.default)
}
}
}
}
}
Here is the code for Row View ..
struct TaskRowView: View {
let task: Task
// 1
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
HStack(spacing: 12) {
Button(action: {
// 2
viewModel.markComplete(id: task.id, completed: !task.completed)
}) {
Image(systemName: task.completed ? "checkmark.circle.fill" : "circle")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(task.completed ? Color.green : Color.gray)
}
VStack(alignment: .leading, spacing: 8) {
Text(task.title)
.foregroundColor(.black)
if !task.completedAt.formatted().isEmpty {
Text(task.completedAt.formatted())
.foregroundColor(.gray)
.font(.caption)
}
}
Spacer()
}
.padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20))
}
}
Here is the screenshot ..
Let's troubleshoot the discrepancies one by one.
According to your code, each row in the list represents a Task. But, there are two models Task and TaskObject (persistable model) for that.
struct Task: Identifiable {
var id: String
var title: String
var completed: Bool = false
var completedAt: Date = Date()
init(taskObject: TaskObject) {
self.id = taskObject.id.stringValue
self.title = taskObject.title
self.completed = taskObject.completed
self.completedAt = taskObject.completedAt
}
}
class TaskObject: Object {
#Persisted(primaryKey: true) var id: ObjectId
#Persisted var title: String
#Persisted var completed: Bool = false
#Persisted var completedAt: Date = Date()
}
Instead of using two models, convert them into one.
class TaskObject: Object, Identifiable {
#Persisted(primaryKey: true) var id: ObjectId
#Persisted var title: String
#Persisted var completed: Bool = false
#Persisted var completedAt: Date = Date()
var idStr: String {
id.stringValue
}
}
Therefore, there's no need for mapping to another object after retrieving it from the database. The updated setupObserver function should be...
private func setupObserver() {
do {
let realm = try Realm()
let results = realm.objects(TaskObject.self)
token = results.observe({ [weak self] changes in
// 6
self?.tasks = results
.sorted(by: { $0.completedAt > $1.completedAt })
.sorted(by: { !$0.completed && $1.completed })
})
} catch let error {
print(error.localizedDescription)
}
}
Let's address your questions now.
When I try to add the new record with navigation link it returns the duplicate record as well
It does not produce duplicate data. Instead, the same data is displayed twice in the view. To correct this, remove one of the two instances of TaskRowView(task: task).
struct TaskListView: View {
#EnvironmentObject private var viewModel: TaskViewModel
var body: some View {
ScrollView {
LazyVStack (alignment: .leading) {
ForEach(viewModel.tasks, id: \.id) { task in
TaskRowView(task: task) // first row 📌
Divider().padding(.leading, 20)
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task) // second row 📌
}.animation(.default)
}
}
}
}
}
Next question,
I am expecting change the navigation but since it got duplicate record , the first record does not work but the second one it work.
Again, the second one changes navigation, and the first one does not, because this is exactly what is written in the code.
TaskRowView(task: task) // Why would it change navigation?
Divider().padding(.leading, 20)
NavigationLink (destination: TaskView(task: task)) {
TaskRowView(task: task) // changing navigation
}

how to make A SwiftUI Image Gallery/Slideshow With Auto Scrolling in SwiftUI?

How can I make a slider when my Data is coming from API? I am using
this(below code) for static images work fine but whenever I try to
use API data then my code does not work.
How to Set the Marquee in this images.
This is My code
public struct MagazineModel: Decodable {
public let magzineBanners: [MagzineBanner]
}
public struct MagzineBanner: Decodable, Identifiable {
public let id: Int
public let url: String
}
This is My View Model
//View Model for Magazines and showing Details
class MagazineBannerVM: ObservableObject{
#Published var datas = [MagzineBanner]()
let url = "ApiUrl"
init() {
getData(url: url)
}
func getData(url: String) {
guard let url = URL(string: url) else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(MagazineModel.self, from: data)
DispatchQueue.main.async {
self.datas = results.magzineBanners
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct MagazineBannerView: View{
#ObservedObject var list = MagazineBannerVM()
public let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
#State var currentIndex = 0
#State var totalImages = 2
var body: some View{
ScrollView(.horizontal) {
GeometryReader { proxy in
TabView(selection: $currentIndex) {
HStack{
ForEach(list.datas, id: \.id){ item in
Group{
AsyncImage(url: URL(string: item.url)){ image in
image
.resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}placeholder: {
Image("logo_gray").resizable()
.frame(width:UIScreen.main.bounds.width, height: 122)
}
}
}
}
}
.tabViewStyle(PageTabViewStyle())
.onReceive(timer, perform: { _ in
withAnimation{
currentIndex = currentIndex < totalImages ? currentIndex + 1: 0
}
})
}
}
}
}
I want to change images after every 2 seconds and every images has
full width as the screen width
And it is showing the half of screen width and showing both images in
single view

fetch array data from API in SwiftUI?

This is Model for My API and we will use this model in future. can Anyone tell me this is correct or Not. Im a native user of SwiftUI.
public struct Temperatures {
public let bookDetails: BookDetails
public init(bookDetails: BookDetails) {
self.bookDetails = bookDetails
}
}
public struct BookDetails {
public let data: [Datum]
public init(data: [Datum]) {
self.data = data
}
}
public struct Datum : Hashable, Identifiable {
public let id = UUID()
public let title: String
public let published: String
public let url: String
public init( title: String, published: String, url: String) {
self.title = title
self.published = published
self.url = url
}
}
And this is ViewModel
And i cant fetch the data of data[]
View Model For Prime Book And Showing Details
class PrimeBookVM: ObservableObject{
#Published var datas = [Datum]()
init(){
let source = "https://********/api/book-search?is_prime"
let url = URL(string: source)!
let session = URLSession(configuration: .default)
session.dataTask(with: url){
(data, _, err) in
if err != nil{
print(err?.localizedDescription ?? "Hello Error")
return
}
let json = try!JSON(data: data!)
for i in json["data"]{
let published = i.1["published"].stringValue
let title = i.1["title"].stringValue
let url = i.1["url"].stringValue
DispatchQueue.main.async {
self.datas.append(Datum(title: title, published: published, url: url))
}
}
}
.resume()
}
}
This is my View and try to fetch the detail of data array in api.
struct PrimeBooksView: View{
#StateObject var list = PrimeBookVM()
var body: some View{
ScrollView(.horizontal){
HStack{
ForEach(list.datas, id: \.self){ item in
VStack(alignment: .leading){
WebImage(url: URL(string: item.url)!, options: .highPriority, context: nil)
.resizable()
.frame(width: 180, height: 230)
Text(item.title)
.multilineTextAlignment(.leading)
.font(.system(size: 16))
.foregroundColor(Color("default"))
Text(item.published)
.font(.system(size: 12))
.fontWeight(.light)
.foregroundColor(Color("default"))
}
.padding(.all,4)
.background(Color.white).cornerRadius(8)
.shadow(color: .gray, radius: 1)
}
.padding(.all,1)
}
}
}
}
Thank You So much in Advance for Help.
Try this example code, with a working set of data model structs, and an updated getData() function
to fetch the data from the server. You still need to check the server documentation,
to determine which properties are optional.
import Foundation
import SwiftUI
struct ContentView: View {
var body: some View {
PrimeBooksView()
}
}
class PrimeBookVM: ObservableObject {
#Published var datas = [Datum]()
init() {
getData()
}
func getData() {
guard let url = URL(string: "https://alibrary.in/api/book-search?is_prime") else { return }
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data {
do {
let results = try JSONDecoder().decode(ApiResponse.self, from: data)
DispatchQueue.main.async {
self.datas = results.bookDetails.data
}
}
catch {
print(error)
}
}
}.resume()
}
}
struct PrimeBooksView: View{
#StateObject var list = PrimeBookVM()
var body: some View{
ScrollView(.horizontal){
HStack {
ForEach(list.datas, id: \.self){ item in
VStack(alignment: .leading){
AsyncImage(url: URL(string: item.url)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 180, height: 230)
} placeholder: {
ProgressView()
}
Text(item.title)
.multilineTextAlignment(.leading)
.font(.system(size: 16))
Text(item.published)
.font(.system(size: 12))
.fontWeight(.light)
}
.padding(4)
.background(Color.white).cornerRadius(8)
.shadow(color: .gray, radius: 1)
}
}
}
}
}
public struct ApiResponse: Codable {
let bookDetails: BookDetails
let bookSearch: String?
let uploadTypeID: Int
let stackID: String
let data: Int
enum CodingKeys: String, CodingKey {
case bookDetails, bookSearch
case uploadTypeID = "upload_type_id"
case stackID = "stack_id"
case data
}
}
public struct BookDetails: Codable {
let currentPage: Int
let data: [Datum]
let firstPageURL: String
let from, lastPage: Int
let lastPageURL, nextPageURL, path: String
let perPage: Int
let prevPageURL: String?
let to, total: Int
enum CodingKeys: String, CodingKey {
case data, from, path, to, total
case currentPage = "current_page"
case firstPageURL = "first_page_url"
case lastPage = "last_page"
case lastPageURL = "last_page_url"
case nextPageURL = "next_page_url"
case perPage = "per_page"
case prevPageURL = "prev_page_url"
}
}
public struct Datum : Hashable, Identifiable, Codable {
public let id = UUID() // <-- could be Int
public let title: String
public let published: String
public let url: String
public init( title: String, published: String, url: String) {
self.title = title
self.published = published
self.url = url
}
enum CodingKeys: String, CodingKey {
case title, published, url
}
}

index out of bound from TabViewStyle of SwiftUI

I am trying to implement a tabView that takes a list of items into pages that I could swipe back and forth. However, it keeps bugging out with an "index out of bound" error. It's confusing to me because I never set index at all, and I don't know how to force an index either...
Below are my code. Apologize for any naive code, I am new to SwiftUI. Any helps are appreciated, thank you!
import SwiftUI
//#Published private var list = QuestionList.self
struct QuestionList: Codable {
var list:[QuestionItem]
}
class QuestionItem: Codable, Identifiable {
var id: Int
var text: String
var type: Int
var answer: String
}
struct ContentView: View {
#State private var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
isShowForm = !isShowForm
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
print(type(of: qlist[1]))
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
self.showForm()
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach(qlist.indices, id: \.self) { index in
if qlist[index].type == 0 {
HStack {
Text("\(self.qlist[index].text)")
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text("\(self.qlist[index].text)")
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qlist[index].answer) {
}
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It is not recommended to use indices in ForEach loops, instead use something like this updated code
that allows you to use your TextField :
ForEach($qlist) { $qItem in // <-- here $
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer) {
}
}
}
}
and as I mentioned in my comment, remove the print(qlist[1]) and print(type(of: qlist[1])) in showForm,
because if qlist is empty or only has one element, you will get the index out of bound error .
Remember one element is qlist[0].
EDIT-1: full test code:
this is the code I used in my test. It does not give any index errors.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var qlist = [QuestionItem]()
#State private var isShowForm = false
#State private var q1 = true
#State private var answer = ""
#State private var isOn = [Bool]()
#State private var selectedTab = 0
func showForm() {
let url = URL(string: "http://127.0.0.1:3000/question")!
let task = URLSession.shared.dataTask(with: url) {
data, response, error in
if let error = error {
print(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
// print(response)
return
}
guard let data = data else {
return
}
do {
let list = try JSONDecoder().decode([QuestionItem].self, from:data)
qlist = list
// print(qlist[1])
for i in 0..<qlist.count {
isOn.append(qlist[i].type == 0)
}
print(isOn)
// print(isOn)
// print(type(of: qlist[1]))
isShowForm.toggle() // <--- here important
} catch {
print("error: ", error)
}
}
task.resume()
}
var body: some View {
Text("Hello, world!")
.padding()
Button("Open Form") {
// self.showForm()
// simulate showForm()
qlist = [
QuestionItem(id: 0, text: "text0", type: 0, answer: "0"),
QuestionItem(id: 1, text: "text1", type: 1, answer: "1"),
QuestionItem(id: 2, text: "text2", type: 2, answer: "2"),
QuestionItem(id: 3, text: "text3", type: 3, answer: "3")
]
isShowForm.toggle() // <--- here after qlist is set
}
if (isShowForm) {
TabView(selection: $selectedTab) {
ForEach($qlist) { $qItem in
if qItem.type == 0 {
HStack {
Text(qItem.text)
// Toggle("", isOn: $isOn[index])
Toggle("", isOn: $q1)
}
} else {
VStack {
Text(qItem.text)
// .lineLimit(2)
// .multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
// .frame(width: 300)
TextField("Enter your answer here:", text: $qItem.answer)
.border(.red)
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
.indexViewStyle(.page(backgroundDisplayMode: .always))
}
}
}
struct QuestionItem: Codable, Identifiable { // <-- here note the struct
var id: Int
var text: String
var type: Int
var answer: String
}

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()
}
}

Resources