Append things to Array at launching - ios

Hi have a question to the following code:
struct ContentView: View {
#State private var listItems = [Item]()
let savedItems = UserDefaults.standard.string(forKey: "Items") ?? "error"
var body: some View {
NavigationView{
List{
ForEach(listItems){ item in
Text(item.name)
}
NavigationLink(
destination: AddView(listItems: $listItems),
label: {
Text(" Add")
.foregroundColor(.blue)
})
}
.navigationBarTitle("MyApp")
}
}
}
struct AddView: View {
#State var text:String = ""
#Binding var listItems: [Item]
var body: some View {
VStack{
TextField("Name", text: $text).padding().background(Color(.systemGray5)).frame(width: 400,alignment: .center).cornerRadius(20)
Button(action: {
listItems.append(Item(name: text, image: "Gif"))
print(listItems)
UserDefaults.standard.set(listItems, forKey: "Items")
}, label: {
Text("Add")
})
}
}
}
If i append something in the AddView, i want to save it in the UserDefaults and append it automatically when the app starts. But I don't know how to do this. Can you help me ?
PS: I'm using AppDelegate and SceneDelegate.

To save something in the UserDefaults it will need to be either of those things:
The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
see full documentation
You are trying assign and array of Item objects, and while it is perfectly fine to save an array, the Element type of this array will also need to be one of the types mentioned in the documentation.
Your problem can be solved using Codable conformance.
Extend your item type with Codable
struct Item: Codable {
/// your implementation
}
Save encoded items to UserDefaults
extension UserDefaults {
func saveItems(_ items: [Item]) {
let encoder = JSONEncoder()
if let data = try? encoder.encode(items) {
set(data, forKey: "Items")
}
}
}
simply call this method anytime you want to save listItems array.
UserDefaults.standard.saveItems(listItems)
Read stored items from UserDefaults
extension UserDefaults {
func readItems() -> [Item]? {
guard let data = value(forKey: "Items") as? Data else {
return nil
}
let decoder = JSONDecoder()
return try? decoder.decode([Item].self, from: data)
}
}
with this method you can lazily load items when you need them
#State private var listItems = UserDefaults.standard.readItems() ?? []

Related

Core data for favourite buttons

I want to make it so you can favourite a "landmark" in one view (LandmarkDetail), and access a list of all the "landmarks" in another view with the ones I've favourited highlighted. First I used "#AppStorrage" but I was told too to use Core Data for it instead. So far I have the favourite button working in the LandmarkDetail view with "#AppStorage" but apparently I need to change that so it uses Core Data.
I've look around to get an understanding of how to do it with Core Data but I could really use a helping hand if anyone can help. I've already seen and read some tutorials about core data and how to set it up, but I can't find anything for my specific problem where I pull in data from a JSON and I need Core Data to handle the favourite feature.
Here is my code for the favourite button
struct FavoriteButton: View {
#AppStorage ("isFavorite") var isFavorite: Bool = false
var body: some View {
Button {
isFavorite.toggle()
} label: {
Label("Toggle Favorite", systemImage: isFavorite ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isFavorite ? .yellow : .gray)
}
}
}
Code from the landmark detail view
struct LandmarkDetail: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
Code for the rows in the list view
This is the one not working yet, so far it just pulls the data from a JSON.
MODEL
import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
private var imageName: String
var image: Image{
Image(imageName)
}
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
import Foundation
import Combine
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
#Published var profile = Profile.default
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
First of all you need to create a .xcdatamodeld file named Landmarks. You can create it by pressing right button on the principal folder of your project and searching Data Model. After you need to create a new Entity named Landmark. You can add attributes showed in your model like id, name, park, etc... with their types. After you need to create a new Swift file in which you can create you Core Data Controller like this:
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Landmarks")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
Successively, you need to add to your LandmarksApp.swift file the following code:
struct LandmarksApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
Continue adding this to your LandmarkDetail view:
struct LandmarkDetail: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
To create a new item and add to Core Data you can write:
let landmark = Landmark(context: moc)
landmark.id = id
landmark.name = name
landmark.park = park
etc...
try? moc.save()
For your JSON data you can create a function that convert all JSON data in Core Data following these steps.

Why is my view not getting updated even when #State property has changed?

