SwiftUI serial DispatchQueue in background thread - swift

In SwiftUI I have a List() with the name and status of 3 tasks:
import SwiftUI
struct ContentView: View {
let tasks = [TaskOne(), TaskTwo(), TaskThree()]
var body: some View {
List(tasks) { task in
Text(task.name)
Text(task.httpCode) // TODO
}
Button(action: { runTasks() }) {
Text("Run tasks")
}
}
private func runTasks() {
for task in tasks {
task.run()
// 1. Pass the output to the next task once finished
// 2. Update the status of the task in the List
}
}
}
Each task does http request but can have custom logic and they need to be separate:
protocol ExecutableTask {
var name: String { get set }
var httpCode: Int? { get set }
func execute(input: String) -> String
}
struct TaskOne: ExecutableTask {
var name = "one"
var httpCode: Int?
func execute(input: String) -> String {
// task specific logic here
// TODO: Perform HTTP call to "http://example.com/\(input)" and update httpCode in the List
return "first"
}
}
struct TaskTwo: ExecutableTask {
var name = "two"
var httpCode: Int?
func execute(input: String) -> String {
// task specific logic here
// TODO: Perform HTTP call to "http://example.com/\(input)" and update httpCode in the List
return "second"
}
}
struct TaskThree: ExecutableTask {
var name = "three"
var httpCode: Int?
func execute(input: String) -> String {
// task specific logic here
// TODO: Perform HTTP call to "http://example.com/\(input)" and update httpCode in the List
return "third"
}
}
How can I execute all of the serially in a background thread, passing the output from one to another and updating status in the List?
I suppose I need to update each row in the table by using #State and perform task execution using DispatchQueue. However I'm not entirely sure how to do that.

Related

Using Child SwitchStore Views within a Parent's ForEachStore View

