Skip to content

Commit

Permalink
fix offline mode. (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
Livinglist authored Sep 18, 2023
1 parent 6452b57 commit 8c5af24
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 63 deletions.
63 changes: 63 additions & 0 deletions Shared/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import BackgroundTasks
import Foundation
import SwiftUI
import HackerNewsKit
import UserNotifications

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: Constants.Download.backgroundTaskId,
using: nil) { task in
task.expirationHandler = {
task.setTaskCompleted(success: false)
}

Task {
await OfflineRepository.shared.downloadAllStories()

task.setTaskCompleted(success: true)
}

OfflineRepository.shared.scheduleBackgroundDownload()
}
}

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Register SceneDelegate.
let sceneConfig = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
sceneConfig.delegateClass = SceneDelegate.self
return sceneConfig
}

private func configureUserNotifications() {
UNUserNotificationCenter.current().delegate = self
}
}

class SceneDelegate: NSObject, UIWindowSceneDelegate {
func sceneDidEnterBackground(_ scene: UIScene) {
OfflineRepository.shared.scheduleBackgroundDownload()
}
}

extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound, .list])
}

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
Task {
let content = response.notification.request.content
if let id = Int(content.targetContentIdentifier ?? ""),
id != 0,
let item = await StoriesRepository.shared.fetchComment(id) {
Router.shared.to(item)
}
}
}
}
5 changes: 3 additions & 2 deletions Shared/Models/Stores/ItemStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ extension ItemView {
private var cancellable: AnyCancellable?

init() {
cancellable = NetworkMonitor.shared.networkStatus.sink { isConnected in
self.isConnectedToNetwork = isConnected
cancellable = NetworkMonitor.shared.networkStatus
.sink { isConnected in
self.isConnectedToNetwork = isConnected ?? false
}
}

Expand Down
7 changes: 4 additions & 3 deletions Shared/Models/Stores/StoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ extension HomeView {
private var cancellable: AnyCancellable?

init() {
cancellable = NetworkMonitor.shared.networkStatus.sink { isConnected in
self.isConnectedToNetwork = isConnected
}
cancellable = NetworkMonitor.shared.networkStatus
.sink { isConnected in
self.isConnectedToNetwork = isConnected ?? false
}
}

func fetchStories(status: Status = .inProgress) async {
Expand Down
69 changes: 45 additions & 24 deletions Shared/Services/OfflineRepository.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Alamofire
import BackgroundTasks
import Combine
import Foundation
import SwiftUI
import SwiftData
Expand All @@ -12,15 +14,35 @@ public class OfflineRepository: ObservableObject {
@Published var isDownloading = false
@Published var completionCount = 0

lazy var lastFetchedAt = {
guard let date = UserDefaults.standard.object(forKey: lastDownloadAtKey) as? Date else { return "" }
let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy HH:mm"
return df.string(from: date)
}()
var isInMemory = false

private let storiesRepository = StoriesRepository.shared
private let container = try! ModelContainer(for: StoryCollection.self, CommentCollection.self)
private let downloadOrder = [StoryType.top, .ask, .best]
private let lastDownloadAtKey = "lastDownloadedAt"
private var stories = [StoryType: [Story]]()
private var comments = [Int: [Comment]]()
private var cancellable: AnyCancellable?

public static let shared: OfflineRepository = .init()

private init() {
init() {
cancellable = NetworkMonitor.shared.networkStatus
.dropFirst()
.sink { isConnected in
if let isConnected = isConnected, !self.isInMemory && !isConnected {
self.loadIntoMemory()
}
}
}

public func loadIntoMemory() {
let context = container.mainContext

// Fetch all cached stories.
Expand All @@ -39,13 +61,30 @@ public class OfflineRepository: ObservableObject {
comments[collection.parentId] = collection.comments
}
}

isInMemory = true
}

public func scheduleBackgroundDownload() {
let downloadTask = BGProcessingTaskRequest(identifier: Constants.Download.backgroundTaskId)
// Set earliestBeginDate to be 1 hr from now.
downloadTask.earliestBeginDate = Date(timeIntervalSinceNow: 3600)
downloadTask.requiresNetworkConnectivity = true
downloadTask.requiresExternalPower = true
do {
try BGTaskScheduler.shared.submit(downloadTask)
} catch {
debugPrint("Unable to submit task: \(error.localizedDescription)")
}
}

// MARK: - Story related.

public func downloadAllStories() async -> Void {
isDownloading = true

UserDefaults.standard.set(Date.now, forKey: lastDownloadAtKey)

let context = container.mainContext
var completedStoryId = Set<Int>()

Expand Down Expand Up @@ -93,29 +132,11 @@ public class OfflineRepository: ObservableObject {
}

public func fetchAllStories(from storyType: StoryType) -> [Story] {
return stories[storyType] ?? [Story]()
}

public func fetchStoryIds(from storyType: StoryType) async -> [Int] {
return [Int]()
}

public func fetchStoryIds(from storyType: String) async -> [Int] {
return [Int]()
}

public func fetchStory(_ id: Int) async -> Story? {
return nil
// let context = container.mainContext
// var descriptor = FetchDescriptor<StoryCollection>(
// predicate: #Predicate { $0.id == id }
// )
// descriptor.fetchLimit = 1
// if let results = try? context.fetch(descriptor) {
// return results.first?.story
// } else {
// return nil
// }
guard let stories = stories[storyType] else { return [Story]() }
let storiesWithCommentsDownloaded = stories.filter { story in
comments[story.id].isNotNullOrEmpty
}
return storiesWithCommentsDownloaded
}

// MARK: - Comment related.
Expand Down
4 changes: 4 additions & 0 deletions Shared/Utilities/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ struct Constants {
static let lastFetchedAtKey = "lastFetchedAt"
static let backgroundTaskId = "fetchReplies"
}

struct Download {
static let backgroundTaskId = "download"
}
}
2 changes: 1 addition & 1 deletion Shared/Utilities/NetworkMonitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Network
import Combine

final class NetworkMonitor {
let networkStatus = CurrentValueSubject<Bool, Never>(true)
let networkStatus = CurrentValueSubject<Bool?, Never>(nil)
var onWifi = true
var onCellular = true

Expand Down
3 changes: 1 addition & 2 deletions Shared/Views/Components/ItemRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ struct ItemRow: View {
Label("View on Hacker News", systemImage: "safari")
}
} label: {
Image(systemName: "ellipsis")
Label(String(), systemImage: "ellipsis")
.padding(.leading)
.padding(.bottom, 12)
.padding(.trailing)
.foregroundColor(.orange)
}
}
Expand Down
5 changes: 4 additions & 1 deletion Shared/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,12 @@ struct HomeView: View {
Text("\(offlineRepository.completionCount) completed")
} else {
Label("Download all stories", systemImage: "square.and.arrow.down")
if offlineRepository.lastFetchedAt.isNotEmpty {
Text("last downloaded at \(offlineRepository.lastFetchedAt)")
}
}
}
.disabled(offlineRepository.isDownloading)
.disabled(offlineRepository.isDownloading || !storyStore.isConnectedToNetwork)
Divider()
AuthButton(showLoginDialog: $showLoginDialog, showLogoutDialog: $showLogoutDialog)
Button {
Expand Down
31 changes: 3 additions & 28 deletions Shared/ZCombinatorApp.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import BackgroundTasks
import Foundation
import SwiftUI
import SwiftData
import HackerNewsKit
Expand All @@ -9,6 +11,7 @@ struct ZCombinatorApp: App {
@Environment(\.scenePhase) private var phase
let auth: Authentication = .shared
let notification: AppNotification = .shared
let offlineRepository: OfflineRepository = .shared

var body: some Scene {
WindowGroup {
Expand All @@ -29,31 +32,3 @@ struct ZCombinatorApp: App {
}
}
}

class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}

private func configureUserNotifications() {
UNUserNotificationCenter.current().delegate = self
}
}

extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .sound, .list])
}

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
Task {
let content = response.notification.request.content
if let id = Int(content.targetContentIdentifier ?? ""),
id != 0,
let item = await StoriesRepository.shared.fetchComment(id) {
Router.shared.to(item)
}
}
}
}
1 change: 1 addition & 0 deletions ZCombinator--iOS--Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>download</string>
<string>fetchReplies</string>
</array>
<key>CFBundleURLTypes</key>
Expand Down
8 changes: 6 additions & 2 deletions ZCombinator.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
E5A2CECF2A08B94200D683C6 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A2CECE2A08B94200D683C6 /* ShareViewController.swift */; };
E5A2CED22A08B94200D683C6 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E5A2CED02A08B94200D683C6 /* MainInterface.storyboard */; };
E5A2CED62A08B94200D683C6 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E5A2CECC2A08B94200D683C6 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
E5B5B91F2AB78748005CE503 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B5B91E2AB78748005CE503 /* AppDelegate.swift */; };
E5BF9E6B2A0CAA5C0030E588 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = E5BF9E6A2A0CAA5C0030E588 /* SwiftSoup */; };
E5C448DB2AB6B17A006C023B /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C448DA2AB6B17A006C023B /* NetworkMonitor.swift */; };
E5C448E02AB6CD64006C023B /* CommentCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C448DF2AB6CD64006C023B /* CommentCollection.swift */; };
Expand Down Expand Up @@ -233,6 +234,7 @@
E5A2CECE2A08B94200D683C6 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
E5A2CED12A08B94200D683C6 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
E5A2CED32A08B94200D683C6 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
E5B5B91E2AB78748005CE503 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
E5C448DA2AB6B17A006C023B /* NetworkMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitor.swift; sourceTree = "<group>"; };
E5C448DF2AB6CD64006C023B /* CommentCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCollection.swift; sourceTree = "<group>"; };
E5C7B9792A086C9A00CA6066 /* CommentTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentTile.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -369,6 +371,7 @@
E50FB9CA2886738B00AA325C /* ZCombinatorApp.swift */,
E50FB9CD2886738D00AA325C /* Assets.xcassets */,
E50FB9C82886738B00AA325C /* ZCombinator.xcdatamodeld */,
E5B5B91E2AB78748005CE503 /* AppDelegate.swift */,
);
path = Shared;
sourceTree = "<group>";
Expand Down Expand Up @@ -843,6 +846,7 @@
E5C7B97E2A0887D600CA6066 /* ProfileView.swift in Sources */,
E5687FAD289E194A00901F1F /* TextView.swift in Sources */,
E57640322A0302A10075F5D1 /* FlagButton.swift in Sources */,
E5B5B91F2AB78748005CE503 /* AppDelegate.swift in Sources */,
E54B013A29FC9FB500F7F0A0 /* View+ShareSheet.swift in Sources */,
E50FB9F32886738D00AA325C /* ZCombinator.xcdatamodeld in Sources */,
E5C7B97C2A08706000CA6066 /* View+GetColor.swift in Sources */,
Expand Down Expand Up @@ -1081,7 +1085,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "ZCombinator (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down Expand Up @@ -1119,7 +1123,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "ZCombinator (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 11;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_TEAM = QMWX3X2NF7;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand Down

0 comments on commit 8c5af24

Please sign in to comment.