I am new to iOS/Swift and to the MVVM architecture and was wondering how to send in test data to the SwiftUI Preview. (Actual data is received from the ViewModel from an API call)
Test Data that I want to add in:
test data = [(name: "Test1", price: "18.00-21.00"), (name: "Test2", price: "8.00-11.00"), (name: "Test3", price: "10.00")]
My View:
struct TodayView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
NavigationView {
List(viewModel.results, id: \.self) { item in
Text(item.name)
.font(.subheadline)
Text(item.price ?? "NIL")
.font(.headline)
}
.listStyle(InsetGroupedListStyle())
.navigationBarTitle(Text("\(viewModel.titleDate)"))
}
}
}
struct TodayView_Previews: PreviewProvider {
static var previews: some View {
// Add Test Data here? How to pass it into TodayView?
TodayView(viewModel: ViewModel())
}
}
My View Model( I am sending both the Date & Actual Data to the View):
class ViewModel: ObservableObject {
// Title Date
#Published var titleDate: String = ""
let dateLong = Date()
func createDate() -> String {
let formatter = DateFormatter()
formatter.dateFormat = "E, MMM d"
return formatter.string(from: Date())
}
init(){
self.results = [Model.Calendar]()
titleDate = self.createDate()
loadData()
}
func loadData() {
// API Call
}
// Actual Data received from API call
#Published var results: [Model.Calendar]
}
My Model:
struct Model {
struct Calendar: Codable, Hashable {
var name: String
var price: String
}
}
You can try the following:
struct TodayView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = ViewModel()
viewModel.results = <your_test_data>
return TodayView(viewModel: viewModel)
}
}
Also, you may need to remove loadData() from init (in ViewModel), so your test data won't be overridden.
You can create public protocol called 'ViewModelProtocol', which will have basic functions and properties that your ViewModel needs, like for instance
var results: [Model.Calendar]
And inside of View, you can put type of ViewModel to be of that protocol.
Then, you can inject any object that conforms to that particular 'ViewModelProtocol', which allows you to create two different view models ViewModel and ViewModelMock accordingly, where both of them should be conformed to ViewModelProtocol.
In ViewModelMock you will add dummy data for your testing, while regular ViewModel will have to fetch the data from the Network.
public protocol ViewModelProtocol {
var results: [Model.Calendar]
}
struct ViewModel: ViewModelProtocol {...}
struct ViewModelMock: ViewModelProtocol {...}
Related
How do I pass a bindable object into a view inside a ForEach loop?
Minimum reproducible code below.
class Person: Identifiable, ObservableObject {
let id: UUID = UUID()
#Published var healthy: Bool = true
}
class GroupOfPeople {
let people: [Person] = [Person(), Person(), Person()]
}
public struct GroupListView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
//MARK: Other properties
let group: GroupOfPeople = GroupOfPeople()
//MARK: Body
public var body: some View {
ForEach(group.people) { person in
//ERROR: Cannot find '$person' in scope
PersonView(person: $person)
}
}
//MARK: Init
}
public struct PersonView: View {
//MARK: Environment and StateObject properties
//MARK: State and Binding properties
#Binding var person: Person
//MARK: Other properties
//MARK: Body
public var body: some View {
switch person.healthy {
case true:
Text("Healthy")
case false:
Text("Not Healthy")
}
}
//MARK: Init
init(person: Binding<Person>) {
self._person = person
}
}
The error I get is Cannot find '$person' in scope. I understand that the #Binding part of the variable is not in scope while the ForEach loop is executing. I'm looking for advice on a different pattern to accomplish #Binding objects to views in a List in SwiftUI.
The SwiftUI way would be something like this:
// struct instead of class
struct Person: Identifiable {
let id: UUID = UUID()
var healthy: Bool = true
}
// class publishing an array of Person
class GroupOfPeople: ObservableObject {
#Published var people: [Person] = [
Person(), Person(), Person()
]
}
struct GroupListView: View {
// instantiating the class
#StateObject var group: GroupOfPeople = GroupOfPeople()
var body: some View {
List {
// now you can use the $ init of ForEach
ForEach($group.people) { $person in
PersonView(person: $person)
}
}
}
}
struct PersonView: View {
#Binding var person: Person
var body: some View {
HStack {
// ternary instead of switch
Text(person.healthy ? "Healthy" : "Not Healthy")
Spacer()
// Button to change, so Binding makes some sense :)
Button("change") {
person.healthy.toggle()
}
}
}
}
You don't need Binding. You need ObservedObject.
for anyone still wondering... it looks like this has been added
.onContinuousHover(perform: { phase in
switch phase {
case .active(let location):
print(location.x)
case .ended:
print("ended")
}
})
I have viewmodel and view i have return some API logic in viewmodel and getting one dictionary after some logic..i want to access that dictionary value from view for ex.
viewmodel.someDic
but for now every-time i am getting empty dic.
class Viewmodel: ObservableObject {
#Published private var poductDetails:[String:ProductDetail] = [:]
func createItems(data: ProductRootClass) {
var productDetils = [String: SelectedProductDetail](){
//some logic
productDetils // with some object
self.poductDetails = productDetils
}
}
}
struct View: View {
#StateObject var viewModel: ViewModel
var body: some View {
VStack(alignment: .leading) {
Text("\(viewModel.poductDetails)")
}
.onAppear(perform: {
print("\(viewModel.poductDetails)")
})
}
}
I want to access this dictionary from view.
I tried accessing by returning productDetils from any function but get empty everytime.
may i know the way to access property from viewmodel to view?
You need a class conforming to ObservableObject and a property marked as #Published
class ViewModel : ObservableObject {
#Published var productDetails = [String:ProductDetail]()
func createItems(data: ProductRootClass) {
var productDetils = [String: SelectedProductDetail]()
//some logic
productDetils // with some object
self.productDetails = productDetils
}
}
Whenever the property is modified the view will be updated.
In the view create an instance and declare it as #StateObject
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack(alignment: .leading) {
ForEach(viewModel.productDetails.keys.sorted(), id: \.self) { key in
Text("\(viewModel.poductDetails[key]["someOtherKey"] as! String)")
}
}
}
}
I would prefer an array as model, it's easier to access
You need to get rid of the view model and make a proper model.
class Model: ObservableObject {
#Published private var productDetails:[ProductDetail] = []
}
struct ProductDetail: Identifiable {
let id = UUID()
var title: String
}
Now you can do ForEach(model.productDetails)
You can make your viewModel as a Singleton.
class ViewModel : ObservableObject {
static let viewModelSingleton = ViewModel()
#Published var productDetails = [String : ProductDetail]()
\\API logic}
Access this viewModel Singleton in the view
struct view : View {
#ObservedObject var viewModelObject = ViewModel.viewModelSingleton
var body: some View {
VStack(alignment: .leading) {
Text("\(viewModelObject.productDetails)")
}
}}
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.
I'm trying to update a view with a simple Observable pattern, but it doesn't happen for some reason. The Publisher gets updated, but the subscriber doesn't. I've simplified to the code below. When you click the Add button, the view doesn't get updated and also the variable.
I'm using this function (NetworkManager.shared.saveNew()), because I update after a CloudKit notification. If you know a workaround, I'd be pleased to know!
import SwiftUI
import Combine
public class NetworkManager: ObservableObject {
static let shared = NetworkManager()
#Published var list = [DataSource]()
init() {
self.list = [DataSource(id: "Hello", action: [1], link: "http://hello.com", year: 2020)]
}
func saveNew(item: DataSource) {
self.list.append(item)
}
}
struct DataSource: Identifiable, Codable {
var id: String
var action: [Int]
var link: String
var year: Int
init(id: String, action: [Int], link: String, year: Int) {
self.id = id
self.action = action
self.link = link
self.year = year
}
}
struct ContentView: View {
#ObservedObject var list = NetworkManager()
var body: some View {
VStack {
Button("Add") {
NetworkManager.shared.saveNew(item: DataSource(id: "GoodBye", action: [1], link: "http://goodbye", year: 2030))
}
List(list.list, id:\.id) { item in
Text(item.id)
}
}
}
}
It should be used same instance of NetworkManager, so here is fixed variant (and better to name manager as manager but not as list)
struct ContentView: View {
#ObservedObject var manager = NetworkManager.shared // << fix !!
var body: some View {
VStack {
Button("Add") {
self.manager.saveNew(item: DataSource(id: "GoodBye", action: [1], link: "http://goodbye", year: 2030))
}
List(manager.list, id:\.id) { item in
Text(item.id)
}
}
}
}
I am using SwiftUI on the Apple Watch and trying to use #ObservableObject, #ObservedObject, and #Binding correctly. I'm updating a value in a DetailView, and I want to have it reflected locally, as well as have the data changed globally. The code below works, but I am using a kludge to force the DetailView to redraw itself:
Is there a better way?
-------------- ContentView.swift ---------------
import Combine
import SwiftUI
struct person: Identifiable {
var id:Int = 0
var name:String
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
class AppData: ObservableObject {
#Published var people:[person] = [person(id:0, name:"John"),
person(id:1, name:"Bret"),
person(id:2,name:"Sue"),
person(id:3,name:"Amy")]
}
var gAppData = AppData()
struct ContentView: View {
#ObservedObject var model:AppData
var body: some View {
List( model.people.indices ){ index in
NavigationLink(destination: DetailView(person:self.$model.people[index])) { Text(self.model.people[index].name) }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(model:gAppData)
}
}
-------------- DetailView.swift ---------------
import SwiftUI
struct DetailView: View {
#Binding var person: person
// Created an unnecessary var to force a redreaw of the view
#State var doRedraw:Bool = true
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob"; self.doRedraw = false }) {
Text("Set Name to Bob")
}
}
}
}
struct DestView_Previews: PreviewProvider {
static var previews: some View {
DetailView(person:.constant(person( id:0, name:"John"))) // what does ".constant" actually do?
}
}
The problem here is because your view redraws only when you changes the #State or #Binding variable. Here you do not change the Person variable, but its property, which should not affect the user interface (because you didn't say to do this). I changed your code for a little for showing how to achieve this effect, you can go ahead from this point. You need to remember, what exactly affect UI:
class Person: Identifiable, ObservableObject { // better to assign struct/class names using UpperCamelCase
#Published var name:String // now change of this variable will affect UI
var id:Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
}
// changes in DetailView
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(person.name)
Button(action:{ self.person.name = "Bob" }) {
Text("Set Name to Bob")
}
}
}
}
// preview
struct DetailViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
DetailView()
.environmentObject(Person(id: 1, name: "John"))
}
}
update: full code for List and Detail
import SwiftUI
class Person: Identifiable, ObservableObject { // better to assign type names using UpperCamelCase
#Published var name: String //{
var id: Int = 0
init( id: Int, name:String) {
self.id = id
self.name = name
}
func changeName(_ newName: String) {
self.name = newName
}
}
class AppData: ObservableObject {
#Published var people: [Person] = [Person(id:0, name:"John"),
Person(id:1, name:"Bret"),
Person(id:2,name:"Sue"),
Person(id:3,name:"Amy")]
}
struct ContentViewWithoutGlobalVar: View {
#EnvironmentObject var model: AppData
var body: some View {
NavigationView { // you forget something to navigate between views
List(model.people.indices) { index in
NavigationLink(destination: DetailView()
.environmentObject(self.model.people[index])) {
PersonRow(person: self.$model.people[index])
}
}
}
}
}
struct PersonRow: View {
#Binding var person: Person // this struct will see changes in Person and show them
var body: some View {
Text(person.name)
}
}
struct DetailView: View {
#EnvironmentObject var person: Person
var body: some View {
VStack(){
Text(self.person.name)
Button(action:{ self.person.changeName("Bob") }) {
Text("Set Name to Bob")
}
}
}
}
struct ContentViewWithoutGlobalVar_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentViewWithoutGlobalVar()
.environmentObject(AppData())
DetailView()
.environmentObject(Person(id: 0, name: "John"))
}
}
}