How to get value (not array) from API in SwiftUI - swift

I work with this API: https://api.spacexdata.com/v4/rockets.
By some exploring I decided to use this API getter:
let rocketsData: [Rocket] = [] //<-- Spoiler: This is my problem
class Api {
func getRockets(completion: #escaping ([Rocket]) -> ()) {
guard let url = URL(string: "https://api.spacexdata.com/v4/rockets") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
do {
let rockets = try JSONDecoder().decode([Rocket].self, from: data!)
DispatchQueue.main.async {
completion(rockets)
}
} catch {
print(error.localizedDescription)
}
}
.resume()
}
}
This is my Model:
struct StageInfo: Codable {
let engines: Int
let fuelAmountTons: Double
let burnTimeSec: Int?
enum CodingKeys: String, CodingKey {
case engines
case fuelAmountTons = "fuel_amount_tons"
case burnTimeSec = "burn_time_sec"
}
}
struct Rocket: Codable, Identifiable {
let id = UUID()
let name: String
let firstFlight: String
let country: String
let costPerLaunch: Int
let firstStage: StageInfo
let secondStage: StageInfo
enum CodingKeys: String, CodingKey {
case id
case name
case firstFlight = "first_flight"
case country
case costPerLaunch = "cost_per_launch"
case firstStage = "first_stage"
case secondStage = "second_stage"
}
}
By this model I am able to get an array of values, and I can use this array in my View with only Lists or ForEach loops. But what if I want to use not the array, but some values?
In this view for example I use ForEach loop, that's why everything works perfect:
struct ContentView: View {
#State var rockets = [Rocket]() // <-- Here is the array I was talking about
var body: some View {
NavigationView {
List {
ForEach(rockets) { item in // <-- ForEach loop to get values from the array
NavigationLink(destination: RocketDetailView(rocket: item)) {
RocketRowView(rocket: item)
.padding(.vertical, 4)
}
}
} //: LIST
.navigationTitle("Rockets")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
isShowingSettings = true
}) {
Image(systemName: "slider.horizontal.3")
} //: BUTTON
.sheet(isPresented: $isShowingSettings) {
SettingsView()
}
}
} //: TOOLBAR
} //: NAVIGATION
.onAppear() {
Api().getRockets { rockets in // <-- Method to get the array from the API
self.rockets = rockets
}
}
}
}
//MARK: - PREVIEW
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And finally here I have a View with an item that I use to create the List of this items in another View:
struct RocketRowView: View {
var rocket: Rocket // <-- Here I don't need to use an array as previously, so I create a usual variable with Rocket instance
var body: some View {
HStack {
Image("Falcon-9")
.resizable()
.scaledToFill()
.frame(width: 80, height: 80, alignment: .center)
.background(Color.teal)
.cornerRadius(8)
VStack(alignment: .leading, spacing: 5) {
Text(rocket.name) // <-- So this surely doesn't work
.font(.title2)
.fontWeight(.bold)
Text(rocket.country) // <-- And this doesn't work neither
.font(.caption)
.foregroundColor(Color.secondary)
}
} //: HSTACK <-- And I can't use here inside onAppear the method that I used before to get the array, because I don't need the array
}
}
struct RocketRowView_Previews: PreviewProvider {
static var previews: some View {
RocketRowView(rocket: rocketsData[0]) // <-- Because of the variable at the top of this View I have to add missing argument for parameter 'rocket' in call. Immediately after that I get an error: "App crashed due to an out of range index"
}
}
As I wrote in the comments above I create a variable rocket of type Rocket in my RocketRowView() and because of that I have to add missing argument for parameter 'rocket' in RocketRowView_Previews. Here I get an error: "App crashed due to an out of range index". I don't understand why let rocketsData: [Rocket] = [] is empty and how I can use the rocket variable in my View without ForEach looping.
Plead help me dear experts. I've already broken my brain trying to figure out for a whole week, what I should do.

