SwiftUI global var and func at launch - ios

I have this func that parses a json. Then I use the data for that to populate a bunch of UI slot. The problem is it's a big json (if 10.1MB is big) and it takes 5-8 seconds to load. At app launch that's no big deal, but right now it' reprising that data every time.
Right now I just have this mode were each struct view starts with:
var results = [ScryfallCard]()
.onApear {
results = func()
}
func() -> [ScryfallCard]
I can't find for the life of me find out how to make a global variable, assign it the var globleResults = func() so my app loads all that data upfront and doesn't take 5 second to load each view.

In order for your view to update the results, you need to make turn your results results into #State var results: [ScryfallCard] = []
Then with your function that deals with the JSON, no matter how long it takes to process, it should update the view once the results = func() is finished.
Avoid using global variables and aim for creating different components to encapsulate the variables.
You would have something like this
class NetworkRequester: ObservableObject {
#Published var results: [String] = []
var cancellable: AnyCancellable?
func getResults() {
cancellable = URLSession.shared.dataTaskPublisher(for: URL(fileURLWithPath: "path-to-JSON")!)
.map({ $0.data })
.decode(type: [ScryfallCard].self, decoder: JSONDecoder())
.sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print(error)
case .finished:
break
}
}) { newResults in
self.results = newResults
}
}
}
The link suggested in the comment is a useful resource to learn more about how to deal with rendering data.

