{"id":26201612,"url":"https://github.com/solcat124/progressview","last_synced_at":"2026-04-29T12:36:27.324Z","repository":{"id":281759406,"uuid":"946334486","full_name":"solcat124/ProgressView","owner":"solcat124","description":"Demonstrate how progress of a function can be displayed using SwiftUI's ProgressView","archived":false,"fork":false,"pushed_at":"2025-03-11T02:12:30.000Z","size":107,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-11T02:29:23.711Z","etag":null,"topics":["determinate","macos","progressview","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/solcat124.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-03-11T01:23:17.000Z","updated_at":"2025-03-11T02:20:19.000Z","dependencies_parsed_at":"2025-03-11T02:29:25.170Z","dependency_job_id":"e2a4c29b-ec5b-4f6f-a128-a9a5977dd37b","html_url":"https://github.com/solcat124/ProgressView","commit_stats":null,"previous_names":["solcat124/progressview"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solcat124%2FProgressView","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solcat124%2FProgressView/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solcat124%2FProgressView/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/solcat124%2FProgressView/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/solcat124","download_url":"https://codeload.github.com/solcat124/ProgressView/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243148025,"owners_count":20243903,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["determinate","macos","progressview","swiftui"],"created_at":"2025-03-12T03:22:39.517Z","updated_at":"2025-12-24T12:16:20.648Z","avatar_url":"https://github.com/solcat124.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"#  Determinate ProgressView\n\n## About\n\nThe macOS app documented here looks at different aspects of displaying a determinate progress view. The goal is to understand how to have progress in the completion of a function displayed using ProgressView.\n\n![app view](ReadMe.app.png)\n\n- Version X: getting start -- run a lengthy operation without displaying a progress view\n- Version 0: the basics -- shows what a default determinate progress bar looks like; brings up 2 instances of a progress view with a static progress value\n- Version 1: show a progress bar that shows progress, but the progress isn't meaningful\n- Version 2: working example -- show a progress bar that shows progress of a function executing a lengthy operation\n- Version 3: enhances version 2\n- Version 4: a demonstration of how things fail \n- Version 5: an alternate of Version 3 based on async/await code\n\n## Getting Started\n\n### A Lengthy Operation\nExecuting a task, for example using a button to run the task, is straightforward. In this implementation, clicking the button launches a function `lengthyOperation`:\n\n```\nstruct RunViewX: View {\n    var body: some View {\n        VStack {\n            Button {\n                lengthyOperation()\n            } label: {\n                Text(\"Run\")\n            }\n        }\n     }\n}\n```\n\nAs the function may take a while to complete, providing feedback to the user that all is well can be helpful. (Another important consideration is that the application is unresponsive while `lengthyOperation` is running.)\n\nFor the sake of discussion, consider the following model code. The `lengthyOperation` function has some defined steps where progess can be reported, and goes off for extended time while computing these steps. While it might seem tempting to simulate the lengthy task using a sleep-like function, doing so leads one down a rabbit hole on how to avoid timer concurrency concerns that are likely not relevant here. Using a processing loop avoids side issues and probably better mimics code in an actual implementation.\n\n```\nvar lengthyProgress: Double = 0\n\n// MARK: - Code without async/await\n\nfunc lengthyOperation() {\n    print (\"long running function: start\")\n    lengthyProgress = 0\n    while lengthyProgress \u003c 100 {\n        lengthyProgress += 10\n        lengthyStep()\n        print (\"long running function: progress = \\(lengthyProgress)\")\n    }\n    print (\"long running function: done\")\n}\n\nfunc lengthyStep() {\n    for i in 0..\u003c1000000 {\n        let _ = i * 2\n    }\n}\n\n```\n\n### Determinate Progress View\nA progress view is one that shows progress toward completion of a task. An indeterminate view shows progress while completing a task of unknown duration.\nA determine view shows a measure of progress while completing a task with a determinate end, such as downloading a file with a known size or advancing through a defined number of steps.\n \nSwiftUI's `ProgressView` can be used to manage the display of the progress view. \n\nA basic determinate progress view with a given progress toward completion is shown in this image:\n\n![progress view](ReadMe.view.jpg)\n\nThe top portion shows the display when the progress and total values are 0, where the completion bar moves back and forth from 0 to 100%. The bottom portion displays a completion bar when the total is not 0.\n\nThis is the view code for the image above: \n\n```\nstruct View0: View {\n    var body: some View {\n        VStack(alignment: .leading) {\n            ProgressView(\"Processing...\", value: 0, total: 0.0)\n            Text(\"\\(Int(0))/100\")\n        }\n        .padding()\n\n        \n        VStack(alignment: .leading) {\n            ProgressView(\"Processing...\", value: 20, total: 100.0)\n            Text(\"\\(Int(20))/100\")\n        }\n        .padding()\n    }\n}\n```\n\nThis view code shows a button that toggles display of the progress view above:\n\n```\nstruct RunView0: View {\n    @State private var isRunning = false\n\n    var body: some View {\n        VStack {\n            Text(\"Static progress display\")\n            Toggle(isOn: $isRunning) {\n                Text(\"Version 0\")\n            }\n            .toggleStyle(.button)\n        }\n        .padding()\n\n        if isRunning {\n            View0()\n                .padding()\n        }\n\n     }\n}\n\n```\n\n## Asynchronous Operation\nRunning a model task and progress view synchronously means that they run sequentially. The result is that the progress view starts with the progress at 0, then the model task runs, and when the model task is finished the progress view is updated with the latest value of `progress` (quite possibly 100%). In other words, the actual progress is not effectively displayed. To be effective, the model task can be run asynchronously so that the view and model code run concurrently.\n\nIn this code a user launches a model task, `lengthyOperation` asynchronously: \n\n```\nstruct RunViewDispatch: View {\n    @State private var isRunning = false\n\n    var body: some View {\n        VStack {\n            Text(\"Progress is independent of the model code\")\n            Button {\n                isRunning = true\n                DispatchQueue.global(qos: .background).async {\n                    lengthyOperation()\n                }\n            } label: {\n                Text(\"Show version 1\")\n            }\n        }\n        .padding()\n\n        if isRunning {\n            View1(show: $isRunning)\n        }\n    }\n}\n```\n\n## Dynamic Progress: Adding a Timer\nDisplaying a dynamic level of progress is more invovled as some level of interaction is required so that progress made during the run task is used by the progress display. In a model-view paradigm, interaction between modeling code and view code must be done with care to avoid the **Publishing changes from background threads is not allowed** warning.\n\nOne way to deal with a dynamic level of progress is to use a timer, where timer events are used to update the view on the status of the model code. This works because the timer event can make use of the `.onReceive` method that deals with concurrency. This avoids a warning about publishing changes from background threads.\n\nHere's view code that adds a timer that dynamically creates an event where the progress can be updated.\n\n```\nstruct View1: View {\n    @Binding var show: Bool\n    @State private var progress: Double = 0.0\n    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()\n\n    var body: some View {\n        VStack(alignment: .leading) {\n            ProgressView(\"Processing...\", value: progress, total: 100.0)\n                .onReceive(timer) { _ in\n                    if progress \u003c 100.0 {\n                        progress += 10 // updates progress (but independent of model code)\n                    } else {\n                        show = false\n                    }\n                }\n            Text(\"\\(Int(progress))/100\")\n        }\n        .padding()\n    }\n}\n```\n\nAlthough the progress is dynamically updated, it isn't based on model code. So while this might look interesting, it likely isn't very useful without additional coding. We need the `progress` value to be linked to a value reported in model code. \n\n### App Version 1\n\nIn review, here is the sequence of events:\n\n- display a button to launch the `lengthyOperation` function -- see RunViewDispatch\n- start progress display -- see View1\n- run the task -- see the model implementation of `lengthyOperation`\n- stop progress display -- updates stop when progress = 100%, see View 1\n\nNote that the reported progress is independent of the function `lengthyOperation`, so the progress reported has nothing to do with the actual progress. This is considered next. \n\n![progress view1](ReadMe.view1.png)\n\n## Linking the View and Model\nIn the code above the progress view still has no knowledge of the status of `lengthyOperation`. It also seems that the timer will continue to run as long as the progress view is being executed.\n\nLooking back at the implementation of `lengthyOperation`, there is a global variable `lengthyProgress` that reports its progress.\n\nIn this version of the progress view the view `progress` is tied to `lengthyProgress`:\n\n\n```\nstruct View2: View {\n    @Binding var show: Bool\n    @State private var progress: Double = 0.0\n    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()\n\n    var body: some View {\n        VStack(alignment: .leading) {\n            ProgressView(\"Processing...\", value: progress, total: 100.0)\n                .onReceive(timer) { _ in\n                    progress = lengthyProgress     // lengthyProgress is updated in model code\n                    if progress \u003e= 100.0 {\n                        show = false\n                    }\n                }\n            Text(\"\\(Int(progress))/100\")\n        }\n        .padding()\n    }\n}\n```\n\n## Stop the Timer\nIt is possible to kill the timer using\n\n```\ntimer.upstream.connect().cancel()\n```\n\nAdding this to the progress view looks like \n\n```\nstruct View3: View {\n    @Binding var show: Bool\n    @State private var progress: Double = 0.0\n    let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()\n\n    var body: some View {\n        VStack(alignment: .leading) {\n            ProgressView(\"Processing...\", value: progress, total: 100.0)\n                .onReceive(timer) { _ in\n                    progress = lengthyProgress\n                    if progress \u003e= 100.0 {\n                        show = false\n                        timer.upstream.connect().cancel()\n                    }\n                }\n            Text(\"\\(Int(progress))/100\")\n        }\n        .padding()\n    }\n}\n```\n\nWhen the progress becomes 100%, the timer stops running and the progress view becomes static. \n\n### App Version 3: A Complete Solution \n\nIn review, here is the sequence of events:\n\n- display a button to launch the `lengthyOperation` function -- see RunViewDispatch\n- start progress display -- see View3\n- run the task -- see the model implementation of `lengthyOperation`\n- stop progress display -- updates stop when progress = 100%, see View 3\n\n## Alternate Asynchronous Implementation\n\nWrapping the `lengthyOperation` function in \n\n```\nDispatchQueue.global(qos: .background).async {\n    lengthyOperation()\n}\n```\n\nindicates that `lengthyOperation` is to be run asynchronously, which allows the execution of `lengthyOperation` and `ProgressView` to be run concurrently.\n\nAn alternate approach is to use the async/await concurrency code.\n\nThe async version of the model code looks like\n\n```\nfunc lengthyOperationAsync() async {\n    print (\"long running function: start\")\n    lengthyProgress = 0\n    while lengthyProgress \u003c 100 {\n        lengthyProgress += 10\n        await lengthyStepAsync()\n        print (\"long running function: progress = \\(lengthyProgress)\")\n    }\n    print (\"long running function: done\")\n}\n\nfunc lengthyStepAsync() async {\n    for i in 0..\u003c1000000 {\n        let _ = i * 2\n    }\n}\n```\n\nWithin the view code,\n\n```\nDispatchQueue.global(qos: .background).async {\n    lengthyOperation()\n}\n```\n\nis replaced by\n\n```\nTask { @MainActor in\n    await lengthyOperationAsync()\n}\n```\n\nThe `Task{@MainActor}` construct is included since the function is being called within a SwiftUI function that does not support concurrency.\n\nThe new view code looks like\n\n```\nstruct RunViewAsync: View {\n    @State var message: String\n    @State var version: String\n    @State private var isRunning = false\n\n    var body: some View {\n        VStack {\n            Text(message)\n            Button {\n                isRunning = true\n                Task { @MainActor in\n                    await lengthyOperationAsync()\n                }\n            } label: {\n                Text(\"Show version \\(version)\")\n            }\n        }\n\n        if isRunning {\n            View3(show: $isRunning)\n        }\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolcat124%2Fprogressview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsolcat124%2Fprogressview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsolcat124%2Fprogressview/lists"}