The data in the preview area has nothing to do with the received data. If you want a preview you have to provide a custom instance.
A usual way is to add a static example property to the structs like this
struct StageInfo: Codable {
let engines: Int
let fuelAmountTons: Double
let burnTimeSec: Int?
enum CodingKeys: String, CodingKey {
case engines
case fuelAmountTons = "fuel_amount_tons"
case burnTimeSec = "burn_time_sec"
}
static let firstStage = StageInfo(engines: 1, fuelAmountTons: 44.3, burnTimeSec: 169)
static let secondStage = StageInfo(engines: 1, fuelAmountTons: 3.30, burnTimeSec: 378)
}
struct Rocket: Codable, Identifiable {
let id = UUID()
let name: String
let firstFlight: String
let country: String
let costPerLaunch: Int
let firstStage: StageInfo
let secondStage: StageInfo
enum CodingKeys: String, CodingKey {
case id
case name
case firstFlight = "first_flight"
case country
case costPerLaunch = "cost_per_launch"
case firstStage = "first_stage"
case secondStage = "second_stage"
}
static let example = Rocket(id: UUID(), name: "Falcon 1", firstFlight: "2006-03-24", country: "Republic of the Marshall Islands", costPerLaunch: 6700000, firstStage: StageInfo.firstStage, secondStage: StageInfo.secondStage)
}
Then just refer to the example
struct RocketRowView_Previews: PreviewProvider {
static var previews: some View {
RocketRowView(rocket: Rocket.example)
}
}

Related

Cannot convert value of type 'String' to expected argument type Event when searching from cloudkit

