How to show a different price value and currency based on the country - swift

I have an app aiming to manage a collection (like pokemon cards). It has JSON data with different variables, in which, the price. To display the list of all items I do the following:
struct itemRow: View {
let items: [Item] = Bundle.main.decode("item.json")
var body: some View {
List {
ForEach(items) { item in
HStack {
Text(item.name)
Text("\(String(format: "%.2f", item.price))€")
}
}
}
}
}
Which creates a straightforward list with names and prices stacked one on top of the other. My issue is that the original data are in €, so when the user uses the app whenever he is in the world, he will see Euro prices. I wanted to do something like an 'if, else' statement or whatever works, to make the user see the correct price even if he has USD. I was able only to show different currencies in the text, like '90$' instead of '90€', but the right conversion is like '97.69$'. I know that rates change every day, but due to the simple app, even some static constants are good enough for my purpose. I would like to support GBP and USD, not every currency

I solved in this way, basing the if-else statement on the Locale
struct itemRow: View {
let items: [Item] = Bundle.main.decode("item.json")
let locale = Locale.current
var body: some View {
List {
ForEach(items) { item in
HStack {
Text(item.name)
if (locale.identifier == "en_US") {
Text("\(String(format: "%.2f", minifig.price * 1.10))$")
.foregroundColor(.gray)
.font(.system(size: 12))
} else if (locale.identifier == "en_GB") {
Text("\(String(format: "%.2f", minifig.price * 0.88))£")
.foregroundColor(.gray)
.font(.system(size: 12))
} else {
Text("\(String(format: "%.2f", minifig.price))€")
.foregroundColor(.gray)
.font(.system(size: 12))
}
}
}
}
}
}

Related

Convert multiple detail tab views into single display

So I have an implementation on a project that someone else has made and I would like to switch the functionality of it, but I am unsure on how to do it, so I wanted to see if someone might be able to help me.
So I have the following code:
TabView(selection: $mainViewModel.selectedItemId) {
ForEach(mainViewModel.selectedCategory == "" ? mainViewModel.places: mainViewModel.places.filter({$0.category == mainViewModel.selectedCategory})) { item in
NavigationLink(
destination: PlaceDetailView(place: item),
label: {
PlaceTabViewDetail(item: item)
.background(Color("ColorWhite")).cornerRadius(20)
.frame(width: getRect().width - 40)
.padding(.horizontal, 10)
.padding(.bottom, 50)
.tag(item.id)
}
) //: END NAVIGATIONLINK
} //: END FOREACH
} //: END TABVIEW
Which outputs this:
How can I extract the "item" and use just a single display, so instead of being able to search through numerous locations tabs, I want just one single display like this:
Basically, I want to utilize "item" since it has all the data, but instead of a foreach, just have a single output.
If you want to show a single item from an array which is currently selected then you can try something like this.
Add a computed property in your MainViewModel.
var selectedItem: Place? {
places.first({ $0.id == selectedItemId })
}
Now use this property in your view like this.
if let item = mainViewModel.selectedItem {
NavigationLink(
destination: PlaceDetailView(place: item),
label: {
PlaceTabViewDetail(item: item)
.background(Color("ColorWhite")).cornerRadius(20)
.frame(width: getRect().width - 40)
.padding(.horizontal, 10)
.padding(.bottom, 50)
.tag(item.id)
}
)
}

How to Order An Array Of Data and Display It In That Order (Data On Firebase)

I have a project where I want to display badges (an image with a title) in a horizontal scroll view. The data for the badges is stored in a Firestore database. My code can fetch the data and show the badges in a horizontal scroll view. At the moment the badges are in a random order and I want to be able to sort them so they are in the correct order. For example:
Currently I have 4 badges, These include:
10k steps badge, 20k steps badge, 30k steps badge, 40k steps badge
This is the order I want them to be displayed in the horizontal scrollview however they are in a random order.
How can I order this data correctly? I am currently using a ForEach to Loop through the data.
My Model for the badges is as follows:
struct FitnessBadge: Identifiable {
var id: String
var fitness_badge_name: String
var fitness_badge_details: String
var fitness_badge_image: String
}
The View for the Badges is as follows:
struct FitnessBadgeView: View {
var fitnessBadge: FitnessBadge
var body: some View {
VStack(alignment: .center){
// Downloading Image From Web...
WebImage(url: URL(string: fitnessBadge.fitness_badge_image))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 150, height: 150)
.cornerRadius(15)
HStack(spacing: 8){
Text(fitnessBadge.fitness_badge_name)
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.black)
}
HStack(alignment: .center){
Text(fitnessBadge.fitness_badge_details)
.font(.subheadline)
.foregroundColor(.gray)
.lineLimit(2)
Spacer(minLength: 0)
}
}
}
}
How the badges are being displayed in a scroll view is as follows:
ScrollView(.horizontal, showsIndicators: false, content: {
VStack(alignment: .leading) {
Text("Fitness Badges")
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.black)
HStack(spacing: 10){
ForEach(AchievementPageModel.filteredFitnessBadges){fitnessBadge in
// Badge View...
ZStack(alignment: Alignment(horizontal: .center, vertical: .top), content: {
FitnessBadgeView(fitnessBadge: fitnessBadge)
})
}
}
}
})
I want it to go: 10k steps, 20k steps, 30k steps etc.
The AchievementPageModel.filteredFitnessBadges array is as follows:
class AchievementPageViewModel: NSObject, ObservableObject {
// Fitness Badges Data...
#Published var fitnessBadges: [FitnessBadge] = []
#Published var filteredFitnessBadges: [FitnessBadge] = []
}
And the fetching data for the badges is as follows:
func fetchFitnessBadgeData(){
let db = Firestore.firestore()
db.collection("FitnessBadges").getDocuments { (snap, err) in
guard let fitnessBadgeData = snap else{return}
self.fitnessBadges = fitnessBadgeData.documents.compactMap({ (doc) -> FitnessBadge? in
let id = doc.documentID
let name = doc.get("fitness_badge_name") as! String
let image = doc.get("fitness_badge_image") as! String
let details = doc.get("fitness_badge_details") as! String
return FitnessBadge(id: id, fitness_badge_name: name, fitness_badge_details: details, fitness_badge_image: image)
})
self.filteredFitnessBadges = self.fitnessBadges
}
}
There is a terrific answer by #HunterLion but it could become impractical and not scaleable because it assumes you're loading ALL of the documents from FitnessBadges into memory (an array) to allow for sorting.
What if there are 1 million documents? That could be a LOT of data and overwhelm the device.
Another option is to let Firestore do the heavy lifting; instead of sorting on your app, let Firestore do it!
With larger amounts of data, you will be way better off allowing Firebase to sort on the server and present that sorted data to your app because then you can read smaller 'chunks' of data through pagination.
A better solution would be to use order(by: like this
db.collection("FitnessBadges").order(by: "fitness_badge_name").getDocuments
That sorts millions of documents in a flash on the server and then you can get just the section you want - records 1-10 for example, and then 11-20 and they will be sorted.
For reference: Firestore Pagination
In the view where badges are being displayed, replace:
ForEach(AchievementPageModel.filteredFitnessBadges)
with:
ForEach(AchievementPageModel.filteredFitnessBadges.sorted { $0.fitness_badge_name < $1.fitness_badge_name })
This will sort by the field fitness_badge_name. I have assumed that's the field that contains "10k", "20k"... if the key is another field, just replace the variable in the .sorted() function.

Update Text Field only when Variable updates from the cloud in IOS app

I am using Firebase to append a String of data for an Array. I would then like to display this data on the screen. The view on the screen is a List and each Post is looking at a different part of the Array (example; postName[0] vs. postName[1].
My problem is that as soon as the screen loads, the data has not completely loaded from the cloud yet and therefore the Array is empty. This causes the app to crash saying that "Index out of range" since there is nothing in an array that a Text box is trying to read from.
The Array receives data from Firebase and if the data arrives fast enough no issue occurs, however, sometimes the data does not come fast enough and crashes saying index not in range.
Is there anything I can set up to not load the Text field until the data has finished loading?
Code provided:
List(fetchPostModel.postsNearby, id: \.self) { post in
ZStack {
if !fetchPostModel.postName.isEmpty { Text(fetchPostModel.postName[Int(post) ?? 0])
.font(.title)
.bold()
.padding()
} else { Text("Loading...").font(.title).bold().padding() }
}
.onAppear {
fetchFromCloud(postNumber: fetchFromCloud.postNumber[Int(post) ?? 0])
}
}
To prevent the "Index out of range" you can unwrap the property first to avoid the Int(post) ?? 0
if !fetchPostModel.postName.isEmpty {
if let postIndex = post {
Text(fetchPostModel.postName[Int(postIndex)])
.font(.title)
.bold()
.padding()
}
} else { Text("Loading...").font(.title).bold().padding() }
you can create one extension for that
extension Collection where Indices.Iterator.Element == Index {
public subscript(safe index: Index) -> Iterator.Element? {
return (startIndex <= index && index < endIndex) ? self[index] : nil
}
}
Now it can be applied universally across different collections
Example:
[1, 2, 3][safe: 4] // Array - prints 'nil'
(0..<3)[safe: 4] // Range - prints 'nil'
In your problem, you can use like that
List(fetchPostModel.postsNearby, id: \.self) { post in
ZStack {
if let currentPostName = fetchPostModel.postName[safe: post] {
Text(currentPostName)
.font(.title)
.bold()
.padding()
} else {
Text("Loading...").font(.title).bold().padding()
}
}
.onAppear {
fetchFromCloud(postNumber: fetchFromCloud.postNumber[Int(post) ?? 0])
}
}

Swift HStack View not conforming to protocol 'View'

I have built this code
struct StarDifficultyView: View {
var numberOfStarsToShow: Int
var numberOfTotalStarsToShow: Int = 5
var body: some View {
HStack{
var numberLeftToShow = numberOfStarsToShow
ForEach(1..<numberOfTotalStarsToShow+1){_ in
if(numberLeftToShow > 0){
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
numberLeftToShow -= 1
}else{
Image(systemName: "star.fille")
.foregroundColor(Color.yellow)
}
}
}
}
}
It gives me an error on the line if(numberLeftToShow > 0){ saying "Type '()' cannot conform to 'View'"
Can anyone tell me what I'm doing wrong
Explaining the issue:
You should not add expressions inside the view builder. So numberLeftToShow -= 1 will throw and error because it returns a void ('aka' type()) and this does not conform to View! that is the exact reason for the compiler!
Note 1
Don't use SwiftUI like the UIKit! SwiftUI views may execute over time on any state change and should not be used for calculating anything in this way
Note 2
You can convert 1..<numberOfTotalStarsToShow+1 to a closed range like 1...numberOfTotalStarsToShow (Although you don't need it at all for this question)
Note 3
Try not to use branch and convert your if/else code to something like:
Image(systemName: numberLeftToShow > 0 ? "star.fill" : "star.fille")
.foregroundColor(Color.yellow)
Note 4:
The lower bound of a range can not be less than the upper range, but you can iterate over a reversed range like:
(1...numberOfTotalStarsToShow).reversed()
Note 5:
try using a single source of truth like the forEach parameter itself!
Note 6:
Swift can infer the type and you don't need to pass it again:
so change Color.yellow to .yellow
Final Result:
Here is the code reviewed answer (based on the answer you have provided yourself):
var body: some View {
HStack {
ForEach(1...numberOfTotalStarsToShow, id:\.self) { i in
Image(systemName: "star.fill")
.foregroundColor(i > numberOfStarsToShow ? .gray : .yellow)
}
}
}
Don't throw away the closure parameter for the ForEach!
var body: some View {
HStack{
ForEach(0..<numberOfTotalStarsToShow){ i in // don't ignore the "i" here by writing "_"
// "i" will be different in each "iteration"
// use that to figure out which image to show
if(i < numberOfStarsToShow){
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
} else {
Image(systemName: "star")
.foregroundColor(Color.yellow)
}
}
}
}
Never mind, I just did this
struct StarDifficultyView: View {
var numberOfStarsToShow: Int
var numberOfTotalStarsToShow: Int = 5
var body: some View {
HStack{
ForEach(1..<numberOfStarsToShow+1){_ in
Image(systemName: "star.fill")
.foregroundColor(Color.yellow)
}
ForEach(1..<numberOfTotalStarsToShow-numberOfStarsToShow+1){_ in
Image(systemName: "star.fill")
.foregroundColor(Color.gray)
.opacity(0.7)
}
}
}
}
Basically, it just loops through the number of yellow stars to show and then works out how many grey ones to show and does another ForEach to display the leftover ones needed

SwiftUI TextField Lag

When I have multiple text fields rendered in SwiftUI in a given view, I am getting noticeable lag that is directly proportional to the number of text fields. If I change these to simple text views, the lag goes down considerably.
I have looked at SO and found a few questions about lag with TextField but generally it seems like there's a preponderance that the lag is caused by the data source because when using a constant value, the lag is not observed.
I have created a demo project to illustrate the issue. I have an array of 20 contact names and for each name create a contact card with three email addresses. If I toggle the view between rendering the email addresses as Text vs TextField Views (with a constant value), the time taken from button tap to the last view's .onAppear is 80-100 ms (Text) and 300-320 ms (TextField).
Both views take a noticeable time to render, but clearly the TextFields take a significantly longer time to render on this contrived, trivial app. In our app, we are rendering significantly more information and not using constant values for the TextFields so this lag produces more pronounced effects (sometimes a few seconds). Is there some way around this issue for SwiftUI TextFields? Below is the code for the demo project. I know there are better ways to write the code, just threw it together quickly to demonstrate the speed issues.
Also, interestingly, if I put the ForEach into a List (or just try to use a list directly from the array data), no ContactCard views are rendered at all.
Any help is greatly appreciated!
import SwiftUI
var formatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
return formatter
}
struct ContentView: View {
let contacts: Array<(first: String, last: String)> = [
("John", "Stone"),
("Ponnappa", "Priya"),
("Mia", "Wong"),
("Peter", "Stanbridge"),
("Natalie", "Lee-Walsh"),
("Ang", "Li"),
("Nguta", "Ithya"),
("Tamzyn", "French"),
("Salome", "Simoes"),
("Trevor", "Virtue"),
("Tarryn", "Campbell-Gillies"),
("Eugenia", "Anders"),
("Andrew", "Kazantzis"),
("Verona", "Blair"),
("Jane", "Meldrum"),
(" Maureen", "M. Smith"),
("Desiree", "Burch"),
("Daly", "Harry"),
("Hayman", "Andrews"),
("Ruveni", "Ellawala")
]
#State var isTextField = false
var body: some View {
ScrollView {
VStack {
HStack {
Button("Text") {
print("text tapped: \(formatter.string(from: Date()))")
isTextField = false
}
Button("TextField") {
print("text tapped: \(formatter.string(from: Date()))")
isTextField = true
}
}
ForEach(contacts, id: \.self.last) { contact in
ContactCard(name: contact, isTextField: $isTextField)
}
}
}
}
}
struct ContactCard: View {
var name: (first: String, last: String)
#Binding var isTextField: Bool
var emailAddresses: Array<String> {
[
"\(name.first).\(name.last)#home.com",
"\(name.first).\(name.last)#work.com",
"\(name.first).\(name.last)#work.org",
]
}
var body: some View {
VStack {
Text("\(name.first) \(name.last)")
.font(.headline)
ForEach(emailAddresses, id: \.self) { email in
HStack {
Text("Email")
.frame(width: 100)
if isTextField {
TextField("", text: .constant(email))
.onAppear(){
print("view appeared: \(formatter.string(from: Date()))")
}
} else {
Text(email)
.onAppear(){
print("view appeared: \(formatter.string(from: Date()))")
}
}
Spacer()
}
.font(.body)
}
}
.padding()
}
}
Use LazyVStack in your scroll view instead of VStack. It worked for me, tested using 200 contact names.