How to assign a property of an actor to a view controller property? - swift

I have an actor like this:
I want to assign a value from the NewsCache actor to the ViewController, but it gives the following error:
Main actor-isolated property 'cache_data' can not be mutated from a Sendable closure
actor NewsCache {
var current_news_on_fetch : [String:Bool] = [:]
var news_dict : [String:NewsFeedPageStruct] = [:]
var time_dict_news : [String:String] = [:]
var ordered_uuid_news : [String] = []
}
class ViewController : UIViewController {
var cache_data : [String : NewsFeedPageStruct] = [:]
var actor_news = NewsCache()
func setData(){
print("Below is the problem")
Task.detached { [self] in
self.cache_data = await self.actor_news.get_uuids()//this is the line which produces the error
await self.reloadSnapshot(hash_news: str_arr.0) //this is in the MainActor
}
}
}

You can use MainActor.run to run a closure on the main actor. But MainActor.run takes a synchronous (non-async) closure, so you'll want to call get_uuids before calling MainActor.run:
func setData_0(){
Task.detached {
let cache_data = await self.actor_news.get_uuids()
await MainActor.run {
self.cache_data = cache_data
self.reloadSnapshot()
}
}
}
Another solution is to wrap the assignment to cache_data in a #MainActor-bound closure that you execute immediately:
func setData_1(){
Task.detached {
await { #MainActor in
self.cache_data = await self.actor_news.get_uuids()
}()
await self.reloadSnapshot()
}
}

Related

MainActor and async await when reading and writing

I understand the new async syntax in Swift in the sense that if I call it, then it will handle a pool of asynchronous queues / threads (whatever) to do the work. What I don't understand is how we return to the main thread once it's all over.
// On main thread now
let manager = StorageManager()
let items = await manager.fetch // returns on main thread?
struct StorageManager {
private func read() throws -> [Item] {
let data = try file.read()
if data.isEmpty { return [] }
return try JSONDecoder().decode([Item].self, from: data)
}
func fetch() async {
fetchAndWait()
}
func fetchAndWait() {
if isPreview { return }
let items = try? read()
fetchedItems = items ?? []
}
func save() throws {
let data = try JSONEncoder().encode(fetchedItems)
try file.write(data)
}
}
I want to make sure that I read and write from/to disk in the correct way i.e. is thread safe when necessary and concurrent where possible. Is it best to declare this struct as a #MainActor ?
There is nothing in the code you've given that uses async or await meaningfully, and there is nothing in the code you've given that goes onto a "background thread", so the question as posed is more or less meaningless. If the question did have meaning, the answer would be: to guarantee that code doesn't run on the main thread, put that code into an actor. To guarantee that code does run on the main thread, put that code into a #MainActor object (or call MainActor.run).
The async methods do not return automatically to the main thread, they either:
complete in the background whatever they are doing
or
explicitly pass at a certain moment the execution to the main thread through a #MainActor function/ class. (edited following #matt's comment)
In the code above you can start by correcting the fact that fetch() does not return any value (items will receive nothing based on your code).
Example of your code for case 1 above:
let manager = StorageManager()
let items = await manager.fetch // not on the main thread, the value will be stored in the background
struct StorageManager {
private func read() throws -> [Item] {
let data = try file.read()
if data.isEmpty { return [] }
return try JSONDecoder().decode([Item].self, from: data)
}
func fetch() async -> [Item] {
if isPreview { return }
let items = try? read()
return items ?? []
}
func save() throws {
let data = try JSONEncoder().encode(fetchedItems)
try file.write(data)
}
}
Example for case 2 above (I created an #Published var, which should only be written on the main thread, to give you the example):
class ViewModel: ObservableObject {
let manager = StorageManager()
#Published var items = [Item]() // should change value only on main thread
func updateItems() {
Task { // Enter background thread
let fetchedItems = await self.manager.fetch()
// Back to main thread
updateItemsWith(fetchedItems)
}
}
#MainActor private func updateItemsWith(newItems: [Item]) {
self.items = newItems
}
}
struct StorageManager {
private func read() throws -> [Item] {
let data = try file.read()
if data.isEmpty { return [] }
return try JSONDecoder().decode([Item].self, from: data)
}
func fetch() async -> [Item] {
if isPreview { return }
let items = try? read()
return items ?? []
}
func save() throws {
let data = try JSONEncoder().encode(fetchedItems)
try file.write(data)
}
}

