Skip to content

[UIKitNavigation] Presenting UIActivityViewController in async task has weird behaviours #263

Description

@JT501

Description

I am developing a feature to download some files and share with others.
I have to show an alert view to notice user the download progress.
When the download complete, it will automatically dismiss the alert and show the share activity view.

However, when I change the destination state after the download completed,
it will dismiss the view controller instead of presenting the share activity view.

The more weird thing is that it seems only happened when presenting UIActivityController,
I got not problems when presenting normal view controller or alert view controller.

I found out a workaround by adding a short buffer time between the destination state change.
I have made a simple implementation below that showcase the weird behavior.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

Dismiss the alert and then present the UIActivityController.

Actual behavior

The alert is dismissed and the parent view controller is also dismissed.

Steps to reproduce

import UIKit
import UIKitNavigation

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        viewSetup()
    }

    func viewSetup() {
        let button = UIButton(type: .system)
        button.setTitle("Present ViewController", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(button)

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        button.addAction(
            .init(
                handler: { [weak self] _ in
                    guard let self else { return }
                    present(PresentedViewController(), animated: true)
                }
            ),
            for: .touchUpInside
        )
    }
}

@Observable
class PresentedViewModel {
    @CasePathable
    enum Destination {
        case alert
        case shareActivity
    }

    var destination: Destination?

    func onButtonTap() {
        destination = .alert

        // this will dismiss the presented view controller
        Task {
            // Doing some async works, like downloading files
            try? await Task.sleep(for: .seconds(2))

            // Workaround, if adding short buffer time to change state, it will be fine
            // try? await Task.sleep(for: .milliseconds(500))
            // destination = nil
            // try? await Task.sleep(for: .milliseconds(500))
            
            destination = .shareActivity
        }
    }

    func onAlertButtonTap() {
        // No problems here
        destination = .shareActivity
    }
}

class PresentedViewController: UIViewController {
    @UIBindable var viewModel = PresentedViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewSetup()
        navigation()
    }

    func viewSetup() {
        view.backgroundColor = .systemBackground

        let button = UIButton(type: .system)
        button.setTitle("Show Alert Now", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false

        view.addSubview(button)

        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])

        button.addAction(
            .init(
                handler: { [weak self] _ in
                    guard let self else { return }
                    viewModel.onButtonTap()
                }
            ),
            for: .touchUpInside
        )
    }

    func navigation() {
        present(isPresented: UIBinding($viewModel.destination.alert)) {
            let alertVC = UIAlertController(
                title: "Alert",
                message: "Wait to show Share Activity",
                preferredStyle: .alert
            )
            alertVC.addAction(
                .init(
                    title: "Show",
                    style: .destructive,
                    handler: { [weak self] _ in
                        guard let self else { return }
                        viewModel.onAlertButtonTap()
                    }
                )
            )
            return alertVC
        }

        present(isPresented: UIBinding($viewModel.destination.shareActivity)) {
            let shareVC = UIActivityViewController(
                activityItems: ["Texts to share"],
                applicationActivities: nil
            )
            return shareVC
            // Weird, no problems when presenting a normal view controller
            // return PresentedViewController()
        }
    }
}

SwiftUI Navigation version information

2.2.3

Destination operating system

iOS 18

Xcode version information

16.2

Swift Compiler version information

swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions