JSON data not copied to structure - swiftui

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

Related

in SwiftUI, I have 2 Entities (A & B) in my CoreData with a relationship (one to many) between them, how can I fetch all attributes of B in TextFields

Let's say I have 2 entities:
GameSession :which has Attributes "date", "place", "numberofplayer" + a relationship called "players" with "Player"
Player: which has Attributes "name","score_part1","score_part2","score_part3" + a relationship with "GameSession"
the relationship is "one to many": One session can have many players
Let's say now I have a list of GameSession and when I click on on one (with a NavigationLink)
It sends me to a new view where I can see:
All the names of the players of that session (in text) and also right next to the player name I would like to have 3 TextField in which I can enter (an update) "score_part1","score_part2","score_part3" for every players of that session
Basically I am able to display the name of all the players of a given session, But it seems impossible to have the "score_part1","score_part2","score_part3" in editable TextField...
I have an error saying "Cannot convert value of type 'String' to expected argument type 'Binding<String>'"
Basically in my first view I have something like that:
struct RamiListePartieUIView: View {#Environment(.managedObjectContext) var moc#FetchRequest(entity: GameSession.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \GameSession.date, ascending: false)]) var gamesessions: FetchedResults<GameSession>
var body: some View {
VStack {
List {
ForEach(gamesessions, id: \.date) { session in
NavigationLink (destination: DetailPartieSelecUIView(session: session)){
Text("\(session.wrappedPlace) - le \(session.wrappedDate, formatter: itemFormatter) ")
}
}
.onDelete(perform: deleteSessions)
.padding()
}
}
}
}
And in my second view I have something like that:
struct DetailPartieSelecUIView: View {
#State var session:GameSession
#Environment(\.managedObjectContext) var moc
var body: some View {
Section("Ma session du \(session.wrappedDate, formatter: itemFormatter)"){
ForEach(session.playersArray, id: \.self) { player in
HStack {
Text(player.wrappedName) // OK it works
TextField("score", text : player.wrappedScore_part1) // it generates an error
TextField("score", text : player.wrappedScore_part2) // it generates an error
TextField("score", text : player.wrappedScore_part3) // it generates an error
}
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
// formatter.dateStyle = .short
// formatter.timeStyle = .medium
formatter.dateFormat = "YYYY/MM/dd" //"YY/MM/dd"
return formatter
}()
also,
I have defined the "wrappedScore_part1","wrappedScore_part2","wrappedScore_part3" in the Player+CoreDataProperties.swift file
and "wrappedPlace", "wrappedData" as well as the "PlayersArray" in the GameSession+CoreDataProperties.swift file
it is done like that:
public var wrappedPlace: String {
place ?? "Unknown"
}
// Convert NSSet into an array of "Player" object
public var playersArray: [Player] {
let playersSet = players as? Set<Player> ?? []
return playersSet.sorted {
$0.wrappedName< $1.wrappedName
}
}
I am new at coding with swiftUI so I am probably doing something wrong... If anyone can help me it would be much appreciated.
Thanks a lot
I have tried a lot of things. Like changing the type of my attribute to Int32 instead os String. As I am suppose to enter numbers in those fields, I thought it would be best to have Integer. But it didn't change anything. and ultimately I had the same kind of error message
I tried also to add the $ symbol, like that:
TextField("score", text : player.$wrappedScore_part1)
But then I had other error message popping up at the row of my "ForEach", saying "Cannot convert value of type '[Player]' to expected argument type 'Binding'"
And also on the line just after the HStack, I had an error saying "Initializer 'init(_:)' requires that 'Binding' conform to 'StringProtocol'"
Thank you for your help!
Best regards,
JB
Your first problem of how to fetch the players in a session you need to supply a predicate to the #FetchRequest<Player>, e.g.
#FetchRequest
private var players: FetchedResults<Player>
init(session: Session) {
let predicate = NSPredicate(format: "session = %#", session)
let sortDescriptors = [SortDescriptor(\Player.timestamp)] // need something to sort by.
_players = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate)
}
That acts like a filter and will only return the players that have the session relation equalling that object. The reason you have to fetch like this is so any changes will be detected.
The second problem about the bindings can be solved like this:
struct PlayerView: View{
#ObservedObject var player: Player {
var body:some View {
if let score = Binding($player.score) {
TextField("Score", score)
}else{
Text("Player score missing")
}
}
}
This View takes the player object as an ObservedObject so body will be called when any of its properties change and allows you to get a binding to property. The Binding init takes an optional binding and returns a non-optional, allowing you to use it with a TextField.

How can I reliably update a String in SwiftUI based on a #Published Array from an ObservableObject?

I’m building a macOS app based on data from CloudKit. I’m running into an issue where one of the Strings in my UI is not being updated as I would expect it to.
Model
I have an Event struct, which has an ID and a timestamp:
struct Event: Identifiable, Hashable {
let id: UUID
let timestamp: Date
init(id: UUID = UUID(),
timestamp: Date) {
self.id = id
self.timestamp = timestamp
}
}
Periodically, my app fetches updated Event data from CloudKit. This is handled in an ObservedObject called DataStore.
For example, the fetch happens when the DataStore is initialized, and when a push notification comes from the server to indicate there is new information.
The function updateLocalEvents() in DataStore is called to actually update the local in-memory #Published Array, which calls fetchEvents() to actually get the current set of last 10 events data from CloudKit.
class DataStore: ObservableObject {
#Published var recentEvents: [Event] = []
init() {
updateLocalEvents()
}
func updateLocalEvents() {
print("updateLocalEvents()")
fetchEvents()
.receive(on: RunLoop.main)
.assign(to: &$recentEvents)
}
private func fetchEvents() -> AnyPublisher<[Event],Never> {
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Event",
predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "timestamp",
ascending: false)]
let operation = CKQueryOperation(query: query)
operation.resultsLimit = 10
operation.qualityOfService = .userInitiated
var events = [Event]()
operation.recordFetchedBlock = { record in
if let id = UUID(uuidString: record.recordID.recordName),
let timestamp = record.object(forKey: "timestamp") as? Date
{
events.append(Event(id: id,
timestamp: timestamp))
}
}
return Future<[Event],Never> { promise in
operation.completionBlock = {
promise(.success(events))
}
CKConstants.container
.privateCloudDatabase
.add(operation)
}
.eraseToAnyPublisher()
}
}
View
In my view, I show a String to indicate the time since the last event. For example, it may say 1 hour ago or 3 hours ago using a RelativeDateTimeFormatter.
This is stored in a timeAgo #State variable of type String?.
There is a Timer that attempts to update the timeAgo String? every minute, using an .onReceive modifier for the Timer, and another .onReceive modifier that uses the #Published Array of Events to update the timeAgo String?. Here is my view code:
struct EventsView: View {
#EnvironmentObject var store: DataStore
#State private var timer: Publishers.Autoconnect<Timer.TimerPublisher> = Timer
.publish(every: 60,
on: .main,
in: .common)
.autoconnect()
#State private var timeAgo: String?
var body: some View {
VStack {
if let mostRecentEvent = store.recentEvents.first {
Text(timeAgo ?? relativeTimeFormatter.localizedString(for: mostRecentEvent.timestamp, relativeTo: Date()))
.fontWeight(.bold)
.font(.system(.largeTitle,
design: .rounded))
.onReceive(timer) { _ in
timeAgo = relativeTimeFormatter
.localizedString(for: mostRecentEvent.timestamp, relativeTo: Date())
}
.onReceive(store.$recentEvents) { recentEvents in
print(".onReceive(store.$recentEvents)")
if let mostRecentEvent = recentEvents.first {
timeAgo = relativeTimeFormatter
.localizedString(for: mostRecentEvent.timestamp, relativeTo: Date())
}
}
} else {
Text("No Event Data")
}
}
.frame(minWidth: 250,
maxWidth: 250,
minHeight: 200,
maxHeight: 200)
}
}
private let relativeTimeFormatter: RelativeDateTimeFormatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .numeric
return formatter
}()
The problem is that, sometimes, there is a push notification indicating new data, and the updateLocalEvents() function is called, updating the recentEvents variable, which also triggers .onReceive(store.$recentEvents). I can see this is happening with my print statements. However, the timeAgo variable does not always get updated, and the view still shows the old string.
How should I change this to get my desired result of always keeping the timeAgo String? up-to-date based on the current value of #Published var recentEvents?
I’m also open to any other suggestions to simplify or improve any of this code I shared! Thanks!
It turns out I made a dumb mistake and posted this thinking it was a SwiftUI or Combine issue, when really there was a different problem.
From what I can tell now, SwiftUI and everything I had set up was actually working fine.
The real issue was in the data I was fetching. The array of Events based on the query results were not what I expected. I was receiving a push notification from CloudKit that indicated there were, and based on that I would kick off the query to CloudKit to fetch the new data. This data that was fetched was outdated, and did not include the new events (or still included deleted events).
I found that if I included a delay of just a couple seconds between the notification arriving and sending the query, the expected data would come back from CloudKit and everything would update accordingly.
I should have actually been using the CKFetchDatabaseChangesOperation and related features of the CloudKit API to fetch and process the actual changes that triggered the notification instead of kicking off a fresh query each time.