I decoded the Json response and confirmed its validity,I also am able to print the elements of the array in loadData(). however when I try to access the elements of the response array in my Vstack I get an array out of bounds error. Shouldn't this change the #State welcome var and update my UI?
import SwiftUI
struct ContentView: View {
#State private var welcome = [WelcomeElement]()
func loadData() async {
if let url = URL(string: "https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?number=5") {
let (data,_) = try! await URLSession.shared.data(from: url)
print(data)
if let welcome = try? (JSONDecoder().decode(Welcome.self, from: data)) {
print(welcome[1])
}
}
}
var body: some View {
VStack{
Text(welcome[1].fact)
Text(welcome[2].fact)
}.task {
await loadData()
}
}
}
// MARK: - WelcomeElement
struct WelcomeElement: Codable {
let fact: String
}
typealias Welcome = [WelcomeElement]
First, you need to add a check in VStack that only display Text if array isn’t empty. This solves the out of bounds crash.
Second, you are never setting the #State var array to the decoded data. You need to set the array variable after decoding.
if let welcome = try? (JSONDecoder().decode(Welcome.self, from: data)) {
print(welcome[1])
self.welcome = welcome
}

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.

Problems saving data to UserDefaults

I'm struggling with saving some date to UserDefaults. I have a struct, an array of which I'm going to save:
struct Habit: Identifiable, Codable {
var id = UUID()
var name: String
var comments: String
}
Then, in the view, I have a button to save new habit to an array of habits and put it into UserDefaults:
struct AddView: View {
#State private var newHabit = Habit(name: "", comments: "")
#State private var name: String = ""
let userData = defaults.object(forKey: "userData") as? [Habit] ?? [Habit]()
#State private var allHabits = [Habit]()
var body: some View {
NavigationView {
Form {
Section(header: Text("Habit name")) {
TextField("Jogging", text: $newHabit.name)
}
Section(header: Text("Description")) {
TextField("Brief comments", text: $newHabit.comments)
}
}
.navigationBarTitle("New habit")
.navigationBarItems(trailing: Button(action: {
allHabits = userData
allHabits.append(newHabit)
defaults.set(allHabits, forKey: "userData")
}) {
addButton
})
}
}
}
When I tap the button, my app crashes with this thread: Thread 1: "Attempt to insert non-property list object (\n \"HabitRabbit.Habit(id: 574CA523-866E-47C3-B56B-D0F85EBD9CB1, name: \\\"Wfs\\\", comments: \\\"Sdfdfsd\\\")\"\n) for key userData"
What did I do wrong?
Adopting Codable doesn't make the object property list compliant per se, you have to encode and decode the object to and from Data.
Something like this
func loadData() -> [Habit]
guard let userData = defaults.data(forKey: "userData") else { return [] }
return try? JSONDecoder().decode([Habit].self, from: userData) ?? []
}
func saveData(habits : [Habit]) {
guard let data = try? JSONEncoder().encode(habits) else { return }
defaults.set(data, forKey: "userData")
}

How to display value fetched from database?

I’m developing an iOS application with SwiftUI, and I’m having trouble displaying my data fetched from database.
Code
import SwiftUI
import Firebase
import FirebaseDatabase
var ref = Database.database().reference()
class Observe {
static func currentSingleEventObserve(completion: #escaping ((String?) -> ())) {
let path = "supersonic/current"
let ref = Database.database().reference().child(path)
_ = ref.observeSingleEvent(of: .value, with: { snapshot in
let temp = (snapshot.value! as AnyObject).description
completion(temp)
})
}
}
struct CurrentDistance: View {
var value: NSDictionary?
var refHandle: UInt = 0
#State var distance: String
init() {
Observe.currentSingleEventObserve(completion: { temp in
self.distance = temp // I want to mutate self.distance here
})
}
var body: some View {
VStack {
Text("Distance")
Text(self.distance)
}
}
}
struct CurrentDistance_Previews: PreviewProvider {
static var previews: some View {
CurrentDistance()
}
}
Database
|-supersonic
|- current: 30
Problem
I want to mutate self.distance in the initializer. Trying to mutate self.distance in the closure I got an error Escaping closure captures mutating 'self' parameter, and I don't know how to update the value.
How can I display the value fetched from the database?
This is a somewhat generic answer as we are just going to be updating a UI element based on a value read from Firebase.
Firebase is asynchronous and values are only valid following the Firebase function, within the closure.
Suppose we have a simple SwiftUI app that displays a Text object, a button to click to load the data from Firebase, and then a var that holds what the text should be.
struct ContentView: View {
#State var buttonText = "Initial Button Label"
var body: some View {
VStack {
Text(buttonText)
Button(action: {
self.readFirebase()
}) {
Text("Click Me!")
}
}
}
func readFirebase() {
let ref = my_firebase_ref
let textRef = ref.child("string_node")
textRef.observeSingleEvent(of: .value, with: { snapshot in
let myText = snapshot.value as? String ?? "No String"
self.buttonText = myText
})
}
}
Here's my Firebase structure
root
string_node: "Hello, World"

Resources