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.
Related
I was trying out the new async/await pattern in swift and I came accross something which I found it confusing.
struct ContentView: View {
var body: some View {
Text("Hello World!")
.task {
var num = 1
Task {
print(num)
}
Task {
print(num)
}
}
}
func printScore() async {
var score = 1
Task { print(score) }
Task { print(score) }
}
}
Can someone please clarify looking at the above screenshot on why the compiler only complaints about captured var inside printScore() function and does not complaint when the same is being done using the task modifier on the body computed property of the ContentView struct (i.e Line 14-24) ?
This is the example I came up with and got confused on the compiler behavior.I also change the compiler setting "Strict Concurrency Checking” build setting to “Complete” and still don't see the compiler complaining.
This is a special power of #MainActor. In a View, body is tagged MainActor:
#ViewBuilder #MainActor var body: Self.Body { get }
Tasks inherit the context of their caller, so those are are also MainActor. If you replace Task with Task.detached in body, you will see the same error, since this will move the Task out of the MainActor context.
Conversely, if you add #MainActor to printScore it will also compile without errors:
#MainActor func printScore() async {
var score = 1
Task { print(score) }
Task { print(score) }
}
score inherits the MainActor designation, and is protected. There is no concurrent access to score, so this is fine and correct.
This actually applies to all actors, but I believe the compiler has a bug that makes things not quite behave the way you expect. The following code compiles:
actor A {
var actorVar = 1
func capture() {
var localVar = 1
Task {
print(actorVar)
print(localVar)
}
}
}
However, if you remove the reference to actorVar, the closure passed to Task will not be put into the actor's context, and that will make the reference to localVar invalid. IMO, this is a compiler bug.
I have the following View:
struct ContentView: View {
#State var data = [SomeClass]()
var body: some View {
List(data, id: \.self) { item in
Text(item.someText)
}
}
func fetchDataSync() {
Task.detached {
await fetchData()
}
}
#MainActor
func fetchData() async {
let data = await SomeService.getAll()
self.data = data
print(data.first?.someProperty)
// > Optional(115)
print(self.data.first?.someProperty)
// > Optional(101)
}
}
now the method fetchDataSync is a delegate that gets called in a sync context whenever there is new data. I've noticed that the views don't change so I've added the printouts. You can see the printed values, which differ. How is this possible? I'm in a MainActor, and I even tried detaching the task. Didn't help. Is this a bug?
It should be mentioned that the objects returned by getAll are created inside that method and not given to any other part of the code. Since they are class objects, the value might be changed from elsewhere, but if so both references should still be the same and not produce different output.
My theory is that for some reason the state just stays unchanged. Am I doing something wrong?
Okay, wow, luckily I ran into the Duplicate keys of type SomeClass were found in a Dictionary crash. That lead me to realize that SwiftUI is doing some fancy diffing stuff, and using the == operator of my class.
The operator wasn't used for actual equality in my code, but rather for just comparing a single field that I used in a NavigationStack. Lesson learned. Don't ever implement == if it doesn't signify true equality or you might run into really odd bugs later.
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.)
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)
I'm building an app using MVVM and ReactiveCocoa to do bindings between the viewModel and the UI, however the view model validation signal subscribe block is not getting called.
My view model is pretty simple and barebones:
class ViewModel: RVMViewModel {
var name: String = "" {
willSet {
println("New Value: \(newValue)")
}
}
required init(){
super.init()
let signal = self.rac_valuesForKeyPath("name", observer: self)
signal.subscribeNext {
println("Subscribe block: \($0)")
}
}
}
In my view controller, I have the following bindings:
//observe ui and programatic changes
RACSignal.merge([self.nameField.racTextSignal(), self.nameField.rac_valuesForKeyPath("text", observer:self)]).subscribeNext({
(next) -> Void in
if let text = next as? String {
self.viewModel.name = text
}
})
RAC(self.nameField, "text") = self.viewModel.rac_valuesForKeyPath("name", observer: self)
I got the RAC macro working in swift based off what I read here.
Now, in my view bindings in my view controller, the subscribeNext blocks are called just fine. In my viewModel, in willSet, the new value prints out. HOWEVER, the subscribe block on my signal in my init block is only being called once, when the property is first initialized. This is driving me up a wall, anyone have any ideas?
I found a solution after a bunch of experimenting. By assigning a signal directly to the view model property, the subscribe block is called every time the value changes.
So instead of doing this:
RACSignal.merge([self.nameField.racTextSignal(), self.nameField.rac_valuesForKeyPath("text", observer:self)]).subscribeNext({
(next) -> Void in
if let text = next as? String {
self.viewModel.name = text
}
})
I did this:
RAC(self.viewModel, "name") <~ RACSignal.merge([self.nameField.racTextSignal(),
self.nameField.rac_valuesForKeyPath("text", observer:self)])
I used this link to get the RAC and <~ to work in swift.
I do not have a solution yet - I am away from my laptop till evening. However, try making signal in the global scope or an instance variable... If that doesn't work, try it on a singleton as a method you explicitly call ... These are more tests but if you tell me how it goes we can work it out together.
A better solution than the one that's accepted is to simply mark the property dynamic:
dynamic var name: String = "" {
willSet {
println("New Value: \(newValue)")
}
}
This enables Obj-C level KVO which is typically disabled for Swift only properties.