Swiftui decodable, Printing multiple searches - swiftui

So in Swiftui, I'm trying to output multiple multiple of post.hints. It's a part of a large hierarchy. "results" itself is a list and I'm trying to make result = every single option rather than just one. What's the most efficient way to do that?
URLSession.shared.dataTask(with: url) { (data, _, _) in
if let posts = try? JSONDecoder().decode(Welcome.self, from: data!) {
results = [posts.hints[10].food]
}

I didn't realize I could just make a for loop and append each one.
if let posts = try? JSONDecoder().decode(Welcome.self, from: data!) {
// results = [posts.hints[10].food]
for i in posts.hints {
results.append(i.food)
}
}

Related

SwiftUI List: inserting item + changing section order = app crash

Please see the code below. Pressing the button once (or twice at most) is almost certain to crash the app. The app shows a list containing two sections, each of which have four items. When button is pressed, it inserts a new item into each section and also changes the section order.
I have just submitted FB9952691 to Apple. But I wonder if anyone on SO happens to know 1) Does UIKit has the same issue? I'm just curious (the last time I used UIkit was two years ago). 2) Is it possible to work around the issue in SwiftUI? Thanks.
import SwiftUI
let groupNames = (1...2).map { "\($0)" }
let groupNumber = groupNames.count
let itemValues = (1...4)
let itemNumber = itemValues.count
struct Item: Identifiable {
var value: Int
var id = UUID()
}
struct Group: Identifiable {
var name: String
var items: [Item]
var id = UUID()
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
}
struct Data {
var groups: [Group]
// initial data: 2 sections, each having 4 items.
init() {
groups = groupNames.map { name in
let items = itemValues.map{ Item(value: $0) }
return Group(name: name, items: items)
}
}
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
}
struct ContentView: View {
#State var data = Data()
var body: some View {
VStack {
List {
ForEach(data.groups) { group in
Section {
ForEach(group.items) { item in
Text("\(group.name): \(item.value)")
}
}
header: {
Text("Section \(group.name)")
}
}
}
Button("Press to crash the app!") {
withAnimation {
data.change()
}
}
.padding()
}
}
}
More information:
The error message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=8, oldGlobalRowCount=8)'
The issue isn't caused by animation. Removing withAnimation still has the same issue. I believe the issue is caused by the section order change (though it works fine occasionally).
Update: Thank #Yrb for pointing out an out-of-index bug in insertItem(). That function is a setup utility in the example code and is irrelevant to the issue with change(). So please ignore it.
The problem is here:
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
You are attempting to do too much to the array at once, so in the middle of reversing the order, the array counts are suddenly off, and the List (and it's underlying UITableView) can't handle it. So, you can either reverse the rows, or add an item to the rows, but not both at the same time.
As a bonus, this will be your next crash:
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
though it is not causing the above as I fixed this first. You have set a fixed Int for itemNumber which is the count of the items in the first place. Arrays are 0 indexed, which means the initial array indices will be (0...3). This line let index = (0...itemNumber).randomElement()! gives you an index that is in the range of (0...4), so you have a 20% chance of crashing your app each time this runs. In this sort of situation, always use an index of (0..<Array.count) and make sure the array is not empty.
I got Apple's reply regarding FB9952691. The issue has been fixed in iOS16 (I verified it).

In SwiftUI, what's the best way to dynamically update rows based on underlying data when underlying data has not changed?

I have an app that lists Events from Core Data. Each event has a date.
When I list the Events, I want show the date, unless the date was today or yesterday, and in that case I want to show Today or Yesterday instead of the date.
As of now, I have a function that handles generating the String to show in the row. However, I've noticed that if a day passes and I re-open the app, it shows outdated information. For example, if there is an event from the previous day that said Today when I had the app open the previous day, it will still say Today instead of Yesterday when I re-open the app. Obviously this function is not being called every time I open the app, but I am wondering what the best approach is for making this more dynamic.
These are the avenues I am considering, but not sure what would be best, so I wanted to post here to get recommendations and see if I'm overlooking anything important:
Somehow do something with .onAppear on the row to re-calculate it every time the app is opened (I'm not sure how expensive this date calculation stuff is for each event, but even if it's not expensive I'm not sure how I would tell the rows to re-run the function when the app comes to the foreground)
Switch to a computed property (I don't know if this would be any different than putting a function in there, like I have now. This could be bad to have it called every time if it's an expensive call, but assuming it's not how would I get this to refresh every time the app comes to the foreground?)
Come up with a solution to only re-calculate each row if the day has changed (this is probably what I'd try to do if I knew the row calculation was very expensive, but seems like it might be overkill here, and I'm also not sure how I would go about telling each row to re-run the function)
Here is my code (I left out my date formatter code, but it's pretty standard and shouldn't matter for this):
struct ContentView: View {
#FetchRequest(fetchRequest: Event.eventsNewestFirst)
private var events: FetchedResults<Event>
var body: some View {
NavigationView {
ForEach(events){ event in
EventRow(event: event)
}
}
}
}
struct EventRow: View {
#ObservedObject var event: Event
var body: some View {
Text(event.dateAndTimeString())
}
}
extension Event {
func dateAndTimeString() -> String {
guard let date = self.date else { return "Error" }
let timeString = DateAndNumberFormatters.simpleTimeDisplay.string(from: date)
let dateString: String
if let todayOrYesterday = date.asTodayOrYesterday() {
dateString = todayOrYesterday
} else {
dateString = DateAndNumberFormatters.simpleShortDateDisplay.string(from: date)
}
return "\(dateString) at \(timeString)"
}
}
extension Date {
func asTodayOrYesterday() -> String? {
let calendar = Calendar.current
let dayComponents = calendar.dateComponents([.year, .month, .day], from: self)
let todayDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
var yesterdayDateComponents = calendar.dateComponents([.year, .month, .day], from: Date())
yesterdayDateComponents.day = yesterdayDateComponents.day! - 1
let dayDate: Date! = calendar.date(from: dayComponents)
let todayDayDate: Date! = calendar.date(from: todayDateComponents)
let yesterdayDayDate: Date! = calendar.date(from: yesterdayDateComponents)
switch dayDate {
case todayDayDate:
return "Today"
case yesterdayDayDate:
return "Yesterday"
default:
return nil
}
}
}
The possible approach is to observe scene phase and force refresh observed core data object as needed, like
struct EventRow: View {
#ObservedObject var event: Event
#Environment(\.scenePhase) var scenePhase
var body: some View {
Text(event.dateAndTimeString())
.onChange(of: scenePhase) {
if $0 == .active {
event.objectWillChange.send()
}
}
}
}
The scenePhase approach in another answer did not work.
The solution I ended up using relies on a publisher of UIApplication.didBecomeActiveNotification instead:
struct EventRow: View {
#ObservedObject var event: Event
#State private var dateAndTime: String = "Error"
var body: some View {
NavigationLink(destination: EventDetailView(event: event)) {
Text(dateAndTime)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
dateAndTime = event.dateAndTimeString()
}
}
}
}

JSON data not copied to structure

This api is getting called correctly and is not falling into the "fetch failed" error line. The ContentView structure contains a standard menu with .onAppear at the bottom. Using the Xcode debugger I can see the data in decodedResponse but not in result. Is it really necessary to use #State results? Some of values in result / UserRates will be immediately pulled out and stored elsewhere. I would also like to use the retrieved date but currently my results definition keeps its non-defined. I'm assuming that to retrieve the data from the structure it is: UserRates.rates[9].key and UserRates.rates[9].value. Trying to read these with a print statement returns an error.
Galen Smith
struct UserRates: Codable {
let rates:[String: Double]
let base: String
let date: String
}
struct ContentView: View {
#State private var results = UserRates(rates: [:], base: "", date: "")
var body: some View {
... standard menu system in body
.onAppear(perform: loadCurrencies)
}
private func loadCurrencies() {
guard let url = URL(string: "https://api.exchangeratesapi.io/latest?base=USD") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let decodedResponse = try? JSONDecoder().decode(UserRates.self, from: data) {
// we have good data – go back to the main thread
DispatchQueue.main.async {
// update our UI
**self.results = decodedResponse**
}
return
}
}
// problem if we fail into this
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
I had planned to copy 32 String / Double data pairs from decodedResponse to the UserRates structure then find and copy 11 exchange rates using the dictionary key / value method to a rateArray. But since the copy always fails, I decided to use the dictionary key / value method on the decodedResponse structure and copying directly to rateArray skipping the UserRate structure entirely. I may in the future use more exchange rates so that is why I load all available rates from my source. The real purpose of rateArray is to store the exchange rate reciprocals.
Galen

Store dictionary in UserDefaults

This is a similar approach to Save dictionary to UserDefaults, however, it is intended for SwiftUI, not using a single line like set, so I want to store the value somewhere with a variable so I can call it easily. Also it's different because I'm asking for an initialization.
I have the following:
#Published var mealAndStatus: Dictionary
init() {
mealAndStatus = ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"]
if let storedDay = UserDefaults.standard.value(forKey: "mealAndStatus") {
mealAndStatus = storedDay as! Dictionary
}
}
1- How do I correctly store that dictionary in UserDefaults in SwiftUI?
2- That init, do I have to call it at the beginning of ContentView? Or can I leave it on the other swift file like that? Not sure how the init gets called.
I already made one with bool working:
#Published var startDay: Bool
init() {
startDay = true
if let storedDay = UserDefaults.standard.value(forKey: "startDay") {
startDay = storedDay as! Bool
}
}
but the dictionary doesn't seem to work. I need to initialize that dictionary and also store it in UserDefaults so I can access it later. Any help is appreciated.
This is the perfect solution I found for SwiftUI:
Store this somewhere, in my case I created a class just for UserDefaults:
#Published var mealAndStatus: [String: Date] =
UserDefaults.standard.dictionary(forKey: "mealAndStatus") as? [String: Date] ?? [:] {
didSet {
UserDefaults.standard.set(self.mealAndStatus, forKey: "mealAndStatus")
}
}
That above initializes the dictionary and also creates a variable to be easily called and use to update the value. This can be modified at lunch time and add new values, that way is initialized with whatever I want.
Furthermore, now on Apple Dev wwdc20 they announced a new way of handling UserDefaults with SwiftUI which may be even better than the above. The propery wrapper is called: #AppStorage.
Using JSONEncoder and JSONDecoder would help you convert to data any struct or dictionary that conforms to codable.
let arrayKey = "arrayKey"
func store(dictionary: [String: String], key: String) {
var data: Data?
let encoder = JSONEncoder()
do {
data = try encoder.encode(dictionary)
} catch {
print("failed to get data")
}
UserDefaults.standard.set(data, forKey: key)
}
func fetchDictionay(key: String) -> [String: String]? {
let decoder = JSONDecoder()
do {
if let storedData = UserDefaults.standard.data(forKey: key) {
let newArray = try decoder.decode([String: String].self, from: storedData)
print("new array: \(newArray)")
return newArray
}
} catch {
print("couldn't decode array: \(error)")
}
return nil
}
// You would put this where you want to save the dictionary
let mealAndStatus = ["Breakfast": "initial", "Snack": "notSet", "Lunch": "notSet", "Snack2": "notSet", "Dinner": "notSet"]
store(dictionary: mealAndStatus, key: arrayKey)
// You would put this where you want to access the dictionary
let savedDictionary = fetchDictionay(key: arrayKey)
On a side note, you probably shouldn't be using standard defaults for storing stuff like this. Storing it as a database, or saving it in a file especially with encryption on eith the database or the file might be a bit safer.

Optional displayed on label since Swift 3 upgrade

My code was working perfectly with Swift2 and now for some label the variable is displayed but with Optional(....) just before...
Good code on Swift 2
var sstitre1:String!
print(sstitre1)
There is a perform segue which populate a value to sstitre1.
var sstitre1:String?
sstitre1 = json[21].string
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "hugo"
{
if let destinationVC = segue.destination as? MonumentViewController{
destinationVC.sstitre1 = "\(sstitre1)"
}
}
}
With Swift3, i ve get :
Optional(.....)
I want to get rid of Optional.
So i have as recommend on several post from stackoverflow, made some code change.
var sstitre1:String!
If let espoir = sstitre1 {
print (espoir)
}
But unfortunately it still displays Optional....
Pretty weird ....
In the line
destinationVC.sstitre1 = "\(sstitre1)"
always a literal string "Optional(foo)" is assigned to destinationVC.sstitre1 assuming the variable contains "foo"
The solution is to remove the String Interpolation
destinationVC.sstitre1 = sstitre1
PS: You should really use more descriptive variable names