Files
iOS/Sources/App/Frontend/DownloadManager/DownloadManagerView.swift
Copilot 5d88cf34cc Fix: DownloadManagerView traps window on Mac Catalyst with no dismiss path (#4379)
On Mac Catalyst, downloading the emergency kit backup presented
`DownloadManagerView` with no way to dismiss it — no swipe gesture, no
close button — requiring a force-quit to recover the window.

Mac Catalyst has no sheet swipe-to-dismiss, so the view needs an
explicit close affordance via a navigation bar.

## Changes

- **`DownloadManagerView`** — conditionally adds a `CloseButton` as
leading toolbar item when `Current.isCatalyst`, using the existing
`.modify` pattern
- **`WebViewController+WebKitDelegates`** — wraps the
`UIHostingController` in a `UINavigationController` on Mac Catalyst so
the toolbar is rendered

```swift
// WebViewController+WebKitDelegates.swift
if Current.isCatalyst {
    let navigationController = UINavigationController(rootViewController: downloadController)
    presentOverlayController(controller: navigationController, animated: true)
} else {
    presentOverlayController(controller: downloadController, animated: true)
}
```

```swift
// DownloadManagerView.swift
.modify { view in
    if Current.isCatalyst {
        view.toolbar {
            ToolbarItem(placement: .topBarLeading) {
                CloseButton { dismiss() }
            }
        }
    } else {
        view
    }
}
```

Pattern mirrors the existing `WidgetSelectionView` Mac Catalyst handling
in `EntityAddToHandler`.

## Screenshots

Before: "Download finished" overlay with no dismiss control, app window
unresponsive until force-quit.

After: Navigation bar with close button allows dismissal normally.

## Link to pull request in Documentation repository

Documentation: home-assistant/companion.home-assistant#

## Any other notes

No documentation change needed — no user-visible feature added, only a
broken interaction fixed.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Lost window control of HA app after downloading emergency
kit backup</issue_title>
> <issue_description><!-- Please READ THIS FIRST
> If your issue relates to something not looking right on Home Assistant
within the Companion App, please check if the error is present in Safari
on macOS too. If the issue is also seen in Safari, please open an issue
on the frontend repo
(https://github.com/home-assistant/frontend/issues/new?labels=bug&template=BUG_REPORT.md)
instead -->
> 
> **Device model, version and app version**
> <!-- Please include your mac's model and version as listed in 'About
This Mac'. Please also give the app version listed in the Home
Assistant>About Home Assistant, please include the number in brackets.
Safari can be found by launching it in Safari > About Safari. -->
> 
> Model Name: M1 Pro MBP
> macOS Version: 15.7.4
> App Version: 2026.2.1
> Safari Version: n/a
> 
> **Home Assistant Core Version**
> <!-- Please give the version number of Home Assistant Core you are
running -->
> 2026.2.2
> 
> **Describe the bug**
> 
> Downloading the emergency kit through the macOS app results in the app
window staying continually on screen and being unable to click anywhere
else on screen to regain control of the app.
> 
> The app must be force closed via Activity Monitor to regain control of
the app.
> 
> **To Reproduce**
> 
> 1. Open macOS HA app.
> 2. Go to Settings, System, Backups, Settings and History, Download
emergency kit
> 
> **Expected behavior**
> 
> Window to close after the config is downloaded allowing the user to
control the app again.
> 
> **Screenshots**
> <!-- If applicable, add screenshots to help explain your problem. -->
> 
> <img width="692" height="716" alt="Image"
src="https://github.com/user-attachments/assets/0807b43a-5f6c-4651-bbdd-7ff0daf72e94"
/>
> 
> **Additional context**
> <!--Add any other context about the problem here.-->
> </issue_description>
> 
> <agent_instructions>When presenting the DownloadManagerView on Mac
Catalyst we can’t rely on presentation defender and gesture to dismiss,
we need a navigation view with a “CloseButton” as leading toolbar
item</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes home-assistant/iOS#4378

<!-- START COPILOT CODING AGENT TIPS -->
---

🔒 GitHub Advanced Security automatically protects Copilot coding agent
pull requests. You can protect all pull requests by enabling Advanced
Security for your repositories. [Learn more about Advanced
Security.](https://gh.io/cca-advanced-security)

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
2026-02-23 10:20:26 +00:00

133 lines
4.3 KiB
Swift

import Shared
import SwiftUI
@available(iOS 17.0, *)
struct DownloadManagerView: View {
@Environment(\.dismiss) private var dismiss
@StateObject var viewModel: DownloadManagerViewModel
@State private var shareWrapper: ShareWrapper?
init(viewModel: DownloadManagerViewModel) {
self._viewModel = .init(wrappedValue: viewModel)
}
var body: some View {
NavigationView {
VStack(spacing: .zero) {
content
Spacer()
}
.onDisappear {
viewModel.cancelDownload()
// For mac the file should remain in the download folder to keep expected behavior
if !Current.isCatalyst {
viewModel.deleteFile()
}
}
.onChange(of: viewModel.finished) { _, newValue in
if newValue, Current.isCatalyst {
URLOpener.shared.open(AppConstants.DownloadsDirectory, options: [:], completionHandler: nil)
}
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
CloseButton {
dismiss()
}
}
}
}
.navigationViewStyle(.stack)
}
@ViewBuilder
private var content: some View {
if viewModel.finished {
successView
} else if viewModel.failed {
fileCard
failedCard
} else {
HAProgressView(style: .large)
.padding(DesignSystem.Spaces.four)
Text(verbatim: L10n.DownloadManager.Downloading.title)
.font(.title.bold())
fileCard
Text(viewModel.progress)
.animation(.easeInOut(duration: 1), value: viewModel.progress)
}
}
private var successView: some View {
VStack(spacing: DesignSystem.Spaces.three) {
Image(systemSymbol: .checkmark)
.foregroundStyle(.green)
.font(.system(size: 100))
.symbolEffect(
.bounce,
options: .nonRepeating
)
Text(verbatim: L10n.DownloadManager.Finished.title)
.font(.title.bold())
if let url = viewModel.lastURLCreated {
if Current.isCatalyst {
Button {
URLOpener.shared.open(AppConstants.DownloadsDirectory, options: [:], completionHandler: nil)
} label: {
Label(viewModel.fileName, systemSymbol: .folder)
}
.buttonStyle(.primaryButton)
} else {
ShareLink(viewModel.fileName, item: url)
.lineLimit(1)
.truncationMode(.middle)
.padding()
.foregroundStyle(.white)
.background(Color.haPrimary)
.clipShape(RoundedRectangle(cornerRadius: DesignSystem.CornerRadius.oneAndHalf))
.padding()
.onAppear {
shareWrapper = .init(url: url)
}
.sheet(item: $shareWrapper, onDismiss: {}, content: { data in
ActivityViewController(shareWrapper: data)
})
}
}
}
.padding()
}
private var fileCard: some View {
HStack {
Image(systemSymbol: .docZipper)
Text(viewModel.fileName)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(.gray.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: DesignSystem.CornerRadius.oneAndHalf))
.padding()
}
private var failedCard: some View {
Text(viewModel.errorMessage)
.frame(maxWidth: .infinity, alignment: .leading)
.multilineTextAlignment(.leading)
.padding()
.background(.red.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: DesignSystem.CornerRadius.oneAndHalf))
.padding()
}
}
#Preview {
if #available(iOS 17.0, *) {
DownloadManagerView(viewModel: .init())
} else {
Text("Hey there")
}
}