This is a follow up to a question asked at Decoding Exchange Rate JSON in SwiftUI. I need some help with displaying the rate entries. I understand that is a dictionary key / value, but I'm not sure how to pull each string / double pair out of the structure.
Here is a typical string format received from the API:
{
"rates": { "CAD": 1.5497, "HKD": 9.2404, "ISK":159.0 },
"base": "EUR",
"date": "2020-11-27"
}
struct RateResult: Codable {
let rates: [String: Double]
let base, date: String
}
struct ContentView: View {
#State private var results = RateResult(rates: [:], base: "", date: "")
var curr = "EUR"
var body: some View {
Text("Base Currency = \(results.base)")
Text("Date = \(results.date)")
List(results, id: \.results.rates) { item in
VStack(alignment: .leading) {
Text(results.rates[item.key])
Text(results.rates[item.value])
}
}
.onAppear(perform: loadData)
}
func loadData() {
}
}
To get an array of key/value pairs from dictionary, you can use .map:
let dict = ["one": 1, "two": 2, "three": 3]
let pairs = dict.map { ($0, $1) } // ex: [("one": 1"), ("three": 3), ("two": 2)]
So, you can use the same in a List:
List(results.rates.map { ($0, $1) }, id: \.0) { currency, rate in
VStack {
Text(currency)
Text("\(rate)")
}
}
Related
In the last few months, many developers have reported NavigationLinks to unexpectedly pop out and some workarounds have been published, including adding another empty link and adding .navigationViewStyle(StackNavigationViewStyle()) to the navigation view.
Here, I would like to demonstrate another situation under which a NavigationLink unexpectedly pops out:
When there are two levels of child views, i.e. parentView > childLevel1 > childLevel2, and childLevel2 modifies childLevel1, then, after going back from level 2 to level 1, level 1 pops out and parentView is shown.
I have filed a bug report but not heard from apple since. None of the known workarounds seem to work. Does someone have an idea what to make of this? Just wait for iOS 15.1?
Below is my code (iPhone app). In the parent view, there is a list of persons from which orders are taken. In childLevel1, all orders from a particular person are shown. Each order can be modified by clicking on it, which leads to childLevel2. In childLevel2, several options are available (here only one is shown for the sake of brevity), which is the reason why the user is supposed to leave childLevel2 via "< Back".
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var paId: Int
var n: Int // used only in the real code
}
class Data : ObservableObject {
init() {
pastries = [
Pastry(id: 0, name: "Prezel"),
Pastry(id: 1, name: "Donut"),
Pastry(id: 2, name: "bagel"),
Pastry(id: 3, name: "cheese cake"),
]
persons = [
Person(id: 0, name: "Alice", orders: [Order(paId: 1, n: 1)]),
Person(id: 1, name: "Bob", orders: [Order(paId: 2, n: 1), Order(paId: 3, n: 1)])
]
activePersonsIds = [0, 1]
}
#Published var activePersonsIds: [Int] = []
#Published var persons: [Person] = []
#Published var pastries: [Pastry]
#Published var latestOrder = Order(paId: 0, n: 1)
lazy var pastryName: (Int) -> String = { (paId: Int) -> String in
if self.pastries.first(where: { $0.id == paId }) == nil {
return "undefined pastryId " + String(paId)
}
return self.pastries.first(where: { $0.id == paId })!.name
}
var activePersons : [Person] {
return activePersonsIds.compactMap {id in persons.first(where: {$0.id == id})}
}
}
#main
struct Bretzel_ProApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
#StateObject var data = Data()
var body: some View {
TabView1(data: data)
// in the real code, there are more tabs
}
}
struct TabView1: View {
#StateObject var data: Data
var body: some View {
NavigationView {
List {
ForEach(data.activePersons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
VStack (alignment: .leading) {
Text(person.name)
}
}
)
}
}
.listStyle(PlainListStyle())
.navigationTitle("Orders")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
List() {
ForEach (0...p.orders.count-1, id: \.self) { loop in
Section(header:
HStack() {
Text("BESTELLUNG " + String(loop+1))
}
) {
EPSubview1(data: data, psId: psId, loop: loop)
}
}
}.navigationTitle(p.name)
.listStyle(InsetGroupedListStyle())
}
}
struct EPSubview1: View {
#ObservedObject var data: Data
var psId: Int
var loop: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
let o1: Order = p.orders[loop]
NavigationLink(
destination: SelectPastry(data: data)
.onAppear() {
data.latestOrder.paId = o1.paId
}
.onDisappear() {
data.persons[pindex].orders[loop].paId = data.latestOrder.paId
},
label: {
VStack(alignment: .leading) {
Text(String(o1.n) + " x " + data.pastryName(o1.paId))
}
}
)
}
}
struct SelectPastry: View {
#ObservedObject var data : Data
var body: some View {
VStack {
List {
ForEach(data.pastries, id: \.self) {pastry in
Button(action: {
data.latestOrder.paId = pastry.id
}) {
Text(pastry.name)
.foregroundColor(data.latestOrder.paId == pastry.id ? .primary : .secondary)
}
}
}.listStyle(PlainListStyle())
}
}
}
The problem is your ForEach. Despite that fact that Person conforms to Identifiable, you're using \.self to identify the data. Because of that, every time an aspect of the Person changes, so does the value of self.
Instead, just use this form, which uses the id vended by Identifiable:
ForEach(data.activePersons) { person in
Which is equivalent to:
ForEach(data.activePersons, id: \.id) { person in
I have a very weird issue with my double-tap gesture. I don't know this is a bug or my codes are wrong. Here, I want to use the Double Tap Gesture to open my DetailView by using NavigationLink. The problem is the page of my DetailView shows randomly which is supposed to match with the page of my SourceView. If I use a regular NavigationLink without using .onTapGesture, the seletected pages are matched. I couldn't find any solution online, but I found there are some bugs in NavigationLink. My codes examples are below. Please let me know if you have the same issue in using both onTapGesture and NavigationLink.
import SwiftUI
struct DataArray: Identifiable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
public struct ListDataArray {
static var dot = [
DataArray(number: 1,
cities: "Baltimore"
name1: "John",
name2: "Mike"),
DataArray(number: 2,
cities: "Frederick"),
DataArray(number: 3,
cities: "Catonsville"
name1: "Susan",
name2: "Oliver",
name3: "Jude",
name4: "Erik"),
]
}
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
#State var isDoubleTab: Bool = false
var body: some View {
NavigationView {
LazyVStack(spacing: 10) {
ForEach (datas, id: \.id) { data in
HomeView(data: data)
.onTapGesture(count: 2) {
self.isDoubleTapped.toggle()
}
NavigationLink(
destination: DetailView(data: data),
isActivate: $isDoubleTab) {
EmptyView()
}
Divider()
.padding()
}
}
.navigationBarHidden(true)
}
}
}
struct DetailView: View {
var data = DataArray
var body: some View {
VStact {
Text(data.cities)
}
}
}
struct HomeView: View {
var data: DataArray
var body: some View {
VStack {
HSTack {
Text(data.cities).padding()
Text(data.name1)
if let sur2 = data.name2 {
surName2 = sur
Text(surName2)
}
}
}
}
}
The above examples are closely matched my current code. For simplicity, I just shortened some duplicate names including padding() etc...
The problem is that you're using the init(_:destination:isActive:) version of NavigationLink inside
a ForEach. When you set isDoubleTab to true, all of the visible NavigationLinks will attempt to present. This will result in the unexpected behavior that you're seeing.
Instead, you'll need to use a different version of NavigationLink: init(_:destination:tag:selection:). But first, replace #State var isDoubleTab: Bool = false with a property that stores a DataArray — something like #State var selectedData: DataArray?.
#State var selectedData: DataArray? /// here!
...
.onTapGesture(count: 2) {
/// set `selectedData` to the current loop iteration's `data`
/// this will trigger the `NavigationLink`.
selectedData = data
}
Then, replace your old NavigationLink(_:destination:isActive:) with this alternate version — instead of presenting once a Bool is set to true, it will present when tag matches selectedData.
NavigationLink(
destination: DetailView(data: data),
tag: data, /// will be presented when `tag` == `selectedData`
selection: $selectedData
) {
EmptyView()
}
Note that this also requires data, a DataArray, to conform to Hashable. That's as simple as adding it inside the struct definition.
struct DataArray: Identifiable, Hashable {
Result:
Full code:
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
// #State var isDoubleTab: Bool = false // remove this
#State var selectedData: DataArray? /// replace with this
var body: some View {
NavigationView {
LazyVStack(spacing: 10) {
ForEach(datas, id: \.id) { data in
HomeView(data: data)
.onTapGesture(count: 2) {
selectedData = data
}
NavigationLink(
destination: DetailView(data: data),
tag: data, /// will be presented when `data` == `selectedData`
selection: $selectedData
) {
EmptyView()
}
Divider()
.padding()
}
}
.navigationBarHidden(true)
}
}
}
struct DetailView: View {
var data: DataArray
var body: some View {
VStack {
Text(data.cities)
}
}
}
struct HomeView: View {
var data: DataArray
var body: some View {
VStack {
HStack {
Text(data.cities).padding()
Text(data.name1 ?? "")
/// not sure what this is for, doesn't seem related to your problem though so I commented it out
// if let sur2 = data.name2 {
// surName2 = sur
// Text(surName2)
// }
}
}
}
}
public struct ListDataArray {
static var dot = [
DataArray(
number: 1,
cities: "Baltimore",
name1: "John",
name2: "Mike"
),
DataArray(
number: 2,
cities: "Frederick"
),
DataArray(
number: 3,
cities: "Catonsville",
name1: "Susan",
name2: "Oliver",
name3: "Jude",
name4: "Erik"
)
]
}
struct DataArray: Identifiable, Hashable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
Other notes:
Your original code has a couple syntax errors and doesn't compile.
You should rename DataArray to just Data, since it's a struct and not an array.
I am able to show the name, cost per launch, etc of the JSON. However I cannot access the value for payload_weights and flic_images. I think because they are inside an array of objects.
Here is part of the JSON file
[
{
"rocketid": 1,
"id": "falcon1",
"name": "Falcon 1",
"type": "rocket",
"active": false,
"stages": 2,
"boosters": 0,
"cost_per_launch": 6700000,
"success_rate_pct": 40,
"first_flight": "2006-03-24",
"country": "Republic of the Marshall Islands",
"company": "SpaceX",
"height": {
"meters": 22.25,
"feet": 73
},
"diameter": {
"meters": 1.68,
"feet": 5.5
},
"mass": {
"kg": 30146,
"lb": 66460
},
"payload_weights": [
{
"id": "leo",
"name": "Low Earth Orbit",
"kg": 450,
"lb": 992
}
]
This is my model:
struct Rocket: Codable, Identifiable {
var id: String
var name: String
var type: String
var active: Bool
var stages: Int
var boosters: Int
var cost_per_launch: Int
struct PayloadWeight: Codable {
var id: String
var name: String
var kg: Int
var lb: Int
}
var payload_weights: [PayloadWeight]
}
This is the view I want to show my data.
struct ContentView: View {
let rockets = Bundle.main.decode([Rocket].self, from: "Rocket.json")
var body: some View {
List(rockets) { rocket in
HStack {
Text(rocket.name)
}
}
}
}
The payload_weights property is an array. Which means you can either access the first item (if it exists) or display all of them.
You may try the following:
struct ContentView: View {
let rockets: [Rocket] = Bundle.main.decode([Rocket].self, from: "Rocket.json")
var body: some View {
List(rockets) { rocket in
VStack {
Text(rocket.name)
List(rocket.payload_weights) { payloadWeight in
self.payloadWeightView(payloadWeight: payloadWeight)
}
}
}
}
func payloadWeightView(payloadWeight: Rocket.PayloadWeight) -> some View {
Text(payloadWeight.name)
}
}
Note: you need to conform PayloadWeight to Identifiable as well:
struct PayloadWeight: Codable, Identifiable { ... }
I'm literally starting programming, so sorry for the rookie question.
I'm working in Xcode/SwiftUI and trying to make a counter where the inial value comes from a JSON file.
I manage to extract the value for strings... but after spending hours trying to find out how to set the initial counter to the value "hull", I'm finally asking help!
My JSON file is formatted as such:
{
"id": 1,
"name": "Chaser",
"type": "Level 1",
"hull": 5,
"shields": 0,
"imageName": "chaser"
},
and my struct is like this:
struct Enemy: Hashable, Codable, Identifiable {
var id: Int
var name: String
var type: String
var hull: Int
var shields: Int
fileprivate var imageName: String
}
In my page, my code is like this:
struct EnemyDetails: View {
#State var count : Int = 0
var enemy : Enemy
var body: some View {
VStack {
EnemyImage(image: Image("EnemyImage"))
.frame(height:300)
VStack {
Spacer()
Text(enemy.name).font(.title)
Text(enemy.type)
Spacer()
HStack {
Button(action: {self.count = self.count - 1}) {
Image("Decrease")
}.padding(20)
Text("\(count)").font(.system(size:100)).padding(20)
Button(action: {self.count = self.count + 1}) {
Image("Increase")
}.padding(20)
}
Spacer()
}
}
}
}
I would like the value "Count" to be the value "Hull" from the JSON file.
Can anyone help?
Many thanks!
Here it is
struct EnemyDetails: View {
var enemy : Enemy
#State private var count : Int // don't initialise here
init(enemy: Enemy) {
self.enemy = enemy
_count = State(initialValue: enemy.hull) // << initial state !!
}
// ... other your code
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{}
}
}