Picker .onChange doesn't change when choosing default - swiftui-picker

I have pretty much same question as here:
SwiftUI Picker onChange or equivalent?,
but .onChange won't solve it exactly I need.
I have wrote a question in that link, but I was told to ask a new question (as I wasn't able to find answer anywhere :(
I have this enum:
enum FruitTea: String, CustomStringConvertible, CaseIterable, Codable {
case peach = "Peach"
case passionFruit = "Passion Fruit"
case mango = "Mango"
case greenApple = "Green Apple"
var description: String { rawValue }
}
enum TypeOfTea: String, CustomStringConvertible, CaseIterable, Codable {
case specialTeaPresso = "Special Tea Presso"
case teaLatte = "Tea Latte"
case mousseSeries = "Mousse Series"
case milkTea = "Milk Tea"
case fruitTea = "Fruit Tea"
var description: String { rawValue }
}
And this picker:
#State private var type: TypeOfTea = .specialTeaPresso
#State private var fruit: FruitTea = .passionFruit
Picker("Fruit Tea", selection: $fruit) {
ForEach(FruitTea.allCases, id: \.self) {
Text($0.rawValue).tag($0)
}
}
.onChange(of: fruit, perform: { _ in
type = .fruitTea
})
When I choose another kind of fruit tea that is already chosen, it works. But If I choose what is default value, my TypeOfTea won't change. I know why - because there was no change. But for my app clicking on Picker means choosing type. Can you please help? Thanks.
In case someone is interested in my all app:
My app on github

I don't know exactly if I understand your problem, but as you mentioned yourself there is no change. And your TypeOfTea remains a constant .fruitTea.
But you could try something like that to actually trigger changes.
struct ContentView: View {
#State private var type: TypeOfTea = .specialTeaPresso
#State private var fruit: FruitTea = .passionFruit
var body: some View {
VStack {
Picker("Fruit Tea", selection: $fruit) {
ForEach(FruitTea.allCases, id: \.self) {
Text($0.rawValue).tag($0)
}
}
Text(type.description)
}
.onChange(of: fruit, perform: { newFruit in
type = newFruit.typeOfTea
})
}
}
struct MyPreviewProvider_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
enum FruitTea: String, CustomStringConvertible, CaseIterable, Codable {
case peach = "Peach"
case passionFruit = "Passion Fruit"
case mango = "Mango"
case greenApple = "Green Apple"
var description: String { rawValue }
var typeOfTea: TypeOfTea {
switch self {
case .peach:
return .fruitTea
case .passionFruit:
return .teaLatte
case .mango:
return .mousseSeries
case .greenApple:
return .milkTea
}
}
}
enum TypeOfTea: String, CustomStringConvertible, CaseIterable, Codable {
case specialTeaPresso = "Special Tea Presso"
case teaLatte = "Tea Latte"
case mousseSeries = "Mousse Series"
case milkTea = "Milk Tea"
case fruitTea = "Fruit Tea"
var description: String { rawValue }
}

I have found a solution based on:
here
I have reduced those 5 pickers to 2. First to choose type of tea and second will show correct view using function that returns some View. Pickers had to be enclosed in AnyView().

Related

SwiftUI - Display nested Array in JSON response - try breaking up the expression into distinct sub-expressions

I have the following JSON response.
{
"results":[
{
"TrimName":"Truck",
"VIN":"GTJ3E18JF164623",
"Price": 47600,
"Odometer": 35511,
"OdometerType": "Miles",
"OptionCodeSpecs":{
"C_SPECS":{
"code":"C_SPECS",
"name":"Specs",
"options":[
{
"code":"SPECS_ACCELERATION",
"name":"5.2 sec",
"long_name":"5.2 seconds",
"description":"0-60 mph"
},
{
"code":"SPECS_TOP_SPEED",
"name":"140 mph",
"long_name":"140 miles per hour",
"description":"Top Speed"
},
{
"code":"SPECS_RANGE",
"name":"264 mi",
"long_name":"264 miles",
"description":"range (EPA)"
}
]
}
}
}
],
"total_matches_found":"53"
}
I built the Struct using app.quicktype.io.
// MARK: - InventoryModel
struct InventoryModel: Codable {
let results: [Result]
let totalMatchesFound: String
enum CodingKeys: String, CodingKey {
case results
case totalMatchesFound = "total_matches_found"
}
}
// MARK: - Result
struct Result: Codable {
let trimName, vin: String
let price, odometer: Int
let odometerType: String
let optionCodeSpecs: OptionCodeSpecs
enum CodingKeys: String, CodingKey {
case trimName = "TrimName"
case vin = "VIN"
case price = "Price"
case odometer = "Odometer"
case odometerType = "OdometerType"
case optionCodeSpecs = "OptionCodeSpecs"
}
}
// MARK: - OptionCodeSpecs
struct OptionCodeSpecs: Codable {
let cSpecs: CSpecs
enum CodingKeys: String, CodingKey {
case cSpecs = "C_SPECS"
}
}
// MARK: - CSpecs
struct CSpecs: Codable {
let code, name: String
let options: [Option]
}
// MARK: - Option
struct Option: Codable {
let code, name, longName, optionDescription: String
enum CodingKeys: String, CodingKey {
case code, name
case longName = "long_name"
case optionDescription = "description"
}
}
I am able to successfully retrieve the JSON response successfully.
#Published var getInventoryQuery: InventoryModel = InventoryModel(results: [], totalMatchesFound: "")
In my ContentView I am able to display the data at the Results object, but I am unable to get to the options array in the OptionCodeSpecs object without the following error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
Xcode Version 12.5.1 (12E507)
#ObservedObject var inv = GetInventory()
The following works great. Code tripped out for clarity.
ScrollView(.horizontal, showsIndicators: false){
HStack(spacing: 10){
ForEach(inv.getInventoryQuery.results, id: \.trimName) { inventory in
VStack(alignment: .leading) {
VStack(alignment: .center) {
Text(inventory.trimName)
However, when I try and get to the options array, I get the following error:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
HStack(alignment: .center){
ForEach(inv.getInventoryQuery.results[inventory].optionCodeSpecs.cSpecs.options) { options in
Text(options.longName)
.font(.headline)
.fontWeight(.light)
Image(systemName: "battery.100")
.renderingMode(.original)
}
}

Display a list from Observable Object SwiftUI

I'm trying to decode some JSON and print it to a list, currently getting this error message,
ForEach, Int, Text> count (626) != its initial count (0). ForEach(_:content:) should only be used for constant data. Instead conform data to Identifiable or use ForEach(_:id:content:) and provide an explicit id!
I can print a specific ticket by accessing result[0]
but I'm not able to return all results to the view.
Here is my ListView
struct WOListView: View {
#EnvironmentObject var ticketData:ControlCenter
var body: some View {
VStack {
Text(String(self.ticketData.jsonData?.result[0].ticketID?.ticketID ?? 0))
List{
ForEach(0 ..< (self.ticketData.jsonData?.result.count ?? 0)) {
Text(String(self.ticketData.jsonData?.result[$0].ticketID?.ticketID ?? 0))
}
}
}
}
}
struct WOListView_Previews: PreviewProvider {
static var previews: some View {
WOListView().environmentObject(ControlCenter())
}
}
WorkOrderResults.swift
struct WorkOrderResults: Codable{
var result:[Result]
enum CodingKeys:String, CodingKey{
case result = "Result"
}
struct Result:Codable{
var ticketID:TicketID?
var summary:Summary?
var status:Status?
var catagory:Catagory?
enum CodingKeys:String, CodingKey{
case ticketID = "1"
case summary = "22"
case status = "96"
case catagory = "164"
}
struct TicketID:Codable {
var ticketID:Int?
enum CodingKeys: String, CodingKey{
case ticketID = "Value"
}
}
Found the answer to my question here!
view-is-not-rerendered-in-nested-foreach-loop!
Change WOListView to look like this...
var body: some View {
VStack {
Text(String(self.ticketData.jsonData?.result[0].ticketID?.ticketID ?? 0))
ForEach(0 ..< (self.ticketData.jsonData?.result.count ?? 0), id: \.self) {
Text(String(self.ticketData.jsonData?.result[$0].ticketID?.ticketID ?? 0))
}
}
}

SwiftUI search field from a local JSON file

I'm trying to retrieve data from a local JSON file like this:
[{
"name": "John",
"lastname": "Doe",
"age": "30"
},
{
"name": "Jane",
"lastname": "Doe",
"age": "20"
}
,
{
"name": "Baby",
"lastname": "Doe",
"age": "3"
}]
The user, using a datapicker can select name and/or lastname
import SwiftUI
struct ContentView : View {
var names = ["John", "Jane", "Baby"]
var lastnames = ["Doe", "Boe"]
#State private var selectedNameItem = 0
#State private var selectedLastNameItem = 0
var body: some View {
VStack {
Picker(selection: $selectedNameItem, label: Text("Names:")) {
ForEach(0 ..< names.count) {
Text(self.names[$0]).tag($0)
}
}
Text("Your choice: ")
+ Text("\(names[selectedNameItem])")
}
VStack {
Picker(selection: $selectedLastNameItem, label: Text("LastName:")) {
ForEach(0 ..< lastnames.count) {
Text(self.lastnames[$0]).tag($0)
}
}
Text("Your choice: ")
+ Text("\(lastnames[selectedLastNameItem])")
}
}
}
Once selected the name/lastyname (as parameter) I want to show a text that said for example: "John Doe is 30 years old"
How I can read the data from JSON and return exactly the age of the user selected without list all the elements?
Thanks a lot,
Fabrizio
To start, I recommend making a struct to represent your JSON structure. Try the following:
struct Person: Codable, Hashable {
let name: String
let lastname: String
let age: String
}
typealias People = [Person]
I usually make a typealias to handle the array version of my data. Once you have defined your data structure to match your JSON, I usually extend my data to make loading from JSON easy.
extension Person {
static func load(fromJson json: URL) -> People {
guard let data = try? Data(contentsOf: json) else {
preconditionFailure("Unable to read data from URL")
}
let jsonDecoder = JSONDecoder()
var people = People()
do {
people = try jsonDecoder.decode(People.self, from: data)
} catch {
print(error)
}
return people
}
}
There are more generic ways to do this to support a wider swath of models, but for your example, this is quick and easy.
Now that you have an easy way to work with your model, an easy solution to what you have in mind could be done by extending the Array type like so:
extension Array where Element == Person {
func retrievePeople(byName name: String) -> People {
return self.filter { $0.name == name }
}
func retrievePeople(byLastName lastname: String) -> People {
return self.filter { $0.lastname == lastname }
}
func retrievePeople(byAge age: String) -> People {
return self.filter { $0.age == age }
}
}
This will allow you to query the entire range of objects by any of the elements and in turn, return the array of matches. If you're certain that there's only one return, you could use the following code to get the first element:
// Let's assume your array of people is stored in this variable
var myPeople: People
if let person = myPeople.retrievePeople(byName: "John").first {
// Do the things you want with this person object here
}
The nice thing about this style of loading/working with data is that it's easy to generate quickly and it will support returning 0 objects, 1 object, and multiple objects. This will also allow you to move the model over to use the features of SwiftUI/Combine (which is what it looks like you're hoping to do above).

I'm trying print data from my response but there is an error

I can print only 1 data, not any more. This is my error:
Thread 1: Fatal error: Index out of range
This is my JSON:
[
{
"Guides": [
{
"_id": "5cbc780edfdb6307006aec37",
"Text": "He is one of Soroush Friend",
"Tavernier": 2
},
{
"_id": "5cbc781bdfdb6307006aec38",
"Text": "He is one of Soroush Friend",
"Tavernier": 2
}
]
}
]
And this is my struct that works well:
struct GuideStruct: Codable {
let guides: [Guide]
enum CodingKeys: String, CodingKey {
case guides = "Guides"
}
}
struct Guide: Codable {
let id, text: String
let tavernier: Int
enum CodingKeys: String, CodingKey {
case id = "_id"
case text = "Text"
case tavernier = "Tavernier"
}
}
And this is my array and my class:
internal static var guides = [guidesarr]()
class guidesarr {
var _id : String
var Text : String
var Tavernier : Int
init(_id : String,Text : String,Tavernier : Int) {
self._id = _id
self.Text = Text
self.Tavernier = Tavernier
}
}
And my codes in viewcontroller:
class GameViewController: UIViewController,UITextFieldDelegate {
typealias guide1 = [GuideStruct]
var i1 = 0
override func viewDidLoad() {
super.viewDidLoad()
let headers : HTTPHeaders = ["Content-Type":"application/json","OAtcp":"0!QSJ5SDG8Q39PPM$DXP5HD1E10"]
Alamofire.request("http://192.168.1.100:3535/DarkDiamonds/Api/GetActiveGames",method :.post,headers: headers).responseJSON { (newresponse) in
do {
let decoder = JSONDecoder()
let responseguide = try decoder.decode(guide1.self, from: newresponse.data!)
for each1 in responseguide {
let newstruct = guidesarr(_id:each1.guides[self.i1].id , Text: each1.guides[self.i1].text, Tavernier: each1.guides[self.i1].tavernier)
self.i1 = self.i1 + 1
AppDelegate.guides.append(newstruct)
}
print(AppDelegate.guides[0])
print(AppDelegate.guides[1])
print(AppDelegate.Games.count)
print(AppDelegate.guides[0].Text)
print(AppDelegate.guides[1].Text)
}catch {
}
}
}
}
I can print:
print(AppDelegate.guides[0])
And print this:
print(AppDelegate.guides[0].Text)
But when I want to print this:
print(AppDelegate.guides[1])
print(AppDelegate.guides[1].Text)
There is error:
Thread 1: Fatal error: Index out of range
There are several issues in your code.
The guidesarr class is unnecessary. Just use your Guide struct.
Use proper naming conventions. Class, struct, and enum names should start with uppercase letters. Property, function, and case names should start with lowercase letters.
Don't force-unwrap data. Safely check it and do proper error checking.
Your main issue is that the two chunks of data you seem to actually want are the two Guide instances inside the one (not two) GuideStruct instances.
I would redo your code something like this:
class GameViewController: UIViewController,UITextFieldDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let headers : HTTPHeaders = ["Content-Type":"application/json","OAtcp":"0!QSJ5SDG8Q39PPM$DXP5HD1E10"]
Alamofire.request("http://192.168.1.100:3535/DarkDiamonds/Api/GetActiveGames", method: .post, headers: headers).responseJSON { (newresponse) in
if let data = newresponse.data {
do {
let decoder = JSONDecoder()
let guides = try decoder.decode([GuideStruct].self, from: data)
for guideStruct in guides {
AppDelegate.guides.append(contentsOf: guideStruct.guides)
}
print(AppDelegate.guides[0])
print(AppDelegate.guides[1])
print(AppDelegate.Games.count)
print(AppDelegate.guides[0].Text)
print(AppDelegate.guides[1].Text)
}catch {
// Bad JSON
}
} else {
// No data
}
}
}
}
And change:
internal static var guides = [guidesarr]()
to:
internal static var guides = [Guide]()
And delete the guidearr class.

Accepting enums of multiple types in functions

enum CompassPoint: String, Printable {
case North = "North"
case South = "South"
case East = "East"
case West = "West"
var description: String {
get {
return self.rawValue
}
}
}
enum Cars: String, Printable {
case Audi = "Audi"
case Lexus = "Lexus"
case Volkswagen = "Volkwagen"
case Jaguar = "Jaguar"
var description: String {
get {
return self.rawValue
}
}
}
func someFunction(direction: CompassPoint) {
println("\(direction)")
}
someFunction(.North) // prints out "North"
someFunction(.Audi) // error(CompassPoint.Type does not have member named 'Audi')
I'm trying to create a function in Swift which accepts both types of Enums, and maybe in the future others as well. Does anyone know how to create a function which accepts all Enums?
How about making someFunction's argument type Printable? Like this:
func someFunction(typeToPrint: Printable) {
println("\(typeToPrint)")
}
someFunction(CompassPoint.North)
someFunction(Cars.Audi)
You could use it like this by creating another enum which holds all the other enums
enum CompassPoint: String, Printable {
case North = "North"
case South = "South"
case East = "East"
case West = "West"
var description: String {
get {
return self.rawValue
}
}
}
enum Cars: String, Printable {
case Audi = "Audi"
case Lexus = "Lexus"
case Volkswagen = "Volkwagen"
case Jaguar = "Jaguar"
var description: String {
get {
return self.rawValue
}
}
}
enum combined
{
case comp(CompassPoint)
case car(Cars)
}
func someFunction(direction: combined) {
println("\(direction)")
}
someFunction(combined.comp(.North))// prints out "North"
someFunction(combined.car(.Audi))