Read Json and Add to picker swiftUI - swiftui

struct Test: View {
#ObservedObject var datas2 = ReadData2()
#State var selectedIndex2 = 0
var body: some View {
VStack{
List{
HStack{
Text("From")
.foregroundColor(Color("Color"))
.fontWeight(.bold)
.font(.system(size: 20))
.padding(.leading, 10.0)
.frame(width: nil, height: nil, alignment: .leading)
Picker(selection: $selectedIndex2, label: Text("")) {
ForEach(0 ..< datas2.cities.count, id: \.self) {
Text(datas2.cities[$0].name)
}
}.foregroundColor(Color("Color"))
.padding(.trailing)
}
}
}
}
struct City: Codable, Identifiable {
enum CodingKeys: CodingKey {
case name
}
var id = UUID()
var name: String
}
class ReadData2: ObservableObject {
#Published var cities = [City]()
init(){
loadData()
}
func loadData() {
guard let url = Bundle.main.url(forResource: "cities", withExtension: "json")
else {
print("Json file not found")
return
}
let data = try? Data(contentsOf: url)
let cities = try? JSONDecoder().decode([City].self, from: data!)
self.cities = cities!
}
}
and I have a json file called cities that is like this :
{
"country": "AD",
"name": "Sant Julià de Lòria",
"lat": "42.46372",
"lng": "1.49129"
},
{
"country": "AD",
"name": "Pas de la Casa",
"lat": "42.54277",
"lng": "1.73361"
},
{
"country": "AD",
"name": "Ordino",
"lat": "42.55623",
"lng": "1.53319"
},
{
"country": "AD",
"name": "les Escaldes",
"lat": "42.50729",
"lng": "1.53414"
},
{
"country": "AD",
"name": "la Massana",
"lat": "42.54499",
"lng": "1.51483"
},
My problem is that cites are too much data, and once I press the picker it takes a long time to read data and post data inside the picker, is there a better way for making it faster ?
and I am trying to make it searchable throught a search text taking the first letter of the city and it is not working !
I hope you can help me !

With very large number of items, I would suggest using a List or a LazyVStack that are optimised for that, instead of a Picker.
Something like the following example code.
Note, you will have to adjust the UI for your purpose.
struct ContentView: View {
var body: some View {
Test()
}
}
struct Test: View {
#StateObject var datas2 = ReadData2()
#State var selectedCity: City?
#State private var searchQuery: String = ""
var body: some View {
VStack (spacing: 20) {
TextField("city search", text: $searchQuery).padding(5)
.overlay(RoundedRectangle(cornerRadius: 15).stroke(Color.blue, lineWidth: 1))
.foregroundColor(.blue)
.frame(width: 160)
.padding(.top, 20.0)
HStack {
Text("From")
.fontWeight(.bold)
.font(.system(size: 20))
.padding(.leading, 10.0)
Spacer()
ScrollView {
LazyVStack {
ForEach(datas2.cities.filter{searchFor($0.name)}.sorted(by: { $0.name < $1.name })) { city in
Text(city.name).foregroundColor(selectedCity == city ? .red : .blue)
.onTapGesture {
selectedCity = city
}
}
}
}.frame(width: 222, height: 111)
}
Spacer()
}
}
private func searchFor(_ txt: String) -> Bool {
return (txt.lowercased(with: .current).hasPrefix(searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(with: .current)) || searchQuery.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
struct City: Codable, Identifiable, Hashable {
enum CodingKeys: CodingKey {
case name
}
var id = UUID()
var name: String
}
class ReadData2: ObservableObject {
#Published var cities = [City]()
init() {
loadData()
}
func loadData() {
if let url = Bundle.main.url(forResource: "cities", withExtension: "json") {
do {
let data = try Data(contentsOf: url)
cities = try JSONDecoder().decode([City].self, from: data)
} catch {
print(" error:\(error)")
}
}
}
}

Related

SwiftUI: Match answer index with option index in Quiz game

I'm building a quiz game and am fairly new to SwiftUI. When the quiz is over, I'd like the user to be able to see all the correct answers. Basically, I want to list all questions with the correct answer, but I'm not sure how to match the answer index (Int) with the correct option (String).
Any help is appreciated!
Here's my JSON Structure:
[
{
"id": "1",
"question": "Heres a question title",
"category": "sports",
"answer": 0,
"options": [
"A",
"B",
"C",
"D"
]
}
...
]
Here's my Model:
struct Question: Identifiable, Codable, Hashable {
var id: String
var question: String
var category: String
var answer: Int
var options: [String]
enum CodingKeys: String, CodingKey {
case id, question, category, answer, options
}
static var allQuestions: [Question] = Bundle.main.decode("quizquestions2022.json")
}
And here's the sheet view where I want to list all answers:
struct AnswerSheetView: View {
#StateObject var gameManager = GameManagerVM()
#Environment(\.presentationMode) var presentationMode
var body: some View {
ScrollView(showsIndicators: false) {
ForEach(gameManager.allQuestions, id: \.self) { question in
VStack(spacing: 16) {
CategoryTagView(name: question.category, size: 12)
Text(question.question)
Text("\(question.answer)")
.foregroundColor(Color(UIColor.systemGreen))
}
.font(.system(size: 20, weight: .bold, design: .rounded))
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding()
.background(Color(UIColor.systemGray6))
.cornerRadius(24)
}
}
.padding()
.background(Color(UIColor.black).ignoresSafeArea())
.navigationTitle("Facit")
.navigationBarTitleDisplayMode(.inline)
}
}
you could try something like this approach, to match the answer index (Int) with the correct option (String):
struct Question: Identifiable, Codable, Hashable {
var id: String
var question: String
var category: String
var answer: Int
var options: [String]
enum CodingKeys: String, CodingKey {
case id, question, category, answer, options
}
// this should be in your GameManagerVM, not here
static var allQuestions: [Question] = Bundle.main.decode("quizquestions2022.json")
func theAnswer() -> String { // <--- here
return (answer >= 0 && answer < options.count) ? options[answer] : ""
}
}
struct AnswerSheetView: View {
#StateObject var gameManager = GameManagerVM()
#Environment(\.presentationMode) var presentationMode
var body: some View {
ScrollView(showsIndicators: false) {
ForEach(gameManager.allQuestions, id: \.self) { question in
VStack(spacing: 16) {
CategoryTagView(name: question.category, size: 12)
Text(question.question)
// --- here
Text(question.theAnswer()).foregroundColor(Color(UIColor.systemGreen))
}
.font(.system(size: 20, weight: .bold, design: .rounded))
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding()
.background(Color(UIColor.systemGray6))
.cornerRadius(24)
}
}
.padding()
.background(Color(UIColor.black).ignoresSafeArea())
.navigationTitle("Facit")
.navigationBarTitleDisplayMode(.inline)
}
}

SwiftUI - Update data on Firebase's Realtime database

I have successfully displayed the data to the UI, but I want the user to be able to update my data again when tapping the "Save" button . Hope you can help me!
Profile
I have successfully displayed the data to the UI, but I want the user to be able to update my data again when tapping the "Save" button . Hope you can help me!
There are many ways to achieve what you want. This is just one approach, by
passing the profileViewModel to EditProfile:
class ProfileViewModel: ObservableObject {
#Published var user = Profile(id: "", image: "", birthDay: "", role: [], gender: "", name: "")
private var ref: DatabaseReference = Database.database().reference()
func fetchData(userId: String? = nil) {
// 8hOqqnFlfGZTj1u5tCkTdxAED2I3
ref.child("users").child(userId ?? "default").observe(.value) { [weak self] (snapshot) in
guard let self = self,
let value = snapshot.value else { return }
do {
print("user: \(value)")
self.user = try FirebaseDecoder().decode(Profile.self, from: value)
} catch let error {
print(error)
}
}
}
func saveUser() {
// save the user using your ref DatabaseReference
// using setValue, or updateChildValues
// see https://firebase.google.com/docs/database/ios/read-and-write
}
}
struct EditProfile: View {
#ObservedObject var profileViewModel: ProfileViewModel // <--- here
var body: some View {
VStack {
Text(profileViewModel.user.name) // <--- you probably meant TextField
.font(.custom("Poppins-Regular", size: 15))
.foregroundColor(Color.black)
Text("\(profileViewModel.user.birthDay)!")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Text("\(profileViewModel.user.gender)")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Text(profileViewModel.user.role.first ?? "")
.font(.custom("Poppins-Regular", size: 22))
.fontWeight(.bold)
.foregroundColor(Color.black)
Button(action: {
// save the profileViewModel.user to database
profileViewModel.saveUser() // <--- here
}) {
Text("Save")
}
}
.padding()
}
}
struct CategoriesView: View {
#ObservedObject var viewModel = SectionViewModel()
#EnvironmentObject var loginViewModel : LoginViewModel
#StateObject var profileViewModel = ProfileViewModel()
var body: some View {
ZStack {
VStack (alignment: .leading, spacing:0) {
EditProfile(profileViewModel: profileViewModel) // <--- here
.padding()
.padding(.bottom,-10)
}
}
.onAppear() {
self.viewModel.fetchData()
profileViewModel.fetchData(userId: loginViewModel.session?.uid)
}
}
}
EDIT1: regarding the updated code.
In your new code, in ProfileHost you are not passing ProfileViewModel.
Use:
NavigationLink(destination: ProfileEditor(profileViewModel: viewModel)) {
ProfileRow(profileSetting: profile)
}
And in ProfileEditor replace profile with profileViewModel.user
You will probably need to adjust profileItem and put it in a .onAppear {...} . Something like this:
struct ProfileEditor: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#ObservedObject var profileViewModel: ProfileViewModel
#EnvironmentObject var loginViewModel: LoginViewModel
let profileLabel: [String] = ["Name", "Account", "Gender", "Role", "Email"]
#State var profileItem: [String] = []
#State var profileEditorRow: [ProfileEditorItem] = []
var body: some View {
VStack {
ForEach(profileEditorRow) { editor in
if editor.id == 5 {
ProfileEditorRow(editor: editor, showLastLine: true)
} else {
ProfileEditorRow(editor: editor, showLastLine: false)
}
}
Button("Save") {
profileViewModel.updateData(userId: loginViewModel.session?.uid)
}
}
.onAppear {
profileItem = [profileViewModel.user.name,
profileViewModel.user.birthDay,
profileViewModel.user.gender,
profileViewModel.user.role.first ?? "",
profileViewModel.user.birthDay]
for n in 1...5 {
profileEditorRow.append(ProfileEditorItem(id: n, label: profileLabel[n-1], item: profileItem[n-1]))
}
}
}
}
EDIT2: update func
func updateData() {
ref.("users").child(user.id).updateChildValues([
"name": user.name,
"birthDay": user.birthDay,
"gender": user.gender,
"role": user.role.first ?? ""])
}
and use this in ProfileEditor :
Button("Save") {
profileViewModel.updateData()
}

The compiler is unable to type-check this expression in reasonable time, My view is not too big though

The following file fails every single time and it won't even let me see a preview of the view. I have know idea what is going on. I have quit Xcode, I have cleaned the build folder. Everything I can think of, I have done. I am working on Xcode version 12.2. Anybody have any ideas?
struct weatherToggle : Identifiable {
let id = UUID()
var value : Bool = false
}
struct Sliders : Identifiable {
var id: UUID
var percent: Double
var name: String
init( percent: Double, name: String ) {
self.id = UUID()
self.percent = percent
self.name = name
}
}
struct MyNodeView : View {
#Binding var myNode : Sliders
var body: some View {
HStack {
Text("\(String(format: "%.f", myNode.percent))%").font(.footnote)
Slider(value: $myNode.percent, in: 0 ... 100).padding()
}
}
}
struct OperatingConditionsView: View {
#State var selection: Int? = nil
let lightGray = Color(hue: 1.0, saturation: 0.0, brightness: 0.392)
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var defensiveLayers = [Sliders]()
#State var sensorsAndSupports = [Sliders]()
// #Binding public var threats: [Any]
#State var availableLaunchPlatformSelections: [String] = []
#State var items = [weatherToggle(),
weatherToggle(),
weatherToggle()]
#State public var battery = "South Korea"
#State var atmosphericsSeverity = [String]()
#State var heliosphericsSeverity = [String]()
var conditions = ["light", "moderate", "severe"]
#State var heliospherics = [String]()
#State var atmospherics = [String]()
//#State var defensiveLayers = [String]()
#State var availableLaunchPlatforms = [String]()
//#State var sensorsAndSupport = [String]()
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("OPERATING CONDITIONS").fontWeight(.bold)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
Button("load stored attack parameter set"){
}.padding(.leading, 30)
Text("ASSET READINESS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
Group {
Text("Available launch platforms").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
VStack {
List {
ForEach(availableLaunchPlatforms, id: \.self) { launchPlatform in
MultipleSelectionRow(title: launchPlatform, isSelected: self.availableLaunchPlatformSelections.contains(launchPlatform)) {
if self.availableLaunchPlatformSelections.contains(launchPlatform) {
self.availableLaunchPlatformSelections.removeAll(where: { $0 == launchPlatform })
}
else {
self.availableLaunchPlatformSelections.append(launchPlatform)
}
}.font(.custom("Gill Sans", size: 12)).foregroundColor(.gray)
}
}.frame(height: 250).font(.footnote)
}
}
Group {
Text("Other defensive layers").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
MyNodeView(myNode: $defensiveLayers[i])
}
}
}.padding(.horizontal, 30)
}
Group {
Text("Sensors & Support").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
Text(sensorsAndSupports[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
MyNodeView(myNode: $sensorsAndSupports[i])
}
}
}.padding(.horizontal, 30)
}
Group {
Text("ATMOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< atmospherics.count, id: \.self) { i in
HStack {
Menu {
ForEach(0 ..< conditions.count) { j in
Button(conditions[j]) {
atmosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(atmospherics[i])
Image(systemName: "cloud.drizzle")
}.frame(maxWidth: .infinity)
Text("Current: " + atmosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
Group {
Text("HELIOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< heliospherics.count, id: \.self) { i in
HStack {
Menu {
ForEach(0 ..< conditions.count) { j in
Button(conditions[j]) {
heliosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(heliospherics[i])
Image(systemName: "cloud.drizzle")
}.frame(maxWidth: .infinity)
Text("Current: " + heliosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
HStack {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.frame(maxWidth: .infinity)
Button("Run"){
let jsonObj: Any = ["Threats": [], "launchPlatforms": availableLaunchPlatformSelections, "defensiveLayers": defensiveLayers.map({ ["layer": $0.name, "percentage": Int($0.percent) ] }), "sensorsAndSupports": sensorsAndSupports.map({ ["SensorSupport": $0.name, "percentage": Int($0.percent) ] }), "atmospherics:": atmosphericsSeverity.map({ ["weather": "", "intensity": $0 ] })]
print(convertJSON(array: jsonObj))
}.foregroundColor(.red).frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity).padding(.all, 30)
}
}.onAppear() {
loadOpConditions(country: battery.replacingOccurrences(of: " ", with: ""), completionHandler: { (data: [Dictionary<String, Any>]) in
for row in data {
for _ in row["atmosperics"] as! [String]
{
atmosphericsSeverity.append("light")
}
for i in row["heliospherics"] as! [String]
{
heliosphericsSeverity.append("light")
}
heliospherics = row["heliospherics"] as! [String]
atmospherics = row["atmosperics"] as! [String]
for i in row["sensorsAndSupport"] as! [String]
{
sensorsAndSupports.append(Sliders(percent: 0, name: i))
}
for i in row["defensiveLayers"] as! [String]
{
defensiveLayers.append(Sliders(percent: 0, name: i ))
}
availableLaunchPlatforms = row["availableLaunchPlatforms"] as! [String]
}
})
}
}
func convertJSON(array: Any) -> String
{
do {
let jsonData = try JSONSerialization.data(withJSONObject: array, options: [])
if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
//print(jsonString)
return jsonString
}
else
{
return ""
}
} catch {
return "error"
}
}
func loadOpConditions(country: String, completionHandler: #escaping ([Dictionary<String, Any>]) -> Void) -> Void {
var request = URLRequest(url: URL(string: "https://salusdatalab.com/api/OperatingConditions/" + country)!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
// request.addValue("Bearer \(LoginViewController.myToken.bearerToken)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data
else {
//p rint(String(describing: error))
return
}
//p rint(String(data: data, encoding: .utf8)!)
guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
else { return }
guard let rootArray = json as? Array<Dictionary<String,Any>>
else { return }
// The outer/root array seems useless from what you have shown in your JSON above, so this is to get to the array of dictionaries.
completionHandler(rootArray)
}
task.resume()
}
}
struct OperatingConditionsView_Previews: PreviewProvider {
static var previews: some View {
OperatingConditionsView()
}
}
I am putting this in here because it says your post is mostly code. Thank you for your help in advance.
To debug this type of situation, you'll generally want to split your view/body section up into multiple chunks that you can comment out/modify.
By doing this, I narrowed it down to your fourth and fifth groups. Code first, then explanation:
struct OperatingConditionsView: View {
#State var selection: Int? = nil
let lightGray = Color(hue: 1.0, saturation: 0.0, brightness: 0.392)
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var defensiveLayers = [Sliders]()
#State var sensorsAndSupports = [Sliders]()
// #Binding public var threats: [Any]
#State var availableLaunchPlatformSelections: [String] = []
#State var items = [weatherToggle(),
weatherToggle(),
weatherToggle()]
#State public var battery = "South Korea"
#State var atmosphericsSeverity = [String]()
#State var heliosphericsSeverity = [String]()
var conditions = ["light", "moderate", "severe"]
#State var heliospherics = [String]()
#State var atmospherics = [String]()
//#State var defensiveLayers = [String]()
#State var availableLaunchPlatforms = [String]()
//#State var sensorsAndSupport = [String]()
var topSection: some View {
Group {
Text("OPERATING CONDITIONS").fontWeight(.bold)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
Button("load stored attack parameter set"){
}.padding(.leading, 30)
Text("ASSET READINESS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
}
}
var firstGroup : some View {
Group {
Text("Available launch platforms").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
VStack {
List {
ForEach(availableLaunchPlatforms, id: \.self) { launchPlatform in
// MultipleSelectionRow(title: launchPlatform, isSelected: self.availableLaunchPlatformSelections.contains(launchPlatform)) {
// if self.availableLaunchPlatformSelections.contains(launchPlatform) {
// self.availableLaunchPlatformSelections.removeAll(where: { $0 == launchPlatform })
// }
// else {
// self.availableLaunchPlatformSelections.append(launchPlatform)
// }
// }.font(.custom("Gill Sans", size: 12)).foregroundColor(.gray)
}
}.frame(height: 250).font(.footnote)
}
}
}
//
var secondGroup : some View {
Group {
Text("Other defensive layers").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
Text(defensiveLayers[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(defensiveLayers.indices, id: \.self) { i in
MyNodeView(myNode: $defensiveLayers[i])
}
}
}.padding(.horizontal, 30)
}
}
//
var thirdGroup : some View {
Group {
Text("Sensors & Support").fontWeight(.bold).foregroundColor(lightGray).padding(.vertical).padding(.leading, 30).font(.system(size: 15))
HStack {
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
Text(sensorsAndSupports[i].name).font(.custom("Gill Sans", size: 12)).padding(.trailing).foregroundColor(.gray).frame(maxHeight: .infinity)
}
}
VStack {
ForEach(sensorsAndSupports.indices, id: \.self) { i in
MyNodeView(myNode: $sensorsAndSupports[i])
}
}
}.padding(.horizontal, 30)
}
}
//
var fourthGroup : some View {
Group {
Text("ATMOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< atmospherics.count, id: \.self) { (i:Int) in //<-- HERE
HStack {
Menu {
ForEach(0 ..< conditions.count) { (j:Int) in //<-- HERE
Button(conditions[j]) {
atmosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(atmospherics[i])
Image(systemName: "cloud.drizzle")
}
.frame(maxWidth: .infinity)
Text("Current: " + atmosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}
.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
}
//
var fifthGroup : some View {
Group {
Text("HELIOSPHERICS").fontWeight(.bold).font(.subheadline)
.foregroundColor(Color(hue: 0.651, saturation: 1.0, brightness: 0.465))
.multilineTextAlignment(.center).padding(.vertical).frame(maxWidth: .infinity)
ForEach(0 ..< heliospherics.count, id: \.self) { (i:Int) in //<-- HERE
HStack {
Menu {
ForEach(0 ..< conditions.count) { (j:Int) in //<-- HERE
Button(conditions[j]) {
heliosphericsSeverity[i] = conditions[j]
}
}
} label: {
Text(heliospherics[i])
Image(systemName: "cloud.drizzle")
}.frame(maxWidth: .infinity)
Text("Current: " + heliosphericsSeverity[i]).frame(maxWidth: .infinity).font(.footnote).foregroundColor(.gray)
}.padding()
.foregroundColor(.white).background(LinearGradient(gradient: Gradient(colors: [Color.black, Color.blue]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(10)
.padding()
}
}
}
var lastButtons : some View {
HStack {
Button("Cancel") {
self.presentationMode.wrappedValue.dismiss()
}.frame(maxWidth: .infinity)
Button("Run"){
let jsonObj: Any = ["Threats": [], "launchPlatforms": availableLaunchPlatformSelections, "defensiveLayers": defensiveLayers.map({ ["layer": $0.name, "percentage": Int($0.percent) ] }), "sensorsAndSupports": sensorsAndSupports.map({ ["SensorSupport": $0.name, "percentage": Int($0.percent) ] }), "atmospherics:": atmosphericsSeverity.map({ ["weather": "", "intensity": $0 ] })]
print(convertJSON(array: jsonObj))
}.foregroundColor(.red).frame(maxWidth: .infinity)
}.frame(maxWidth: .infinity).padding(.all, 30)
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
topSection
firstGroup
secondGroup
thirdGroup
fourthGroup
fifthGroup
lastButtons
}
}.onAppear() {
onAppearFunc()
}
}
func onAppearFunc() {
loadOpConditions(country: battery.replacingOccurrences(of: " ", with: ""), completionHandler: { (data: [Dictionary<String, Any>]) in
for row in data {
for _ in row["atmosperics"] as! [String]
{
atmosphericsSeverity.append("light")
}
for i in row["heliospherics"] as! [String]
{
heliosphericsSeverity.append("light")
}
heliospherics = row["heliospherics"] as! [String]
atmospherics = row["atmosperics"] as! [String]
for i in row["sensorsAndSupport"] as! [String]
{
sensorsAndSupports.append(Sliders(percent: 0, name: i))
}
for i in row["defensiveLayers"] as! [String]
{
defensiveLayers.append(Sliders(percent: 0, name: i ))
}
availableLaunchPlatforms = row["availableLaunchPlatforms"] as! [String]
}
})
}
func convertJSON(array: Any) -> String
{
do {
let jsonData = try JSONSerialization.data(withJSONObject: array, options: [])
if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
//print(jsonString)
return jsonString
}
else
{
return ""
}
} catch {
return "error"
}
}
func loadOpConditions(country: String, completionHandler: #escaping ([Dictionary<String, Any>]) -> Void) -> Void {
var request = URLRequest(url: URL(string: "https://salusdatalab.com/api/OperatingConditions/" + country)!,timeoutInterval: Double.infinity)
request.httpMethod = "GET"
// request.addValue("Bearer \(LoginViewController.myToken.bearerToken)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data
else {
//p rint(String(describing: error))
return
}
//p rint(String(data: data, encoding: .utf8)!)
guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
else { return }
guard let rootArray = json as? Array<Dictionary<String,Any>>
else { return }
// The outer/root array seems useless from what you have shown in your JSON above, so this is to get to the array of dictionaries.
completionHandler(rootArray)
}
task.resume()
}
}
I noticed that if the fourth and fifth groups were commented out, the compile time was dramatically improved. So, I started digging into them.
Often, Swift compile times are adversely affected by trying to do type inference. In this case, specifying the type of the ForEach closure parameter (see my //<-- HERE lines) seems to have fixed the compilation time.
Of course, this exact fix won't work every time, but hopefully I've outlined the strategy for how to debug it.
Note: I've left something commented out where you use MultipleSelectionRow -- make sure that if you're including code here on SO, you include all of the relevant information for someone to compile it

SwiftUI: cannot delete Row in List

i have a small swiftUI programm in Xcode which let me create and delete Users in a list with a stepper to count points of the users.
everything works fine (adding users, renaming users, stepper counting) unless the deletion of the user.
it throws an error:
Fatal error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444 2020-05-23 12:06:22.854920+0200 Counter[21328:1125981] Fatal
error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444
Here is the code:
import SwiftUI
struct ContentView : View {
#State var isEditing = false
#State var stepperWerte = [3, 5, 7, 9]
#State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(0..<editText.count, id: \.self) {
spieler in HStack {
if self.editText.indices.contains(spieler) {
Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
TextField("", text: self.$editText[spieler], onEditingChanged: {_ in }, onCommit: {self.saveText(id: spieler, Text: self.editText[spieler])} )
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
}
}
}
.onDelete(perform: spielerLoeschen)
.frame(width: nil, height: nil, alignment: .trailing)
}
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
stepperWerte.remove(atOffsets: offsets)
editText.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
stepperWerte.append(startLeben)
editText.append(startName)
}
func saveText(id: Int, Text: String) {
editText[id] = Text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
(ignore the "if" after the HStack, it has no real effect and those extra prints in the last Text to show the index and the total count)
if i dump the arrays (stepperWerte and editText) they are removed the right way -> the player selected for deletion will be removed correctly from the two arrays.
if i change
TextField("", text: self.$editText[spieler]
to
TextField("", text: self.$editText[0]
it works (unless naturally it displays the first player in all rows and i got the same error after deleting all the players (=rows))
any help would be great - thank you!
According to #Asperi i have changed my code to the following:
import SwiftUI
struct BetterTextField : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
}
}
struct ContentView : View {
#State var isEditing = false
#State var stepperWerte = [3, 5, 7, 9]
#State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(0..<editText.count, id: \.self) {
spieler in HStack {
if self.editText.indices.contains(spieler) {
Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
BetterTextField(container: self.$editText, index: self.editText.firstIndex(of: self.editText[spieler])!, text: self.editText[spieler])
Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
}
}
}
.onDelete(perform: spielerLoeschen)
.frame(width: nil, height: nil, alignment: .trailing)
}
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
stepperWerte.remove(atOffsets: offsets)
editText.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
stepperWerte.append(startLeben)
editText.append(startName)
}
func saveText(id: Int, Text: String) {
editText[id] = Text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
... and it works - thank you!
but:
is this a bug in SwiftUI or intentional?
The problem is that you are not using your items directly in the ForEach loop. Consider using structs for your data as objects and make them identifiable.
struct Player : Identifiable {
let id = UUID()
var stepperWerte: Int
var editText : String
}
struct ContentView : View {
#State var isEditing = false
#State var players = [Player(stepperWerte: 3, editText: "Player 1"), Player(stepperWerte: 5, editText: "Player 2"), Player(stepperWerte: 7, editText: "Player 3"), Player(stepperWerte: 9, editText: "Player 4")]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(self.players) { player in
SecondView(player: player)
}
.onDelete(perform: spielerLoeschen)
}
.frame(width: nil, height: nil, alignment: .trailing)
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
players.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
players.insert(Player(stepperWerte: 4, editText: "Neuer Player"), at: 0)
}
}
struct SecondView : View {
var player : Player
#State var stepperWerte : Int
#State var name : String
init(player : Player)
{
self._stepperWerte = State(initialValue: player.stepperWerte)
self._name = State(initialValue: player.editText)
self.player = player
}
var body: some View
{
Stepper(value: self.$stepperWerte, in: -1...10, step: 1, label: {
TextField("", text: self.$name)
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(player.stepperWerte)")
})
}
}
I created a struct Player, and then an array of many Players. In the ForEach you can directly use players as Player confirms to Identifiable protocol. This is way easier as you can access a player object in your ForEach loop and you do not have to access everything with indices. In the deleting function you just delete the object out of the array or add something new to it. Deleting now works fine.
I have removed some code from the list row, just to reproduce it easier, just if you are wondering.

Trying to pass an url to an image loader in SwiftUI gives me nil

So, I'm trying to fill a list with data(in this case, they are books) from a JSON, the JSON contains a image for each book, I asked around and an Image Loader is supposedly the only way to do it.
So let me show you some code.
This is the Image Loader:
struct ImageView: View {
#ObservedObject var imageLoader:ImageLoader
#State var image:UIImage = UIImage()
func imageFromData(_ data:Data) -> UIImage {
UIImage(data: data) ?? UIImage()
}
init(withURL url:String) {
imageLoader = ImageLoader(urlString:url)
}
var body: some View {
VStack {
Image(uiImage: imageLoader.data != nil ? UIImage(data:imageLoader.data!)! : UIImage())
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
}
}
}
class ImageLoader: ObservableObject {
#Published var dataIsValid = false
var data:Data?
init(urlString:String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.dataIsValid = true
self.data = data
}
}
task.resume()
}
}
Take a look at the first line in the VStack in the body: some View of the struct, that's where it crashes and gives me Unexpectedly found nil while unwrapping an Optional value.
Now this is the code for the List:
let apiUrl = "https://qodyhvpf8b.execute-api.us-east-1.amazonaws.com/test/books"
class BooksViewModel: ObservableObject {
#Published var books: [Book] = [
.init(id: 1, nombre: "Libro 1", autor: "Autor 1", disponibilidad: true, popularidad: 100, imagen: "https://www.google.com.ar"),
.init(id: 2, nombre: "Libro 2", autor: "Autor 2", disponibilidad: false, popularidad: 80, imagen: "https://www.google.com.ar"),
.init(id: 3, nombre: "Libro 3", autor: "Autor 3", disponibilidad: true, popularidad: 60, imagen: "https://www.google.com.ar")
]
func fetchBooks() {
guard let url = URL(string: apiUrl) else { return }
URLSession.shared.dataTask(with: url) { (data, resp, err) in
DispatchQueue.main.async {
do {
self.books = try JSONDecoder().decode([Book].self, from: data!)
} catch {
print("Failed to decode JSON: ", error)
}
}
}.resume()
}
}
struct ContentView: View {
#ObservedObject var booksVM = BooksViewModel()
var body: some View {
NavigationView {
List {
VStack(alignment: .leading) {
ForEach(booksVM.books.sorted { $0.popularidad > $1.popularidad}) { book in
HStack {
ImageView(withURL: book.imagen)
Text(book.nombre)
Spacer()
Text(String(book.popularidad))
}
HStack {
Text(book.autor)
Spacer()
if book.disponibilidad {
Text("Disponible")
} else {
Text("No disponible")
}
}
Spacer()
}
}.padding([.leading, .trailing], 5)
}
.navigationBarTitle("Bienvenido a la librería flux")
.onAppear(perform: self.booksVM.fetchBooks)
}
}
}
First line of HStack, ImageView(withURL: book.imagen) that's the line that's supposedly passing the string for the url, but I don't know why, this isn't happening. book.imagen should return a string. I don't know if it's because it's not returning a string, or something is wrong on the Image Loader. Can't figure it out. Rest of the data comes just fine.
Here is a piece of the JSON:
[
{
"id": 1,
"nombre": "The design of every day things",
"autor": "Don Norman",
"disponibilidad": true,
"popularidad": 70,
"imagen": "https://images-na.ssl-images-amazon.com/images/I/410RTQezHYL._SX326_BO1,204,203,200_.jpg"
},
{
"id": 2,
"nombre": "100 años de soledad",
"autor": "Garcia Marquez",
"disponibilidad": false,
"popularidad": 43,
"imagen": "https://images-na.ssl-images-amazon.com/images/I/51egIZUl88L._SX336_BO1,204,203,200_.jpg"
},
{
"id": 3,
"nombre": "El nombre del viento",
"autor": "Patrik Rufus",
"disponibilidad": false,
"popularidad": 80,
"imagen": "https://static.megustaleer.com/images/libros_200_x/EL352799.jpg"
}
]
If it helps, this is what I'm trying to achieve
Please let me know if more clarification is needed.
There is nothing wrong with swiftUI code. Here is the working Model sample codes. Maybe you miss something in your model.
struct Book: Identifiable, Decodable {
var popularidad: Int = 0
var imagen : String = ""
var nombre : String = ""
var autor : String = ""
var disponibilidad : Bool = false
var id : Int = 0
}
class BooksViewModel: ObservableObject {
#Published var books : [Book] = []
func fetchBooks(){
do {
if let url = Bundle.main.url(forResource: "LocalCache", withExtension: "json")
{
if let string = try String(contentsOf: url).data(using: .utf8)
{
self.books = try JSONDecoder().decode([Book].self, from: string)
}
}
}
catch{}
}
}