How to associate a block of code with a #State var when it changes

Whenever the value of the #State variable myData changes I would like to be notified and store that data in an #AppStorage variable myStoredData. However, I don't want to have to write explicitly this storing code everywhere the state var is changed, I would like to associate a block of code with it that gets notified whenever the state var changes and performs storage. The reason for this is, for example, I want to pass the state var as a binding to another view, and when that view would change the variable, the storage block would automatically be executed. How can I do this/can I do this in SwiftUI?
struct MyView : View {
#AppStorage("my-data") var myStoredData : Data!
#State var myData : [String] = ["hello","world"]
var body : some View {
Button(action: {
myData = ["something","else"]
myStoredData = try? JSONEncoder().encode(myData)
}) {
Text("Store data when button pressed")
}
.onAppear {
myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
}
}
}
I'm looking for something like this, but this does not work:
#State var myData : [String] = ["hello","world"] {
didSet {
myStoredData = try? JSONEncoder().encode(myData)
}
}
Simply use .onChange modifier anywhere in view (like you did with .onAppear)
struct MyView : View {
#AppStorage("my-data") var myStoredData : Data!
#State var myData : [String] = ["hello","world"]
var body : some View {
Button(action: {
myData = ["something","else"]
myStoredData = try? JSONEncoder().encode(myData)
}) {
Text("Store data when button pressed")
}
.onChange(of: myData) {
myStoredData = try? JSONEncoder().encode($0) // << here !!
}
.onAppear {
myData = (try? JSONDecoder().decode([String].self, from: myStoredData)) ?? []
}
}
}
You can add a set callback extension to the binding to monitor the value change
extension Binding {
/// When the `Binding`'s `wrappedValue` changes, the given closure is executed.
/// - Parameter closure: Chunk of code to execute whenever the value changes.
/// - Returns: New `Binding`.
func onChange(_ closure: #escaping () -> Void) -> Binding<Value> {
Binding(get: {
wrappedValue
}, set: { newValue in
wrappedValue = newValue
closure()
})
}
}
Use extension code
$myData.onChange({
})

Swift: Thread-safe dictionary access via cocoa-bindings

I have a class and I need to bind a few NSTextFields to some values of a dictionary that will be changed by a thread.
class Test: NSObject {
#objc dynamic var dict : [String:Int] = [:]
let queue = DispatchQueue(label: "myQueue", attributes: .concurrent)
func changeValue() {
queue.async(flags: .barrier) {
self.dict["Key1"] = Int.random(in: 1..<100)
}
}
func readValue() -> Int? {
queue.sync {
return self.dict["Key1"]
}
}
}
As far as I understood this is the way to do (so not accessing the variable directly but through a func that handles the queue.
But what when I try to bind a NSTextField to "Key1" of the dict using cocoa bindings?
Binding to the variable "dict" directly works in my tests but I'm not sure (I'm quite sure it isn't) if this is thread safe.
What would be the correct way to do this?
Edit:
This code example looks legit but fails for some reason
class Test: NSObject {
var _dict : [String:Int] = ["Key1":1]
let queue = DispatchQueue(label: "myQueue", attributes: .concurrent)
#objc dynamic var dict:[String:Int] {
get {
queue.sync { return self._dict }
}
set {
queue.async(flags: .barrier) { self._dict = newValue }
}
}
func changeValue() {
queue.async(flags: .barrier) {
// This change will not be visible in the bound object
self._dict["Key1"] = Int.random(in: 1..<100)
// This causes a crash
self.dict = ["Key1":2]
}
}
}

How to assign sink result to variable

