Skip to content

Commit

Permalink
Add download manager for WKWebView (#3157)
Browse files Browse the repository at this point in the history
  • Loading branch information
bgoncal authored Nov 13, 2024
1 parent b0988f0 commit 9892e4b
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 1 deletion.
16 changes: 16 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,8 @@
42DF6B2D2CCF8A2200D7EC14 /* PermissionRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */; };
42DF6B2F2CCF918D00D7EC14 /* BluetoothPermissionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DF6B2E2CCF918D00D7EC14 /* BluetoothPermissionView.swift */; };
42E65F082C8079FE00C4A6F2 /* ControlAssistValueProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E65F072C8079FE00C4A6F2 /* ControlAssistValueProvider.swift */; };
42E6C08A2CE4F4FA007CA622 /* DownloadManagerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E6C0892CE4F4FA007CA622 /* DownloadManagerView.swift */; };
42E6C08C2CE4F7A8007CA622 /* DownloadManagerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E6C08B2CE4F7A8007CA622 /* DownloadManagerViewModel.swift */; };
42E95C552CA44FC90010ECE3 /* SafariWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C542CA44FC90010ECE3 /* SafariWebView.swift */; };
42E95C572CA45EFA0010ECE3 /* OnboardingErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */; };
42E95C592CA46AD50010ECE3 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E95C582CA46AD50010ECE3 /* ActivityView.swift */; };
Expand Down Expand Up @@ -2052,6 +2054,8 @@
42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionRequestView.swift; sourceTree = "<group>"; };
42DF6B2E2CCF918D00D7EC14 /* BluetoothPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothPermissionView.swift; sourceTree = "<group>"; };
42E65F072C8079FE00C4A6F2 /* ControlAssistValueProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlAssistValueProvider.swift; sourceTree = "<group>"; };
42E6C0892CE4F4FA007CA622 /* DownloadManagerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManagerView.swift; sourceTree = "<group>"; };
42E6C08B2CE4F7A8007CA622 /* DownloadManagerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadManagerViewModel.swift; sourceTree = "<group>"; };
42E95C542CA44FC90010ECE3 /* SafariWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebView.swift; sourceTree = "<group>"; };
42E95C562CA45EFA0010ECE3 /* OnboardingErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingErrorView.swift; sourceTree = "<group>"; };
42E95C582CA46AD50010ECE3 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3068,6 +3072,7 @@
11AD2E2A2528FDB700FBC437 /* WebView */ = {
isa = PBXGroup;
children = (
42E6C0882CE4F4EC007CA622 /* DownloadManager */,
42B942F42CAA1E4400E36E02 /* Payload */,
42BE698B2C4691E000745ECA /* Views */,
11DE822F24FAE66F00E636B8 /* UIWindow+Additions.swift */,
Expand Down Expand Up @@ -4141,6 +4146,15 @@
path = Add;
sourceTree = "<group>";
};
42E6C0882CE4F4EC007CA622 /* DownloadManager */ = {
isa = PBXGroup;
children = (
42E6C0892CE4F4FA007CA622 /* DownloadManagerView.swift */,
42E6C08B2CE4F7A8007CA622 /* DownloadManagerViewModel.swift */,
);
path = DownloadManager;
sourceTree = "<group>";
};
42EFFAEA2C8882CC002F10FC /* CarPlay */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -6728,6 +6742,7 @@
425573CC2B5574AD00145217 /* CarPlayAreasZonesTemplate+Build.swift in Sources */,
42A47A8C2C4547B800C9B43D /* WebViewExternalMessageHandler+Build.swift in Sources */,
B626AAF11D8F972800A0D225 /* SettingsDetailViewController.swift in Sources */,
42E6C08C2CE4F7A8007CA622 /* DownloadManagerViewModel.swift in Sources */,
1127381C2622B6F300F5E312 /* DebugSettingsViewController.swift in Sources */,
42266B252B7A4BA900E94A71 /* BarcodeScannerViewModel.swift in Sources */,
11DE823024FAE66F00E636B8 /* UIWindow+Additions.swift in Sources */,
Expand Down Expand Up @@ -6846,6 +6861,7 @@
B661FC88226D478300E541DD /* OnboardingScanningViewController.swift in Sources */,
11A48D8124CA8ADB0021BDD9 /* NotificationCategory+Observation.swift in Sources */,
1100D51D2496AECE00B1073C /* PermissionStatusRow.swift in Sources */,
42E6C08A2CE4F4FA007CA622 /* DownloadManagerView.swift in Sources */,
D0B25BD221323CA600678C2C /* ClientEventPayloadViewController.swift in Sources */,
42E95C572CA45EFA0010ECE3 /* OnboardingErrorView.swift in Sources */,
B641BC1F1E2097EF002CCBC1 /* AboutViewController.swift in Sources */,
Expand Down
5 changes: 4 additions & 1 deletion Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1040,4 +1040,7 @@ Home Assistant is free and open source home automation software with a focus on
"widgets.sensors.description" = "Display state of sensors";
"widgets.sensors.not_configured" = "No Sensors Configured";
"widgets.sensors.title" = "Sensors";
"yes_label" = "Yes";
"yes_label" = "Yes";
"download_manager.downloading.title" = "Downloading";
"download_manager.finished.title" = "Download finished";
"download_manager.failed.title" = "Failed to download file, error: %@";
100 changes: 100 additions & 0 deletions Sources/App/WebView/DownloadManager/DownloadManagerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Shared
import SwiftUI

@available(iOS 17.0, *)
struct DownloadManagerView: View {
@Environment(\.dismiss) private var dismiss

@StateObject var viewModel: DownloadManagerViewModel

init(viewModel: DownloadManagerViewModel) {
self._viewModel = .init(wrappedValue: viewModel)
}

var body: some View {
VStack(spacing: .zero) {
HStack {
Button(action: {
viewModel.deleteFile()
dismiss()
}, label: {
Image(systemSymbol: .xmarkCircleFill)
.font(.title)
.foregroundStyle(
Color(uiColor: .systemBackground),
.gray.opacity(0.5)
)
})
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
}
if viewModel.finished {
successView
} else if viewModel.failed {
fileCard
failedCard
} else {
ProgressView()
.progressViewStyle(.circular)
.scaleEffect(2)
.padding(Spaces.four)
Text(L10n.DownloadManager.Downloading.title)
.font(.title.bold())
fileCard
}
Spacer()
}
}

private var successView: some View {
VStack(spacing: Spaces.three) {
Image(systemSymbol: .checkmark)
.foregroundStyle(.green)
.font(.system(size: 100))
.symbolEffect(
.bounce,
options: .nonRepeating
)
Text(L10n.DownloadManager.Finished.title)
.font(.title.bold())
if let url = viewModel.lastURLCreated {
ShareLink(viewModel.fileName, item: url)
.padding()
.foregroundStyle(.white)
.background(Color.asset(Asset.Colors.haPrimary))
.clipShape(RoundedRectangle(cornerRadius: 12))
.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: 12))
.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: 12))
.padding()
}
}