I'm trying to add a function to my ios app in which the user can search through the list of countries available in CloudKit. To do that I created a search field and I'm trying to read the data available CloudKit and retrieve it but I'm getting the following error.
Cannot convert value of type 'String' to expected argument type Event
Here is the exact location of the error. I'm new to this whole swift thing so I need some help.
This is the page with the search field
import Foundation
import SwiftUI
import CloudKit
struct Event: Identifiable{
let record: CKRecord
let id: CKRecord.ID
let Country: String
let dateTime : Date
init(record: CKRecord){
self.record = record
self.id = record.recordID
self.Country = record["Country"] as? String ?? ""
self.dateTime = record["dateTime"] as? Date ?? Date.now
}
}
struct NewCity: View {
#State private var searchText = ""
#State private var showingNatio = false
#State private var showingNotifi = false
#State var events :[Event] = []
var body: some View {
NavigationView {
VStack{
Text("Search for a country")
.font(.title.weight(.bold))
.foregroundColor(Color.gray)
.searchable(text: $searchText){
ForEach(events) { city in
RowView(city: city)
}
}.onChange(of: searchText) { searchText in
events = events.filter({ city in
city.Country.lowercased().contains(searchText)
})
}
.navigationTitle("Countries")
Text("Start searching for a country \n to add it to your list")
.foregroundColor(Color.gray)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
func fetchEvent(){
events.removeAll()
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType:"Event", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.recordMatchedBlock = {recordID, result in
switch result{
case .success(let record):
let event = Event(record: record)
events.append(event)
case .failure(let error):
print("Error:\(error.localizedDescription)")
}
}
CKContainer.default().publicCloudDatabase.add(operation)
}
}
struct NewCity_Previews: PreviewProvider {
static var previews: some View {
NewCity()
}
}
This page is the row view which displays the format of each row when searched for
import Foundation
import SwiftUI
struct RowView: View {
var city: Event
var body: some View {
HStack {
VStack (alignment: .leading, spacing: 4) {
Text (city.Country)
.font(.title2.weight(.semibold))
}
}
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(city: Event.Country)
.previewLayout(.sizeThatFits)
}
}
Your struct RowView is defined as follows:
struct RowView: View {
var city: Event
// etc
So its initialiser takes an Event
init(city: Event)
You're calling it as follows:
RowView(city: Event.Country)
and Event.Country is defined as:
let Country: String
i.e a String
hence the error message.
You need to pass an Event when you create a RowView

How can I use onDisappear modifier for knowing the killed View in SwiftUI?

I have a study project also you can call it testing/ learning project! Which has a goal to recreate the data source and trying get sinked with latest updates to data source!
In this project I was successful to create the same data source without sending or showing data source to the Custom View! like this example project in down, so I want keep the duplicated created data source with original data source updated!
For example: I am deleting an element in original data source and I am trying read the id of the View that get disappeared! for deleting the same one in duplicated one! but it does not work well!
The Goal: As you can see in my project and my codes! I want create and keep the duplicated data source sinked and equal to original without sending the data source directly to custom View! or even sending any kind of notification! the goal is that original data source should put zero work to make itself known to duplicated data source and the all work is belong to custom View to figure out the deleted item.
PS: please do not ask about use case or something like that! I am experimenting this method to see if it would work or where it could be helpful!
struct ContentView: View {
#State private var array: [Int] = Array(0...3)
var body: some View {
VStack(alignment: .leading, spacing: 10.0) {
ForEach(array.indices, id:\.self) { index in
CustomView(id: index) {
HStack {
Text(String(describing: array[index]))
.frame(width: 50.0)
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture { array.remove(at: index) }
}
}
}
}
.font(Font.body.bold())
}
}
struct CustomView<Content: View>: View {
let id: Int
let content: () -> Content
#StateObject private var customModel: CustomModel = CustomModel.shared
var body: some View {
content()
.preference(key: IntPreferenceKey.self, value: id)
.onPreferenceChange(IntPreferenceKey.self) { newValue in
if !customModel.array.contains(newValue) { customModel.array.append(newValue) }
}
.onDisappear(perform: {
print("id:", id, "is going get removed!")
customModel.array.remove(at: id)
})
}
}
class CustomModel: ObservableObject {
static let shared: CustomModel = CustomModel()
#Published var array: Array<Int> = Array<Int>() {
didSet {
print(array.sorted())
}
}
}
struct IntPreferenceKey: PreferenceKey {
static var defaultValue: Int { get { return Int() } }
static func reduce(value: inout Int, nextValue: () -> Int) { value = nextValue() }
}
Relying on array indexes for ForEach will be unreliable for this sort of work, since the ID you're using is self -- ie the index of the item. This will result in unreliable recalculations of the items in ForEach since they're not actually identifiable by their index. For example, if item 0 gets removed, then what was item 1 now becomes item 0, making using an index as the identifier relatively useless.
Instead, use actual unique IDs to describe your models and everything works as expected:
struct Item: Identifiable {
let id = UUID()
var label : Int
}
struct ContentView: View {
#State private var array: [Item] = [.init(label: 0),.init(label: 1),.init(label: 2),.init(label: 3)]
var body: some View {
VStack(alignment: .leading, spacing: 10.0) {
ForEach(array) { item in
CustomView(id: item.id) {
HStack {
Text("\(item.label) - \(item.id)")
.frame(width: 50.0)
Image(systemName: "trash")
.foregroundColor(.red)
.onTapGesture { array.removeAll(where: { $0.id == item.id }) }
}
}
}
}
.font(.body.bold())
}
}
struct CustomView<Content: View>: View {
let id: UUID
let content: () -> Content
#StateObject private var customModel: CustomModel = CustomModel.shared
var body: some View {
content()
.preference(key: UUIDPreferenceKey.self, value: id)
.onPreferenceChange(UUIDPreferenceKey.self) { newValue in
if !customModel.array.contains(where: { $0 == id }) {
customModel.array.append(id)
}
}
.onDisappear(perform: {
print("id:", id, "is going to get removed!")
customModel.array.removeAll(where: { $0 == id })
})
}
}
class CustomModel: ObservableObject {
static let shared: CustomModel = CustomModel()
#Published var array: Array<UUID> = Array<UUID>() {
didSet {
print(array)
}
}
}
struct UUIDPreferenceKey: PreferenceKey {
static var defaultValue: UUID { get { return UUID() } }
static func reduce(value: inout UUID, nextValue: () -> UUID) { value = nextValue() }
}

Unnamed argument #2 must precede argument 'destination'

I am trying to make a list of eatery locations each of which are displayed in an EateryRow which is able to be clicked to move to the EateryDetail page, however with the implementation of this code I get an error which I believe is related to the syntax of the NavigationLink argument.
Also: I found this question which seems to have the same problem as me but it remains unanswered.
import SwiftUI
struct EateryList: View {
#Binding var eateries: [Eatery]
var body: some View {
NavigationView {
VStack {
List {
ForEach(eateries) {
NavigationLink(destination: EateryDetail(eatery: $eateries[identifiedBy: $0])) { //error here
EateryRow(eatery: $eateries[identifiedBy: $0])
}
}
.onMove {
eateries.move(fromOffsets: $0, toOffset: $1)
EateriesApp.save()
}.onDelete {
eateries.remove(atOffsets: $0)
EateriesApp.save()
}
}
.navigationTitle("Favourite Eateries")
.navigationBarItems(leading: EditButton(), trailing: Button( action: add)
{
Image(systemName: "plus")
}
)
.listStyle(InsetGroupedListStyle())
}
}
}
func add() {
eateries.append(Eatery(name: "Eatery", location: "Insert location here", notes: "Insert notes here", reviews: ["Insert reviews here"], url: "https://i.imgur.com/y3MMnba.png"))
EateriesApp.save()
}
}
I get this error on the line with the NavigationLink:
Unnamed argument #2 must precede argument 'destination'
For further clarity this is how I've used the "eatery" variable in the EateryDetail and EatertyRow views:
struct EateryDetail: View {
#Binding var eatery: Eatery
struct EateryRow: View {
#Binding var eatery: Eatery
And here is my code for Eatery which is defined in a file called eateries.swift:
import Foundation
struct Eatery: Codable, Identifiable {
var id = UUID()
var name: String
var location: String
var notes: String
var reviews: [String] = []
var url: String = ""
}
In eateriesApp.swift this is also defined:
import SwiftUI
#main
struct EateriesApp: App {
#State var model: [Eatery] = EateriesApp.model
static var model: [Eatery] = {
guard let data = try? Data(contentsOf: EateriesApp.fileURL),
let model = try? JSONDecoder().decode([Eatery].self, from: data) else {
return [elCaminoCantina, theFineDine, nightBites, theRiverRodeo, theCozyKitchen, theElegantEatery]
}
return model
}()
static var modelBinding: Binding<[Eatery]>?
var body: some Scene {
EateriesApp.modelBinding = $model
return WindowGroup {
ContentView(eateries: $model)
}
}
You should need to use .indices in ForEach(eateries).
Like this
ForEach(eateries.indices) { index in
NavigationLink(destination: EateryDetail(eatery: $eateries[index])) { //error here
EateryRow(eatery: $eateries[index])
}
}
The problem is you are using a shorthand variable ($0). When you used $0 inside the NavigationLink then $0 is considered for NavigationLink not ForEach. so now both $0 are in conflict in your case.
You can check with the below code. In below code now not produce any error because now there is no use $0 inside the NavigationLink
ForEach(eateries) {
Text($0.name)
NavigationLink(destination: EateryDetail(eatery: $eateries[identifiedBy: $0])) {
Text("$0.name")
}
}
Another solution is to use one variable and store your $0 data like this.
ForEach(eateries) {
let eaterie = $0 //<--- Here
NavigationLink(destination: EateryDetail(eatery: $eateries[identifiedBy: eaterie])) { //<--- Here
EateryRow(eatery: $eateries[identifiedBy: eaterie]) //<--- Here
}
}

Picker in NavigationView: No refresh when custom binding changes

I'm trying to save an enum value to UserDefaults. Therefor I created a custom binding. The UserDefault part works fine but unfortunately the value in the NavigationView does not get refreshed after changing the value with the Picker.
How can I get the chosen weather condition to be displayed?
PS: If not creating additional complexity, I would like to keep the enum Weather of type Int.
import SwiftUI
enum Weather: Int, CaseIterable {
case rain
case sun
case clouds
}
func getWeatherText(weather: Weather) -> String {
switch weather {
case .rain: return "Rain"
case .sun: return "Sun"
case .clouds: return "Clouds"
}
}
struct ContentView: View {
let currentWeather = Binding<Weather>(
get: {
(Weather(rawValue: UserDefaults.standard.integer(forKey: "weather")) ?? .sun)
},
set: {
UserDefaults.standard.set($0.rawValue, forKey: "weather")
}
)
let weathers = Weather.allCases
var body: some View {
NavigationView{
Form{
Picker("Weather", selection: currentWeather) {
ForEach(weathers, id: \.self) { w in
Text(getWeatherText(weather: w)).tag(w)
}
}
}
}
}
}
Thanks to the comment of Asperi, the solution is simple: replacing my custom Binding with #AppStorage. This is the result:
import SwiftUI
enum Weather: Int, CaseIterable {
case rain
case sun
case clouds
}
func getWeatherText(weather: Weather) -> String {
switch weather {
case .rain: return "Rain"
case .sun: return "Sun"
case .clouds: return "Clouds"
}
}
struct ContentView: View {
#AppStorage("weather") var currentWeather: Weather = Weather.clouds
let weathers = Weather.allCases
var body: some View {
NavigationView{
Form{
Picker("Weather", selection: $currentWeather) {
ForEach(weathers, id: \.self) { w in
Text(getWeatherText(weather: w)).tag(w)
}
}
}
}
}
}

Initializer 'init(_:)' requires that '' conform to 'StringProtocol' SwiftUI Picker with Firebase

I have a piece of code where I'm trying to place Firestore data within a picker. I have made it previously so that the picker will show the Firestore data, but I am unable to select it to show in the 'selected view' therefore I rewrote the code and have the following error "Initializer 'init(_:)' requires that 'getSchoolData' conform to 'StringProtocol' "
Please excuse that it may sound like a daft question I just can't seem to solve it. A copy of my code is below. I have tried working on this for weeks but at a loss so please be kind, I'm new to coding.
Thanks in advance,
import SwiftUI
import Firebase
struct SchoolDetailsView: View {
let schoolData = [getSchoolData()]
#State var selectedSchool = 0
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(0 ..< schoolData.count) {
Text(self.schoolData[$0])
}
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("Select your school")
}
}
}
struct SchoolPicker_Previews: PreviewProvider {
static var previews: some View {
SchoolDetailsView()
}
}
class getSchoolData : ObservableObject{
#Published var datas = [schoolName]()
init() {
let db = Firestore.firestore()
db.collection("School Name").addSnapshotListener { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
for i in snap!.documentChanges{
let id = i.document.documentID
let name = i.document.get("Name") as! String
self.datas.append(schoolName(id: id, name: name))
}
}
}
}
struct schoolName : Identifiable {
var id : String
var name : String
}
You may try the following:
struct SchoolDetailsView: View {
#ObservedObject var schoolData = getSchoolData() // make `#ObservedObject`/`#StateObject` instead of const array
#State var selectedSchool = "" // `schoolName.id` is of type String
var body: some View {
VStack {
Form {
Section {
Picker(selection: $selectedSchool, label: Text("School Name")) {
ForEach(schoolData.datas, id: \.id) { // choose whether you want to tag by `id` or by `name`
Text($0.name)
}
}
Text("Selected School: \(selectedSchool)")
}
}.navigationBarTitle("Select your school")
}
}
}