Skip to content

Commit 31c4d52

Browse files
authored
Merge pull request #1134 from kiwix/live-activities-v2
Live activities
2 parents f20a8ec + 48aa306 commit 31c4d52

File tree

10 files changed

+105
-64
lines changed

10 files changed

+105
-64
lines changed

App/App_iOS.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ struct Kiwix: App {
2525
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
2626

2727
private let fileMonitor: DirectoryMonitor
28-
// private let activityService: ActivityService?
28+
private let activityService: ActivityService?
2929

3030
init() {
3131
fileMonitor = DirectoryMonitor(url: URL.documentDirectory) { LibraryOperations.scanDirectory($0) }
3232
// MARK: - live activities
33-
// switch AppType.current {
34-
// case .kiwix:
35-
// activityService = ActivityService()
36-
// case .custom:
37-
// activityService = nil
38-
// }
33+
switch AppType.current {
34+
case .kiwix:
35+
activityService = ActivityService()
36+
case .custom:
37+
activityService = nil
38+
}
3939
UNUserNotificationCenter.current().delegate = appDelegate
4040
// MARK: - migrations
4141
if !ProcessInfo.processInfo.arguments.contains("testing") {
@@ -73,7 +73,14 @@ struct Kiwix: App {
7373
if url.isFileURL {
7474
NotificationCenter.openFiles([url], context: .file)
7575
} else if url.isZIMURL {
76-
NotificationCenter.openURL(url)
76+
switch url {
77+
case DownloadActivityAttributes.downloadsDeepLink:
78+
if FeatureFlags.hasLibrary {
79+
navigation.showDownloads.send()
80+
}
81+
default:
82+
NotificationCenter.openURL(url)
83+
}
7784
}
7885
}
7986
.task {
@@ -85,7 +92,7 @@ struct Kiwix: App {
8592
LibraryOperations.scanDirectory(URL.documentDirectory)
8693
LibraryOperations.applyFileBackupSetting()
8794
DownloadService.shared.restartHeartbeatIfNeeded()
88-
// activityService?.start()
95+
activityService?.start()
8996
case let .custom(zimFileURL):
9097
await LibraryOperations.open(url: zimFileURL)
9198
ZimMigration.forCustomApps()

App/CompactViewController.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,15 @@ private struct CompactView: View {
137137
@EnvironmentObject private var library: LibraryViewModel
138138
@State private var presentedSheet: PresentedSheet?
139139

140-
private enum PresentedSheet: String, Identifiable {
141-
case library
140+
private enum PresentedSheet: Identifiable {
141+
case library(downloads: Bool)
142142
case settings
143143
var id: String {
144-
rawValue
144+
switch self {
145+
case .library(true): return "library-downloads"
146+
case .library(false): return "library"
147+
case .settings: return "settings"
148+
}
145149
}
146150
}
147151

@@ -161,7 +165,7 @@ private struct CompactView: View {
161165
}
162166
Content(tabID: tabID, showLibrary: {
163167
if presentedSheet == nil {
164-
presentedSheet = .library
168+
presentedSheet = .library(downloads: false)
165169
} else {
166170
// there's a sheet already presented by the user
167171
// do nothing
@@ -183,7 +187,7 @@ private struct CompactView: View {
183187
Spacer()
184188
if FeatureFlags.hasLibrary {
185189
Button {
186-
presentedSheet = .library
190+
presentedSheet = .library(downloads: false)
187191
} label: {
188192
Label(LocalString.common_tab_menu_library, systemImage: "folder")
189193
}
@@ -200,8 +204,10 @@ private struct CompactView: View {
200204
.environmentObject(browser)
201205
.sheet(item: $presentedSheet) { presentedSheet in
202206
switch presentedSheet {
203-
case .library:
207+
case .library(downloads: false):
204208
Library(dismiss: dismiss)
209+
case .library(downloads: true):
210+
Library(dismiss: dismiss, tabItem: .downloads)
205211
case .settings:
206212
NavigationStack {
207213
Settings().toolbar {
@@ -216,6 +222,16 @@ private struct CompactView: View {
216222
}
217223
}
218224
}
225+
.onReceive(navigation.showDownloads) { _ in
226+
switch presentedSheet {
227+
case .library:
228+
// switching to the downloads tab
229+
// is done within Library
230+
break
231+
case .settings, nil:
232+
presentedSheet = .library(downloads: true)
233+
}
234+
}
219235
}
220236
}
221237
}

App/SplitViewController.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import UIKit
2121
final class SplitViewController: UISplitViewController {
2222
let navigationViewModel: NavigationViewModel
2323
private var navigationItemObserver: AnyCancellable?
24+
private var showDownloadsObserver: AnyCancellable?
2425
private var openURLObserver: NSObjectProtocol?
2526
private var hasZimFiles: Bool
2627

@@ -74,6 +75,17 @@ final class SplitViewController: UISplitViewController {
7475
self?.preferredDisplayMode = .automatic
7576
}
7677
}
78+
showDownloadsObserver = navigationViewModel
79+
.showDownloads
80+
.receive(on: DispatchQueue.main)
81+
.sink(receiveValue: { [weak self] _ in
82+
if self?.traitCollection.horizontalSizeClass == .regular,
83+
self?.navigationViewModel.currentItem != .downloads {
84+
self?.navigationViewModel.currentItem = .downloads
85+
}
86+
// the compact one is triggered in CompactViewController
87+
})
88+
7789
openURLObserver = NotificationCenter.default.addObserver(
7890
forName: .openURL, object: nil, queue: nil
7991
) { [weak self] notification in

Common/DownloadActivityAttributes.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import ActivityKit
1818

1919
public struct DownloadActivityAttributes: ActivityAttributes {
2020

21+
static let downloadsDeepLink = URL(string: "zim://downloads")
22+
2123
private static func progressFor(items: [DownloadItem]) -> Progress {
2224
let sumOfTotal = items.reduce(0) { result, item in
2325
result + item.total

Model/Utilities/DownloadTime.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ final class DownloadTime {
4343
}
4444
let average = averagePerSecond()
4545
let remainingAmount = totalAmount - latestAmount
46-
let remaingTime = Double(remainingAmount) / average - (now - latestTime)
47-
guard remaingTime > 0 else {
46+
let remainingTime = Double(remainingAmount) / average - (now - latestTime)
47+
guard remainingTime > 0 else {
4848
return 0
4949
}
50-
return remaingTime
50+
return remainingTime
5151
}
5252

5353
private func filterOutSamples(now: CFTimeInterval) {

ViewModel/NavigationViewModel.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515

1616
import CoreData
1717
import WebKit
18+
import Combine
1819

1920
@MainActor
2021
final class NavigationViewModel: ObservableObject {
2122
let uuid = UUID()
2223
// remained optional due to focusedSceneValue conformance
2324
@Published var currentItem: NavigationItem? = .loading
25+
private(set) var showDownloads = PassthroughSubject<Void, Never>()
26+
2427
#if os(macOS)
2528
var isTerminating: Bool = false
2629

Views/Library/Library.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@ import Defaults
2121
/// Tabbed library view on iOS & iPadOS
2222
struct Library: View {
2323
@EnvironmentObject private var viewModel: LibraryViewModel
24-
@SceneStorage("LibraryTabItem") private var tabItem: LibraryTabItem = .categories
24+
@EnvironmentObject private var navigation: NavigationViewModel
25+
@State private var tabItem: LibraryTabItem
2526
@Default(.hasSeenCategories) private var hasSeenCategories
2627
private let categories: [Category]
2728
let dismiss: (() -> Void)?
2829

2930
init(
3031
dismiss: (() -> Void)?,
32+
tabItem: LibraryTabItem = .categories,
3133
categories: [Category] = CategoriesToLanguages().allCategories()
3234
) {
3335
self.dismiss = dismiss
36+
self.tabItem = tabItem
3437
self.categories = categories
3538
}
3639

@@ -70,6 +73,10 @@ struct Library: View {
7073
viewModel.start(isUserInitiated: false)
7174
}.onDisappear {
7275
hasSeenCategories = true
76+
}.onReceive(navigation.showDownloads) { _ in
77+
if tabItem != .downloads {
78+
tabItem = .downloads
79+
}
7380
}
7481
}
7582
}

Views/LiveActivity/ActivityService.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ final class ActivityService {
3535
publisher: @MainActor @escaping () -> CurrentValueSubject<[UUID: DownloadState], Never> = {
3636
DownloadService.shared.progress.publisher
3737
},
38-
updateFrequency: Double = 10,
38+
updateFrequency: Double = 2,
3939
averageDownloadSpeedFromLastSeconds: Double = 30
4040
) {
4141
assert(updateFrequency > 0)

Widgets/DownloadsLiveActivity.swift

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,42 +21,30 @@ struct DownloadsLiveActivity: Widget {
2121
// @Environment(\.isActivityFullscreen) var isActivityFullScreen has a bug, when min iOS is 16
2222
// https://developer.apple.com/forums/thread/763594
2323

24-
/// A start time from the creation of the activity,
25-
/// this way the progress bar is not jumping back to 0
26-
private let startTime: Date = .now
27-
2824
var body: some WidgetConfiguration {
2925
ActivityConfiguration(for: DownloadActivityAttributes.self) { context in
3026
// Lock screen/banner UI
31-
let timeInterval = startTime...Date(
32-
timeInterval: context.state.estimatedTimeLeft,
33-
since: .now
34-
)
3527
VStack {
3628
HStack {
3729
VStack(alignment: .leading) {
3830
titleFor(context.state.title)
39-
progressFor(state: context.state, timeInterval: timeInterval)
31+
progressFor(state: context.state)
4032
}
4133
.padding()
4234
KiwixLogo(maxHeight: 50)
4335
.padding(.trailing)
4436
}
4537
}
4638
.modifier(WidgetBackgroundModifier())
39+
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
4740

4841
} dynamicIsland: { context in
4942
DynamicIsland {
5043
// Expanded UI
5144
DynamicIslandExpandedRegion(.leading) {
52-
let timeInterval = startTime...Date(
53-
timeInterval: context.state.estimatedTimeLeft,
54-
since: .now
55-
)
56-
5745
VStack(alignment: .leading) {
5846
titleFor(context.state.title)
59-
progressFor(state: context.state, timeInterval: timeInterval)
47+
progressFor(state: context.state)
6048
Spacer()
6149
}
6250
.padding()
@@ -79,7 +67,7 @@ struct DownloadsLiveActivity: Widget {
7967
.progressViewStyle(CircularProgressGaugeStyle(lineWidth: 5.7))
8068
.frame(width: 24, height: 24)
8169
}
82-
.widgetURL(URL(string: "https://www.kiwix.org"))
70+
.widgetURL(DownloadActivityAttributes.downloadsDeepLink)
8371
.keylineTint(Color.red)
8472
}.containerBackgroundRemovable()
8573
}
@@ -101,11 +89,25 @@ struct DownloadsLiveActivity: Widget {
10189
.tint(.secondary)
10290
}
10391

92+
private func currentTimeInterval(
93+
state: DownloadActivityAttributes.ContentState
94+
) -> ClosedRange<Date> {
95+
if state.progress < 1 {
96+
let timePassed: TimeInterval = state.progress / (1 - state.progress) * state.estimatedTimeLeft
97+
return Date(timeInterval: 0 - timePassed, since: .now)...Date(
98+
timeInterval: state.estimatedTimeLeft,
99+
since: .now
100+
)
101+
} else {
102+
return Date(timeIntervalSinceNow: 0)...Date(timeIntervalSinceNow: 0)
103+
}
104+
}
105+
104106
@ViewBuilder
105107
private func progressFor(
106-
state: DownloadActivityAttributes.ContentState,
107-
timeInterval: ClosedRange<Date>
108+
state: DownloadActivityAttributes.ContentState
108109
) -> some View {
110+
let timeInterval = currentTimeInterval(state: state)
109111
if !state.isAllPaused {
110112
ProgressView(timerInterval: timeInterval, countsDown: false, label: {
111113
progressText(state.progressDescription)
@@ -146,14 +148,6 @@ extension DownloadActivityAttributes.ContentState {
146148
total: 256,
147149
timeRemaining: 15,
148150
isPaused: true
149-
),
150-
DownloadActivityAttributes.DownloadItem(
151-
uuid: UUID(),
152-
description: "2nd item",
153-
downloaded: 90,
154-
total: 124,
155-
timeRemaining: 2,
156-
isPaused: true
157151
)
158152
]
159153
)
@@ -176,7 +170,7 @@ extension DownloadActivityAttributes.ContentState {
176170
description: "2nd item",
177171
downloaded: 110,
178172
total: 124,
179-
timeRemaining: 2,
173+
timeRemaining: 20,
180174
isPaused: false
181175
)
182176
]

project.yml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ targets:
121121
- path: Kiwix/SplashScreenKiwix.storyboard
122122
destinationFilters:
123123
- iOS
124-
# dependencies:
125-
# - target: Widgets
126-
# destinationFilters:
127-
# - iOS
124+
dependencies:
125+
- target: Widgets
126+
destinationFilters:
127+
- iOS
128128
UnitTests:
129129
type: bundle.unit-test
130130
supportedDestinations: [iOS, macOS]
@@ -143,19 +143,19 @@ targets:
143143
- path: Tests
144144
dependencies:
145145
- target: Kiwix
146-
# Widgets:
147-
# type: app-extension
148-
# supportedDestinations: [iOS]
149-
# settings:
150-
# base:
151-
# PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.ioswidgets
152-
# INFOPLIST_FILE: Widgets/Info.plist
153-
# sources:
154-
# - path: Common
155-
# - path: Widgets
156-
# dependencies:
157-
# - framework: SwiftUI.framework
158-
# - framework: WidgetKit.framework
146+
Widgets:
147+
type: app-extension
148+
supportedDestinations: [iOS]
149+
settings:
150+
base:
151+
PRODUCT_BUNDLE_IDENTIFIER: self.Kiwix.ioswidgets
152+
INFOPLIST_FILE: Widgets/Info.plist
153+
sources:
154+
- path: Common
155+
- path: Widgets
156+
dependencies:
157+
- framework: SwiftUI.framework
158+
- framework: WidgetKit.framework
159159

160160
schemes:
161161
Kiwix:

0 commit comments

Comments
 (0)