#Preview {
if #available(iOS 17.0, *) {
DownloadManagerView(viewModel: .init())
} else {
Text("Hey there")
}
}
57 changes: 57 additions & 0 deletions Sources/App/WebView/DownloadManager/DownloadManagerViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation
import Shared
import WebKit

@MainActor
final class DownloadManagerViewModel: NSObject, ObservableObject {
@Published var fileName: String = ""
@Published var finished: Bool = false
@Published var failed: Bool = false
@Published var errorMessage: String = ""
@Published var lastURLCreated: URL?

func deleteFile() {
if let url = lastURLCreated {
// Guarantee to delete file before leaving screen
do {
try FileManager.default.removeItem(at: url)
} catch {
Current.Log.error("Failed to remove file before leaving download manager at \(url), error: \(error)")
}
}
}
}

extension DownloadManagerViewModel: WKDownloadDelegate {
func download(
_ download: WKDownload,
decideDestinationUsing response: URLResponse,
suggestedFilename: String
) async -> URL? {
let urls = FileManager.default.urls(for: .cachesDirectory, in: .allDomainsMask)
let name = suggestedFilename.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "Unknown"
fileName = name
if let url = URL(string: name, relativeTo: urls[0]) {
lastURLCreated = url
// Guarantee file does not exist, otherwise download will fail
do {
try FileManager.default.removeItem(at: url)
} catch {
Current.Log.error("Failed to remove file for download manager at \(url), error: \(error)")
}

return url
} else {
return nil
}
}

func downloadDidFinish(_ download: WKDownload) {
finished = true
}

func download(_ download: WKDownload, didFailWithError error: any Error, resumeData: Data?) {
errorMessage = L10n.DownloadManager.Failed.title(error.localizedDescription)
failed = true
}
}
10 changes: 10 additions & 0 deletions Sources/App/WebView/WebViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,16 @@ final class WebViewController: UIViewController, WKNavigationDelegate, WKUIDeleg
updateWebViewSettings(reason: .load)
}

func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
if #available(iOS 17.0, *) {
let viewModel = DownloadManagerViewModel()
let downloadManager = DownloadManagerView(viewModel: viewModel)
let downloadController = UIHostingController(rootView: downloadManager)
presentOverlayController(controller: downloadController, animated: true)
download.delegate = viewModel
}
}

func webView(
_ webView: WKWebView,
decidePolicyFor navigationResponse: WKNavigationResponse,
Expand Down
17 changes: 17 additions & 0 deletions Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,23 @@ public enum L10n {
}
}

public enum DownloadManager {
public enum Downloading {
/// Downloading
public static var title: String { return L10n.tr("Localizable", "download_manager.downloading.title") }
}
public enum Failed {
/// Failed to download file, error: %@
public static func title(_ p1: Any) -> String {
return L10n.tr("Localizable", "download_manager.failed.title", String(describing: p1))
}
}
public enum Finished {
/// Download finished
public static var title: String { return L10n.tr("Localizable", "download_manager.finished.title") }
}
}

public enum Extensions {
public enum Map {
public enum Location {
Expand Down

0 comments on commit 9892e4b

Please sign in to comment.