I would like to assign a result form a notification center publisher to the variable alert. The Error that I get is:
Cannot use instance member 'alerts' within property initializer; property initializers run before 'self' is available
Could Someone help me out here?
import Foundation
import SwiftUI
import Combine
final class PublicAlerts: ObservableObject{
init () {
fetchAlerts()
}
var alerts = [String](){
didSet {
didChange.send(self)
}
}
private func fetchPublicAssets(){
backEndService().fetchAlerts()
}
let publicAssetsPublisher = NotificationCenter.default.publisher(for: .kPublicAlertsNotification)
.map { notification in
return notification.userInfo?["alerts"] as! Array<String>
}.sink {result in
alerts = result
}
let didChange = PassthroughSubject<PublicAlerts, Never>()
}
Later I will use alerts this in SwiftUI as a List
Move the subscribtion in init
final class PublicAlerts: ObservableObject{
var anyCancelable: AnyCancellable? = nil
init () {
anyCancelable = NotificationCenter.default.publisher(for: .kPublicAlertsNotification)
.map { notification in
return notification.userInfo?["alerts"] as! Array<String>
}.sink {result in
alerts = result
}
fetchAlerts()
}
var alerts = [String](){
didSet {
didChange.send(self)
}
}
private func fetchPublicAssets(){
backEndService().fetchAlerts()
}
let didChange = PassthroughSubject<PublicAlerts, Never>()
}

Swift: Should ViewModel be a struct or class?

I am trying to use MVVM pattern in my new project. First time, I created all my view model to struct. But when I implemented async business logic such as fetchDataFromNetwork with closures, closures capture old view model value then updated to that. Not a new view model value.
Here is a test code in playground.
import Foundation
import XCPlayground
struct ViewModel {
var data: Int = 0
mutating func fetchData(completion:()->()) {
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
result in
self.data = 10
print("viewModel.data in fetchResponse : \(self.data)")
completion()
XCPlaygroundPage.currentPage.finishExecution()
}.resume()
}
}
class ViewController {
var viewModel: ViewModel = ViewModel() {
didSet {
print("viewModel.data in didSet : \(viewModel.data)")
}
}
func changeViewModelStruct() {
print("viewModel.data before fetch : \(viewModel.data)")
viewModel.fetchData {
print("viewModel.data after fetch : \(self.viewModel.data)")
}
}
}
var c = ViewController()
c.changeViewModelStruct()
Console prints
viewModel.data before fetch : 0
viewModel.data in didSet : 0
viewModel.data in fetchResponse : 10
viewModel.data after fetch : 0
The problem is View Model in ViewController does not have new Value 10.
If I changed ViewModel to class, didSet not called but View Model in ViewController has new Value 10.
You should use a class.
If you use a struct with a mutating function, the function should not perform the mutation within a closure; you should not do the following:
struct ViewModel {
var data: Int = 0
mutating func myFunc() {
funcWithClosure() {
self.data = 1
}
}
}
If I changed ViewModel to class, didSet not called
Nothing wrong here - that's the expected behavior.
If you prefer to use struct, you can do
func fetchData(completion: ViewModel ->()) {
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
result in
var newViewModel = self
newViewModel.data = 10
print("viewModel.data in fetchResponse : \(self.data)")
completion(newViewModel)
XCPlaygroundPage.currentPage.finishExecution()
}.resume()
}
viewModel.fetchData { newViewModel in
self.viewModal = newViewModel
print("viewModel.data after fetch : \(self.viewModel.data)")
}
Also note that the closure provided to dataTaskWithURL does not run on the main thread. You might want to call dispatch_async(dispatch_get_main_queue()) {...} in it.
You could get the self.data in two options: either use a return parameter in your closure for fetchResponse( using viewModel as struct) OR you can create your own set-method/closure and use it in your init method(using viewModel as class).
class ViewModel {
var data: Int = 0
func fetchData(completion:()->()) {
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
result in
self.data = 10
print("viewModel.data in fetchResponse : \(self.data)")
completion()
XCPlaygroundPage.currentPage.finishExecution()
}.resume()
}
}
class ViewController {
var viewModel: ViewModel! { didSet { print("viewModel.data in didSet : \(viewModel.data)") } }
init( viewModel: ViewModel ) {
// closure invokes didSet
({ self.viewModel = viewModel })()
}
func changeViewModelStruct() {
print("viewModel.data before fetch : \(viewModel.data)")
viewModel.fetchData {
print("viewModel.data after fetch : \(self.viewModel.data)")
}
}
}
let viewModel = ViewModel()
var c = ViewController(viewModel: viewModel)
c.changeViewModelStruct()
Console prints:
viewModel.data in didSet : 0
viewModel.data before fetch : 0
viewModel.data in fetchResponse : 10
viewModel.data after fetch : 10
Apple Document
says like this:
willSet and didSet observers are not called when a property is first initialized. They are only called when the property’s value is set outside of an initialization context.