Ok... figured it out. Kinda janky but it works.
I basically had to start at the top and put #EnvironmentObject var globalCards : ScryfallData inside my ContentView. The at each view add #ObservedObject var globalCards = ScryfallData() and pass globalCards: globalCards on to the next View. Then on the last view just use List(globalCards.parsedResults, id: \.id) { item in in my list.
My print("Parsing...") print("Parsing complete") only spits out that at the first build and then next parses again. (somehow even after I swipe-up-quit the app then launch it again it doesn't spit out the print()statements)

Related

Awaiting Task Completion in SwiftUI View

I am working with a view that displays a list of locations. When a user taps on a location, a didSet block containing a Task is triggered in a separate class wrapped with the #ObservedObject property:
struct LocationSearch: View {
#StateObject var locationService = LocationService()
#ObservedObject var networking: Networking
var savedLocations = SavedLocations()
var body: some View {
ForEach(locationService.searchResults, id: \.self) { location in
Button(location.title) {
getCoordinate(addressString: location.title) { coordinates, error in
if error == nil {
networking.lastLocation = CLLocation(latitude: coordinates.latitude, longitude: coordinates.longitude)
// wait for networking.locationString to update
// this smells wrong
// how to better await Task completion in didSet?
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
savedLocations.all.append(networking.locationString!.locality!)
UserDefaults.standard.set(savedLocations.all, forKey: "savedLocations")
dismiss()
}
}
}
}
}
}
}
The Task that gets triggered after networking.lastLocation is set is as follows:
class Networking: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published public var lastLocation: CLLocation? {
didSet {
Task {
getLocationString() { placemark in
self.locationString = placemark
}
getMainWeather(self.lastLocation?.coordinate.latitude ?? 0, self.lastLocation?.coordinate.longitude ?? 0)
getAQI(self.lastLocation?.coordinate.latitude ?? 0, self.lastLocation?.coordinate.longitude ?? 0)
locationManager.stopUpdatingLocation()
}
}
}
What is a better way to ensure that the task has had time to complete and that the new string will be saved to UserDefaults versus freezing my application's UI for one second?
In case it isn't clear, if I don't wait for one second, instead of the new value of locationString being saved to UserDefaults, the former value is saved instead because the logic in the didSet block hasn't had time to complete.
The first thing I think is odd about your code is that you have a class called "Networking" which is storing a location. It seems that you are conflating storing location information with making network requests which is strange to me.
That aside, the way you synchronize work using Async/Await is by using a single task that can put the work in order.
I think you want to coordinate getting some information from the network and then storing that information in user defaults. In general the task structure you are looking for is something like:
Task {
let netData = await makeNetworkRequest(/* parameters /*)
saveNetworkDataInUserDefaults()
}
So instead of waiting for a second, or something like that, you create a task that waits just long enough to get info back from the network then stores the data away.
To do that, you're going to need an asynchronous context in which to coordinate that work, and you don't have one in didSet. You'll likely want to pull that functionality out into a function that can be made async. Without a lot more detail about what your code means, however, it's difficult to give more specific advice.

Core Data / SwiftUI - How to get from a backgroundContext back to the mainContext

I want to use concurrency to perform some expensive core data fetch request in background.
To do so I created a backgroundContext with .newBackgroundContext() and automaticallyMergesChangesFromParent = true
I'm not able to work in the main context with the data I fetched in the backgroundContext. Otherwise the app crashes.
Although the two contexts get synced it seems like I'm bound to the backgroundContext as long as I work with that fetched data, is that right?
And If so, is there any reason why not to perform everything across the whole app in the backgroundContext? That would prevent from accidentally switch to the mainContext.
Or is there any convenient way to get to the mainContext after fetching and processing the data in the backgroundContext?
Here is a small example:
struct ContentView: View {
#Environment(\.managedObjectContext) private var viewContext
let bgContext = PersistenceController.shared.container.newBackgroundContext()
#State var items: [Item] = []
var body: some View {
List {
ForEach(items) { item in
Text(date, formatter: itemFormatter)
}
}
.task {
items = await fetchItems(context: bgContext)
}
}
func fetchItems(context: NSManagedObjectContext) async -> [Item] {
do{
let request: NSFetchRequest<Item> = Item.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "timestamp", ascending: true)]
return try bgContext.fetch(request)
} catch {
return[]
}
}
}
Right now, every further action needs to get done in the backgroundContext, including save and delete functions.
Based on mlhals comment, I found a quite simple Solution.
I perform all expensive fetch requests and calculations in the background.
For the visible elements I use a #FetchRequest property wrapper.
Since it is updating automatically it updates changes made in the background context instantly. Further user actions like deleting etc. I can then proceed in the "main" context.

How to force an initial value when creating a pipe with CurrentValueSubject in Combine in Swift 5?

I am trying to fetch initial value of EventDetailsModel and subscribe to all future updates.
When I call eventDetails(..), all the publishers already have some current value in them (i.e. chatModels and userModels have 10+ items); the problem is that because there are no new updates, the resulting pipe never returns EventDetailModels since .map(...) never gets called.
How can I make the combine pipe do at least one pass through the existing values when I am constructing it so my sink has some initial value?
var chatModels: Publishers.Share<CurrentValueSubject<[ChatModel], Never>> = CurrentValueSubject([]).share()
var userModels: CurrentValueSubject<[String: UserModel], Never> = CurrentValueSubject([:])
func eventDetails(forChatId chatId: String) -> AnyPublisher<EventDetailsModel?, Never> {
return chatModels
.combineLatest(userModels)
.map({ (chatList, userModels) -> EventDetailsModel? in
// Never gets called, even if chatModels and userModels has some existing data 😢
if let chatModel = (chatList.first { $0.id == chatId}) {
return EventDetailsModel(chatModel, userModels)
}
return nil
})
.eraseToAnyPublisher()
}
With combineLatest, you won't get any events until there is a latest for both publishers. That is what combineLatest means. The problem here is not chatModels, which does have a latest. The problem is userModels. Until it publishes for the first time, you won't get any events in this pipeline.
EDIT Okay, so now you've updated your code to reveal that both your publishers are CurrentValueSubjects. Well, in that case, you do get an initial event, as this toy example proves:
var storage = Set<AnyCancellable>()
let sub1 = CurrentValueSubject<Int,Never>(1)
let sub2 = CurrentValueSubject<String,Never>("howdy")
override func viewDidLoad() {
super.viewDidLoad()
sub1.combineLatest(sub2)
}
So if that isn't happening for you, the problem lies elsewhere. For example, maybe you forgot to store your pipeline, so you can't get any events at all. (But who knows? You have concealed the relevant code.)

Swift Combine operator with same functionality like `withLatestFrom` in the RxSwift Framework

I'm working on an iOS application adopting the MVVM pattern, using SwiftUI for designing the Views and Swift Combine in order to glue together my Views with their respective ViewModels.
In one of my ViewModels I've created a Publisher (type Void) for a button press and another one for the content of a TextField (type String).
I want to be able to combine both Publishers within my ViewModel in a way that the combined Publisher only emits events when the button Publisher emits an event while taking the latest event from the String publisher, so I can do some kind of evaluation on the TextField data, every time the user pressed the button. So my VM looks like this:
import Combine
import Foundation
public class MyViewModel: ObservableObject {
#Published var textFieldContent: String? = nil
#Published var buttonPressed: ()
init() {
// Combine `$textFieldContent` and `$buttonPressed` for evaulation of textFieldContent upon every button press...
}
}
Both publishers are being pupulated with data by SwiftUI, so i will omit that part and let's just assume both publishers receive some data over time.
Coming from the RxSwift Framework, my goto solution would have been the withLatestFrom operator to combine both observables.
Diving into the Apple Documentation of Publisher in the section "Combining Elements from Multiple Publishers" however, I cannot find something similar, so I expect this kind of operator to be missing currently.
So my question: Is it possible to use the existing operator-API of the Combine Framework to get the same behavior in the end like withLatestFrom?
It sounds great to have a built-in operator for this, but you can construct the same behavior out of the operators you've got, and if this is something you do often, it's easy to make a custom operator out of existing operators.
The idea in this situation would be to use combineLatest along with an operator such as removeDuplicates that prevents a value from passing down the pipeline unless the button has emitted a new value. For example (this is just a test in the playground):
var storage = Set<AnyCancellable>()
var button = PassthroughSubject<Void, Never>()
func pressTheButton() { button.send() }
var text = PassthroughSubject<String, Never>()
var textValue = ""
let letters = (97...122).map({String(UnicodeScalar($0))})
func typeSomeText() { textValue += letters.randomElement()!; text.send(textValue)}
button.map {_ in Date()}.combineLatest(text)
.removeDuplicates {
$0.0 == $1.0
}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
typeSomeText()
typeSomeText()
typeSomeText()
pressTheButton()
typeSomeText()
typeSomeText()
pressTheButton()
The output is two random strings such as "zed" and "zedaf". The point is that text is being sent down the pipeline every time we call typeSomeText, but we don't receive the text at the end of the pipeline unless we call pressTheButton.
That seems to be the sort of thing you're after.
You'll notice that I'm completely ignoring what the value sent by the button is. (In my example it's just a void anyway.) If that value is important, then change the initial map to include that value as part of a tuple, and strip out the Date part of the tuple afterward:
button.map {value in (value:value, date:Date())}.combineLatest(text)
.removeDuplicates {
$0.0.date == $1.0.date
}
.map {($0.value, $1)}
.map {$0.1}
.sink { print($0)}.store(in:&storage)
The point here is that what arrives after the line .map {($0.value, $1)} is exactly like what withLatestFrom would produce: a tuple of both publishers' most recent values.
As improvement of #matt answer this is more convenient withLatestFrom, that fires on same event in original stream
Updated: Fix issue with combineLatest in iOS versions prior to 14.5
extension Publisher {
func withLatestFrom<P>(
_ other: P
) -> AnyPublisher<(Self.Output, P.Output), Failure> where P: Publisher, Self.Failure == P.Failure {
let other = other
// Note: Do not use `.map(Optional.some)` and `.prepend(nil)`.
// There is a bug in iOS versions prior 14.5 in `.combineLatest`. If P.Output itself is Optional.
// In this case prepended `Optional.some(nil)` will become just `nil` after `combineLatest`.
.map { (value: $0, ()) }
.prepend((value: nil, ()))
return map { (value: $0, token: UUID()) }
.combineLatest(other)
.removeDuplicates(by: { (old, new) in
let lhs = old.0, rhs = new.0
return lhs.token == rhs.token
})
.map { ($0.value, $1.value) }
.compactMap { (left, right) in
right.map { (left, $0) }
}
.eraseToAnyPublisher()
}
}
Kind-of a non-answer, but you could do this instead:
buttonTapped.sink { [unowned self] in
print(textFieldContent)
}
This code is fairly obvious, no need to know what withLatestFrom means, albeit has the problem of having to capture self.
I wonder if this is the reason Apple engineers didn't add withLatestFrom to the core Combine framework.

FetchedResults won't trigger and SwiftUI update but the context saves it successfully

I am trying to update an attribute in my Core Data through an NSManagedObject. As soon as I update it, I save the context and it gets saved successfully.
Problem
After the context saves it, the UI (SwiftUI) won't update it with the new value. If I add a completely new Children into Core Data (insert) the UI gets updated.
What I tried:
Asperi approach - I can print out the correct new value in .onReceive but the UI doesn't update
Using self.objectWillChange.send() before context.save() - didn't work either
Changed the Int16 to String, because I was speculating that somehow Int16 is not observable? Didn't work either
Is this a SwiftUI bug?
As-is State
//only updating the data
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
let fetchReq = NSFetchRequest<Card>(entityName: "Card") //filter distributorID; should return only one Card
fetchReq.resultType = .managedObjectResultType
fetchReq.predicate = NSPredicate(format: "id == %#", params["id"]!) //I get an array with cards
var cards :[Card] = []
cards = try context.fetch(fetchReq)
//some other functions
cards[0].counter = "3" //
//...
self.objectWillChange.send() //doesn't do anything?
try context.save()
//sucessfully, no error. Data is there if I restart the app and "force" a reload of the UI
//Core Data "Card"
extension Card :Identifiable{
#nonobjc public class func fetchRequest() -> NSFetchRequest<Card> {
return NSFetchRequest<Card>(entityName: "Card")
}
//...
#NSManaged public var id: String
//....
}
//Main SwiftUI - data is correctly displayed on the card
#FetchRequest(entity: Card.entity(),
sortDescriptors: [],
predicate: nil)
var cards: FetchedResults<Card>
List {
ForEach(cards){ card in
CardView(value: Int(card.counter)!, maximum: Int(card.maxValue)!,
headline: card.desc, mstatement: card.id)
}
If first code block is a part of ObservableObject, then it does not look that third block, view one, depends on it, and if there is no changes in dependencies, view is not updated.
Try this approach.
But if there are dependencies, which are just not provided then change order of save and publisher as
try context.save()
self.objectWillChange.send()

Resources