I'm facing an issue implementing The Swift Composable Architecture in which I have a list of IdentifiedArray rows within my AppState that holds RowState which holds an EnumRowState as part of it's state, to allow me to SwitchStore on it within a RowView that is rendered within a ForEachStore on the AppView.
The problem I'm running into is that within a child reducer called liveReducer, there is an Effect.timer that is updating every one second, and should only be causing the LiveView to re-render.
However what's happening is that the AddView is also getting re-rendered every time too! The reason I know this is because I've manually added a Text("\(Date())") within the AddView and I see the date changing every one second regardless of the fact that it's not related in anyway to a change in state.
I've added .debug() to the appReducer and I see in the logs that the rows part of the sate shows ...(1 unchanged) which sounds right, so why then is every single row being re-rendered on every single Effect.timer effect?
Thank you in advance!
Below is how I've implemented this:
P.S. I've used a technique as describe here to pullback reducers on an enum property:
https://forums.swift.org/t/pullback-reducer-on-enum-property-switchstore/52628
Here is a video of the problem, in which you can see that once I've selected a name from the menu, ALL the views are getting updated, INCLUDING the navBarTitle!
https://youtube.com/shorts/rn_Yd57n1r8
struct AppState: Equatable {
var rows: IdentifiedArray<UUID, RowState> = []
}
enum AppAction: Equatable {
case row(id: UUID, action: RowAction)
}
public struct RowState: Equatable, Identifiable {
public var enumRowState: EnumRowState
public let id: UUID
}
public enum EnumRowState: Equatable {
case add(AddState)
case live(LiveState)
}
public enum RowAction: Equatable {
case live(AddAction)
case add(LiveAction)
}
public struct LiveState: Equatable {
public var secondsElapsed = 0
}
enum LiveAction: Equatable {
case onAppear
case onDisappear
case timerTicked
}
struct AppState: Equatable { }
enum AddAction: Equatable { }
public let liveReducer = Reducer<LiveState, LiveAction, LiveEnvironment>.init({
state, action, environment in
switch action {
case .onAppear:
return Effect
.timer(
id: state.baby.uid,
every: 1,
tolerance: .zero,
on: environment.mainQueue)
.map { _ in
LiveAction.timerTicked
})
case .timerTicked:
state.secondsElapsed += 1
return .none
case .onDisappear:
return .cancel(id: state.baby.uid)
}
})
public let addReducer = Reducer<AddState, AddAction, AddEnvironment>.init({
state, action, environment in
switch action {
})
}
///
/// Intermediate reducers to pull back to an Enum State which will be used within the `SwitchStore`
///
public var intermediateAddReducer: Reducer<EnumRowState, RowAction, RowEnvironment> {
return addReducer.pullback(
state: /EnumRowState.add,
action: /RowAction.add,
environment: { ... }
)
}
public var intermediateLiveReducer: Reducer<EnumRowState, RowAction, RowEnvironment > {
return liveReducer.pullback(
state: /EnumRowState.live,
action: /RowAction.live,
environment: { ... }
)
}
public let rowReducer: Reducer<RowState, RowAction, RowEnvironment> = .combine(
intermediateAddReducer.pullback(
state: \RowState.enumRowState,
action: /RowAction.self,
environment: { $0 }
),
intermediateLiveReducer.pullback(
state: \RowState.enumRowState,
action: /RowAction.self,
environment: { $0 }
)
)
let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
rowReducer.forEach(
state: \.rows,
action: /AppAction.row(id:action:),
environment: { ... }
),
.init({ state, action, environment in
switch action {
case AppAction.onAppear:
state.rows = [
RowState(id: UUID(), enumRowState: .add(AddState()))
RowState(id: UUID(), enumRowState: .add(LiveState()))
]
return .none
default:
return .none
}
})
)
.debug()
public struct AppView: View {
let store: Store<AppState, AppAction>
public var body: some View {
WithViewStore(self.store) { viewStore in
List {
ForEachStore(
self.store.scope(
state: \AppState.rows,
action: AppAction.row(id:action:))
) { rowViewStore in
RowView(store: rowViewStore)
}
}
}
}
public struct RowView: View {
public let store: Store<RowState, RowAction>
public var body: some View {
WithViewStore(self.store) { viewStore in
SwitchStore(self.store.scope(state: \RowState.enumRowState)) {
CaseLet(state: /EnumRowState.add, action: RowAction.add) { store in
AddView(store: store)
}
CaseLet(state: /EnumRowState.live, action: RowAction.live) { store in
LiveView(store: store)
}
}
}
}
}
struct LiveView: View {
let store: Store<LiveState, LiveAction>
var body: some View {
WithViewStore(self.store) { viewStore in
Text(viewStore.secondsElapsed)
}
}
}
struct AddView: View {
let store: Store<AddState, AddAction>
var body: some View {
WithViewStore(self.store) { viewStore in
// This is getting re-rendered every time the `liveReducer`'s `secondsElapsed` state changes!?!
Text("\(Date())")
}
}
}

Finding an item in an array and updating its value

I have a DataModel with the following struct:
struct Task: Codable, Hashable, Identifiable {
var id = UUID()
var title: String
var completed = false
var priority = 2
}
In one my views, I populate a list with buttons based on each element of that DataModel, essentially a Task.
I pass each task for the view like this:
ForEach(taskdata.filteredTasks(byPriority: byPriotity), id: \.id) { task in
TaskView(task: task)
}
On the view, I add each button as follow:
struct TaskView: View {
#EnvironmentObject private var taskdata: DataModel
#State var task: Task
var body: some View {
Button(action: {
task.completed.toggle() }) {
....
}
}
}
Now, when the user clicks on the button, if completed was false, it makes it true. Simple. However, this doesn't update the DataModel, so I added the following to see if I could find the item in question and update it:
struct TaskView: View {
#EnvironmentObject private var taskdata: DataModel
#State var task: Task
#State var id: UUID // added this so I can also pass the ID and try to find it
var body: some View {
Button(action: {
task.completed.toggle()
if task.completed { // if it's completed, find it an update it.
//taskdata.tasks.first(where: { $0.id == id })?.completed = true
//taskdata.tasks.filter {$0.id == id}.first?.completed = true
/*if let index = taskdata.tasks.first(where: {$0.id == id}) {
print(index)
taskdata.tasks ????
}*/
}
}) {
....
}
All the commented code is what I have tried... I get different errors, from Cannot assign to property: function call returns immutable value to cannot assign to property: 'first' is a get-only property and a few other colorful Xcode errors.
How do I find the correct object on my DataModel and correctly update its .completed to true (or false if it's clicked again).
Thank you
You need to use Array >>> firstIndex
if let index = taskdata.tasks.firstIndex(where: {$0.id == id}) {
taskdata.tasks[index].completed = true
}
There is another way for your problem using a function in extension like this custom update function:
extension Array {
mutating func update(where condition: (Element) -> Bool, with newElement: (Element) -> Element) {
if let unwrappedIndex: Int = self.firstIndex(where: { value in condition(value) }) {
self[unwrappedIndex] = newElement(self[unwrappedIndex])
}
}
}
use case:
taskdata.tasks.update(where: { element in element.id == id }, with: { element in
var newElement: Task = element
newElement.completed = true
return newElement
})

How do I reference an `actor`'s properties from a `Button`'s action or `Binding`?

I have an actor like this, which performs long, complex work constantly in the background:
actor Foo {
var field: [Bar]
struct Bar {
// ...
}
}
How do I update its field from a SwiftUI view?
I tried this, but got these errors:
import SwiftUI
struct MyView: View {
#StateObject
var foo: Foo
var body: some View {
Text("Field count is \(foo.field.count)") // 🛑 Actor-isolated property 'field' can not be referenced from the main actor
Button("Reset foo") {
foo.field = [] // 🛑 Actor-isolated property 'field' can not be mutated from the main actor
}
}
}
How do I access & mutate my actor from within a SwiftUI view?
The problem with accessing the field property of the actor is that it requires and await call, if the access is made outside of the actor. This means a suspension point in your SwiftUI code, which means that when the SwiftUI code resumes, it might no longer be executing on the main thread, and that's a big problem.
If the actor doesn't do background work, then Asperi's solution that uses #MainAction would nicely work, as in that case the SwiftUI accesses happen on the main thread.
But if the actor runs in the background, you need another sync point that runs code on the main thread, that wraps the Foo actor, and which is consumed by your view:
actor Foo {
private(set) var field: [Bar]
func updateField(_ field: [Bar]) {
self.field = field
}
struct Bar {
// ...
}
}
class FooModel: ObservableObject {
private let foo: Foo
#Published var field: [Foo.Bar] = [] {
didSet {
Task { await foo.updateField(field) }
}
}
init(foo: Foo) {
self.foo = foo
Task { self.field = await foo.field }
}
}
struct MyView: View {
#StateObject
var foo: FooModel
However this is only half of the story, as you'll need to also send notifications from Foo to FooModel when the value of field changes. You can use a PassthroughSubject for this:
actor Foo {
var field: [Bar] {
didSet { fieldSubject.send(field) }
}
private let fieldSubject: PassthroughSubject<[Bar], Never>
let fieldPublisher: AnyPublisher<[Bar], Never>
init() {
field = ... // initial value
fieldSubject = PassthroughSubject()
fieldPublisher = fieldSubject.eraseToAnyPublisher()
}
func updateField(_ field: [Bar]) {
self.field = field
}
struct Bar {
// ...
}
}
and subscribe to the published from the model:
class FooModel: ObservableObject {
private let foo: Foo
#Published var field: [Foo.Bar] = [] {
didSet {
Task { await foo.updateField(field) }
}
}
init(foo: Foo) {
self.foo = foo
Task { self.field = await foo.field }
foo.fieldPublisher.receive(on: DispatchQueue.main).assign(to: &$field)
}
}
As you can see, there's a non-trivial amount of code to be written, due to the fact that actors run on arbitrary threads, while your SwiftUI code (or any UI code in general) must be run only on the main thread.

Variable listener in iOS Swift

I am trying to make an independent class/library that runs on its own and handles errors internally, but also gives feedback (status) to the frontend for possible user interaction. In Android I'd solve this with a Listener:
class Status(errCode:Int=0, msg:String=""){
var errCode:Int=0
var text:String=""
private var listener: ChangeListener? = null
init {
this.errCode=errCode
this.text=msg
}
fun set(errCode:Int, msg:String) {
this.errCode=errCode
this.text=msg
if (listener != null) listener!!.onChange()
}
fun getListener(): ChangeListener? {
return listener
}
fun setListener(listener: ChangeListener?) {
this.listener = listener
}
interface ChangeListener {
fun onChange()
}
}
Whenever I want to update it, e.g. on Exception, I call:
catch (e: IOException) {
this.status.set(109,"someException: $e")
}
And in the MainActivity I just have to handle these changes:
myObj.status.setListener(object : Status.ChangeListener {
override fun onChange() {
when(myObj!!.status.errCode) {
//errors
109 -> log(myObj!!.status.text) //some Exception
111 -> myObj!!.restart() //another exc
112 -> myObj!!.checkAll() //some other error
...
In Swift I found didSet that seems similar. I read you can attach it to a struct so whenever something in the struct changes, it is called. So I added my "struct Status" to my "struct myObj" like this:
struct myObj {
var status:Status=Status(errCode: 0, msg: "Init")
struct Status {
var errCode:Int
var msg:String
mutating func set(err:Int, txt:String){
errCode=err
msg=txt
}
}
and in my code I initialize a new instance of myObj
var mObj:myObj=myObj.init() {
didSet{
switch myObj.status.errCode{
case 1:
promptOkay()
case 113:
obj.errorHandling()
default:
log(txt: mObj.status.msg)
}
}
}
However, it never trigger didSet, even though internal functions should change the Status. I read that didSet is not triggered on init, but I do not run anything right now after initializing the class object that should run quite independently. I want to verify that the approach is okay before I go further and have to unravel everything again.
didSet must be declared on the property, not during initialization:
class MyObj {
var status: Status = Status(errCode: 0, msg: "Init") {
didSet {
// status did change
print("new error code: \(status.errCode)")
}
}
struct Status {
var errCode:Int
var msg:String
mutating func set(err:Int, txt:String){
errCode = err
msg = txt
}
}
}
let obj = MyObj()
obj.status.set(err: 10, txt: "error 10") // prints "new error code: 10"
At this point you can react to every changes made to obj.status in the didSetClosure.
Edit — React to didSet from outside the class
If you want to respond to changes from outside the MyObj, I would recommend using a closure:
class MyObj {
var status: Status = Status(errCode: 0, msg: "Init") {
didSet {
statusDidChange?(status)
}
}
// closure to call when status changed
var statusDidChange: ((Status) -> Void)? = nil
struct Status {
var errCode:Int
var msg:String
mutating func set(err:Int, txt:String){
errCode = err
msg = txt
}
}
}
This way, you can assign a closure from outside to perform custom actions:
let obj = MyObj()
obj.statusDidChange = { status in
// status did change
print("new error code: \(status.errCode)")
}
obj.status.set(err: 10, txt: "error 10") // prints "new error code: 10"
Edit 2 — Call didSet closure directly from initialization
You also can manually call the statusDidChange closure during the init.
class MyObj {
var status: Status = Status(errCode: 0, msg: "Init") {
didSet {
statusDidChange(status)
}
}
// closure to call when status changed
var statusDidChange: (Status) -> Void
init(status: Status, statusDidChange: #escaping (Status) -> Void) {
self.status = status
self.statusDidChange = statusDidChange
self.statusDidChange(status)
}
}
let obj = MyObj(status: MyObj.Status(errCode: 9, msg: "error 09")) { status in
// status did change
print("new error code: \(status.errCode)")
}
obj.status.set(err: 10, txt: "error 10")
This will print
new error code: 9
new error code: 10

Swift - How to manage turn order management?

I'm working on a local game which relies on turn order.
Rules;
There are a number of phases in the game (ie: Buy, Sell)
During each phase, a player takes a single turn
Each phase is not considered complete until every player (in turn order) has completed their turn.
I'm not sure how to manage this data. There are a number of things to track.
The phase we are in
The current player on turn
When all players have completed their turns
When the end of the turn order has been reached so we can move to next phase.
Resetting all turn completions when all phases are complete
I'm thinking that a subscription model is the best approach to this, but I'm not used to such a pattern.
Currently I'm using a similar system to a to-do where the phase itself can be marked complete or incomplete.
This is the way I'm currently handling turn orders and phases in Swift playground.
// Turn order management
class TurnOrderManager: NSObject
{
static var instance = TurnOrderManager()
var turnOrder: [Player] = [Player]()
private var turnOrderIndex = 0
var current: Player {
return turnOrder[turnOrderIndex]
}
func next() {
if (self.turnOrderIndex < (self.turnOrder.count-1)) {
turnOrderIndex += 1
}
else {
print("Reached end of line")
}
}
}
class Player: NSObject {
var name: String = ""
override var description: String {
return self.name
}
init(name: String) {
super.init()
self.name = name
}
}
let p1:Player = Player.init(name: "Bob")
let p2:Player = Player.init(name: "Alex")
TurnOrderManager.instance.turnOrder = [p1,p2]
print (TurnOrderManager.instance.current)
TurnOrderManager.instance.next()
print (TurnOrderManager.instance.current)
TurnOrderManager.instance.next()
print (TurnOrderManager.instance.current)
// ---------------------------------
// Phase management
enum PhaseType: Int {
case buying = 1
case selling
}
struct Phase {
var id: PhaseType
var title: String
var completed: Bool = false {
didSet {
// Notify subscribers of completion
guard completed else { return }
handlers.forEach { $0(self) }
}
}
var description:String {
return "Phase: \(self.title), completed: \(completed)"
}
// Task queue
var handlers = [(Phase) -> Void]()
init(id: PhaseType, title: String, initialSubscription: #escaping (Phase) -> Void =
{_ in})
{
self.id = id
self.title = title
subscribe(completion: initialSubscription)
}
mutating func subscribe(completion: #escaping (Phase) -> Void) {
handlers.append(completion)
}
}
class MyParentController {
lazy var phase1: Phase = {
return Phase(id: .buying, title: "Phase #1") {
print("Do something with phase: \($0.title)")
}
}()
}
let controller = MyParentController()
controller.phase1.completed = true
Question:
I'm wanting to notify:
Turn is complete
All turns are complete (so that it can move to next phase)
How do I make my TurnOrderManager alert the PhaseManager that the current turn is complete.
How do I make my PhaseManager know that when all turns are complete to move to the next phase.
I apologize for the verboseness of my query.
Many thanks
You're going to want to define a delegate relationship PhaseManager and your TurnOrderManager.
Here is the Apple docs on protocols.
Here is a great article on delegation.
First you'll need to define a protocol. Something like this:
protocol TurnManagerDelegate {
func complete(turn: objectType)
func allTurnsComplete()
}
Next you'll have to conform your PhaseManager to the protocol:
class PhaseManager: TurnManagerDelegate {
...
func complete(turn: objectType) {
// implement
}
func allTurnsComplete() {
// implement
}
...
}
Last you'll have to add a property on your TurnOrderManager with the delegate:
class TurnOrderManager {
...
var delegate: TurnManagerDelegate
...
}
and call the functions whenever needed in your TurnOrderManager:
...
delegate?.allTurnsComplete() //example
...
You'll also have to set your PhaseManager as the delegate before your TurnOrderManager would try to call any of the delegate methods.