Widget update data properly

I've made a widget which fetches Codable data and it's working just fine in the simulator ONLY. The widget updates within 30 seconds or less after the data has changed. I've set a 5 minute update limit (I understand it's called far less frequently). It's working actually really great in the simulator without any kind of background data fetches and updates in less time than I set in getTimeline. Then I ran into an issue on a a real test device.
The data won't update anywhere between 2-10+ mins while testing a real device, in the snapshot it's updated and can see the new data changes but not in the widget on springboard. I don't understand why the simulator works just fine but not a real device. The Widget is definitely being updated when the data changes but only in the Simulator so am I suppose to fetch data in the background?
I've come across this Keeping a Widget Up To Date | Apple Developer Documentation. I'm still very new to Swift and SwiftUI so this is a little bit harder for me to grasp. I'm trying to understand the section Update After Background Network Requests Complete to update my Codeable data. My guess is the simulator is different from a real device and I need to fetch data in the background for the must up to date data?
The end goal is to have the widget update as frequently as possible with the most current data. I'm not sure I even need the background data fetch?
My data model for my widget as an example (which is working fine)
class DataModel {
var data: DataClass = DataClass(results: []))
func sessions(_ completion: #escaping (DataClass -> Void) {
guard let url = URL(string: "URL HERE") else { return }
var request = URLRequest(url: url)
request.addValue("application/json", forHTTPHeaderField: "Accept")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(DataClass.self, from: data) {
self.data = response
completion(self.data)
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
}
}
}
.resume()
}
}
My getTimeline calling the data model
func getTimeline(in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
let model = DataModel()
var entries: [SimpleEntry] = []
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
let entry = SimpleEntry(date: entryDate, model: model)
entries.append(entry)
model.sessions {_ in
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
I have this for my background network request
import Foundation
import WidgetKit
class BackgroundManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
var completionHandler: (() -> Void)? = nil
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.background(withIdentifier: "widget-bundleID")
config.sessionSendsLaunchEvents = true
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
}()
func update() {
let task = urlSession.downloadTask(with: URL(string: "SAME URL FROM DATA MODEL HERE")!)
task.resume()
}
func urlSession(_ session: URLSession ,downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
print (location)
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
self.completionHandler!()
WidgetCenter.shared.reloadTimelines(ofKind: "Widget")
print("Background update")
}
}
Then in my Widget I set .onBackgroundURLSessionEvents(). I never see any background updates or errors in the console. This seems very wrong, the Codable data will never be updated? How do I properly update my data in the background?
struct Some_Widget: Widget {
let kind: String = "Widget"
let backgroundData = BackgroundManager()
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SomeWidget_WidgetEntryView(entry: entry)
}
.configurationDisplayName("Widget")
.description("Example widget.")
.onBackgroundURLSessionEvents { (sessionIdentifier, completion) in
if sessionIdentifier == self.kind {
self.backgroundData.update()
self.backgroundData.completionHandler = completion
print("background update")
}
}
}
}

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()
}
}
}
}

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.