Digital PCT265 story MVAPCT-48 - Initial demo of loading feed from cache

This commit is contained in:
Scott Pfeil 2024-03-20 19:09:13 -04:00
parent c636cc1ca4
commit 8c32dbbd7d
8 changed files with 242 additions and 57 deletions

View File

@ -96,6 +96,7 @@
AF43A7411FC5FA6F008E9347 /* MVMCoreViewProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = AF43A7401FC5FA6F008E9347 /* MVMCoreViewProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
AF43A74C1FC6109F008E9347 /* MVMCoreSessionObject.h in Headers */ = {isa = PBXBuildFile; fileRef = AF43A74A1FC6109F008E9347 /* MVMCoreSessionObject.h */; settings = {ATTRIBUTES = (Public, ); }; };
AF43A74D1FC6109F008E9347 /* MVMCoreSessionObject.m in Sources */ = {isa = PBXBuildFile; fileRef = AF43A74B1FC6109F008E9347 /* MVMCoreSessionObject.m */; };
AF4955E22BAB1EB200567276 /* MVMCoreCache+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF4955E12BAB1EB200567276 /* MVMCoreCache+Extension.swift */; };
AF60A7F2289212CA00919EEB /* MVMError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF60A7F1289212CA00919EEB /* MVMError.swift */; };
AF60A7F4289212EB00919EEB /* MVMCoreError.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF60A7F3289212EB00919EEB /* MVMCoreError.swift */; };
AF686FDA2A8A876A008F666A /* NavigationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF686FD92A8A876A008F666A /* NavigationOperation.swift */; };
@ -252,6 +253,7 @@
AF43A7401FC5FA6F008E9347 /* MVMCoreViewProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreViewProtocol.h; sourceTree = "<group>"; };
AF43A74A1FC6109F008E9347 /* MVMCoreSessionObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreSessionObject.h; sourceTree = "<group>"; };
AF43A74B1FC6109F008E9347 /* MVMCoreSessionObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MVMCoreSessionObject.m; sourceTree = "<group>"; };
AF4955E12BAB1EB200567276 /* MVMCoreCache+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreCache+Extension.swift"; sourceTree = "<group>"; };
AF60A7F1289212CA00919EEB /* MVMError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMError.swift; sourceTree = "<group>"; };
AF60A7F3289212EB00919EEB /* MVMCoreError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreError.swift; sourceTree = "<group>"; };
AF686FD92A8A876A008F666A /* NavigationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationOperation.swift; sourceTree = "<group>"; };
@ -656,6 +658,7 @@
children = (
AF43A7091FC4F415008E9347 /* MVMCoreCache.h */,
AF43A7081FC4F415008E9347 /* MVMCoreCache.m */,
AF4955E12BAB1EB200567276 /* MVMCoreCache+Extension.swift */,
605A9A292ABD712F00487E47 /* MVMCoreLoggingHandler.swift */,
6042E8FB2B3094680031644B /* MVMCoreLoggingHandlerHelper.h */,
D288D5F426C6EFE000A5C365 /* MVMCoreLoggingHandler+Extension.swift */,
@ -902,6 +905,7 @@
AFBB96351FBA34310008D868 /* MVMCoreErrorConstants.m in Sources */,
AF43A5881FBB67D6008E9347 /* MVMCoreActionUtility.m in Sources */,
AFED77A61FCCA29400BAE689 /* MVMCoreViewControllerStoryBoardMappingObject.m in Sources */,
AF4955E22BAB1EB200567276 /* MVMCoreCache+Extension.swift in Sources */,
016CF36925FA6DD400B82A1F /* ClientParameterHandler.swift in Sources */,
5846ABF42B44BB9000FA6C76 /* Collection+Safe.swift in Sources */,
AF69D4F7286EA0B800BC6862 /* ActionPreviousSubmitHandler.swift in Sources */,

View File

@ -383,8 +383,14 @@
if (requestParameters.backgroundRequest) {
return [self loadBackgroundRequest:requestParameters dataForPage:dataForPage delegateObject:delegateObject];
} else {
if (!requestParameters.noloadingOverlay) {
[[MVMCoreLoadingOverlayHandler sharedLoadingOverlay] startLoading];
}
MVMCoreLoadRequestOperation *loadOperation = [[MVMCoreLoadRequestOperation alloc] initWithRequestParameters:requestParameters dataForPage:dataForPage delegateObject:delegateObject backgroundLoad:NO];
loadOperation.identifier = requestParameters.identifier;
[loadOperation setCompletionBlock:^{
[[MVMCoreLoadingOverlayHandler sharedLoadingOverlay] stopLoading:YES];
}];
[self.blockingLoadQueue addOperation:loadOperation];
return loadOperation;
}

View File

@ -38,6 +38,38 @@ public enum PopBackError: MVMError, CustomStringConvertible {
@objc
public extension MVMCoreLoadRequestOperation {
@objc
@MainActor
func checkIfNewControllerIsNeeded(loadObject: MVMCoreLoadObject) -> Bool {
guard loadObject.requestParameters?.replaceViewIfOnStackElseLoadWithStyle == true,
let pageType = loadObject.pageType else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: Controller shouldn't be replaced.")
return true
}
let newVC = MVMCoreViewControllerMappingObject.shared()?.createMFViewController(ofTemplate: loadObject.pageJSON?.optionalStringForKey("template"), pageType: pageType)
guard let index = NavigationHandler.shared().navigationController?.viewControllers.firstIndex(where: { controller in
(controller as? MVMCoreViewControllerProtocol)?.pageType == pageType && type(of: controller) == type(of: newVC)
}) else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: No matching controller found.")
return true
}
if index == NavigationHandler.shared().navigationController!.viewControllers.count - 1 {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: Controller is already showing.")
Task {
MVMCoreLoadRequestOperation.loadFinished(loadObject, loadedViewController: nil, errorObject: nil)
}
} else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: Pop back to controller.")
guard let operation = try? NavigationHandler.shared().getOperationPopToViewController(with: pageType, navigationController: loadObject.requestParameters?.navigationController, delegateObject: loadObject.delegateObject, animated: !(loadObject.requestParameters?.shouldNotAnimatePush ?? false)) else { return true }
Task {
await navigate(with: operation, loadObject: loadObject)
MVMCoreLoadRequestOperation.loadFinished(loadObject, loadedViewController: nil, errorObject: nil)
}
}
return false
}
@objc
func popBackToPage(for loadObject: MVMCoreLoadObject) {
Task(priority: .high) {

View File

@ -90,6 +90,8 @@
*/
+ (void)removeCaches:(nullable NSDictionary *)cacheDictionary;
+ (void)notifyListenersOfNewResponse:(nullable NSDictionary *)pages modules:(nullable NSDictionary *)modules systemParameters:(nullable NSDictionary *)systemParameters loadObject:(nonnull MVMCoreLoadObject *)loadObject;
/** Creates the view controller based on the load object passed in.
* @param loadObject The load data from the cache or server.
* @param completionHandler The completion handler to load once finished. Returns any loaded view controller and the load.*/

View File

@ -132,71 +132,74 @@
} else {
// No provided load object, check the cache for data first..
[MVMCoreLoadRequestOperation checkCacheForDataForRequest:self.requestParameters completionHandler:^(NSDictionary *pageFromCache, NSDictionary *modulesFromCache) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
if ([self checkAndHandleForCancellation]) {
return;
}
[MVMCoreLoadRequestOperation checkCacheForDataForRequest:self.requestParameters completionHandler:^(NSDictionary *pageFromCache, NSDictionary *modulesFromCache) {
// Log if loaded from cache.
if (pageFromCache) {
MVMCoreLog(@"loaded from cache page %@",[MVMCoreActionUtility formatDictionaryAsJSONString:pageFromCache]);
}
if (modulesFromCache) {
MVMCoreLog(@"loaded from cache modules %@",[MVMCoreActionUtility formatDictionaryAsJSONString:modulesFromCache]);
}
// Create a load object from any data we fetched.
MVMCoreLoadObject *loadObject = [self createLoadObjectWithPageFromCache:pageFromCache modulesFromCache:modulesFromCache];
// Check if we need to go to server for missing data.
MVMCoreRequestParameters *requestForMissingData = [MVMCoreLoadRequestOperation createRequestForDataWithLoadObject:loadObject];
if (!requestForMissingData) {
[[MVMCoreLoggingHandler sharedLoggingHandler] logPageLoadCompleteFor:loadObject.requestParameters.pageType serverProcessingTime:@"0" requestURL:loadObject.requestParameters.URL.absoluteString requestUUID:loadObject.identifier isFromCache:loadObject.pageDataFromCache];
// We have all the needed data, continue with the load.
[MVMCoreLoadRequestOperation handleLoadObject:loadObject error:nil];
} else {
if(!self.backgroundLoad && loadObject.requestParameters.pageType) {
[[MVMCoreLoggingHandler sharedLoggingHandler] logPageLoadStartedFor:loadObject.requestParameters.pageType requestUUID:loadObject.identifier requestURL:loadObject.requestParameters.URL.absoluteString];
if ([self checkAndHandleForCancellation]) {
return;
}
// Send a new request to the server.
[MVMCoreLoadRequestOperation sendRequest:requestForMissingData loadObject:loadObject completionHandler:^(NSDictionary * _Nullable json) {
// Log if loaded from cache.
if (pageFromCache) {
MVMCoreLog(@"loaded from cache page %@",[MVMCoreActionUtility formatDictionaryAsJSONString:pageFromCache]);
}
if (modulesFromCache) {
MVMCoreLog(@"loaded from cache modules %@",[MVMCoreActionUtility formatDictionaryAsJSONString:modulesFromCache]);
}
// Create a load object from any data we fetched.
MVMCoreLoadObject *loadObject = [self createLoadObjectWithPageFromCache:pageFromCache modulesFromCache:modulesFromCache];
// Check if we need to go to server for missing data.
MVMCoreRequestParameters *requestForMissingData = [MVMCoreLoadRequestOperation createRequestForDataWithLoadObject:loadObject];
if (!requestForMissingData) {
[[MVMCoreLoggingHandler sharedLoggingHandler] logPageLoadCompleteFor:loadObject.requestParameters.pageType serverProcessingTime:@"0" requestURL:loadObject.requestParameters.URL.absoluteString requestUUID:loadObject.identifier isFromCache:loadObject.pageDataFromCache];
// We have all the needed data, continue with the load.
[MVMCoreLoadRequestOperation handleLoadObject:loadObject error:nil];
} else {
if(!self.backgroundLoad && loadObject.requestParameters.pageType) {
[[MVMCoreLoggingHandler sharedLoggingHandler] logPageLoadStartedFor:loadObject.requestParameters.pageType requestUUID:loadObject.identifier requestURL:loadObject.requestParameters.URL.absoluteString];
}
// Send a new request to the server.
[MVMCoreLoadRequestOperation sendRequest:requestForMissingData loadObject:loadObject completionHandler:^(NSDictionary * _Nullable json) {
#if ENABLE_HARD_CODED_RESPONSE
if ([[MVMCoreObject sharedInstance].globalLoadDelegate respondsToSelector:@selector(modifyJSON:)]) {
json = [[MVMCoreObject sharedInstance].globalLoadDelegate modifyJSON:json];
}
if ([[MVMCoreObject sharedInstance].globalLoadDelegate respondsToSelector:@selector(modifyJSON:)]) {
json = [[MVMCoreObject sharedInstance].globalLoadDelegate modifyJSON:json];
}
#endif
NSString *serverProcessTime = [(NSDictionary *)json objectChainOfKeysOrIndexes:@[@"ResponseInfo", @"timeStamp"]] ?: @"0";
NSString *serverProcessTime = [(NSDictionary *)json objectChainOfKeysOrIndexes:@[@"ResponseInfo", @"timeStamp"]] ?: @"0";
if(!self.backgroundLoad && loadObject.requestParameters.pageType && serverProcessTime) {
[[MVMCoreLoggingHandler sharedLoggingHandler] logPageLoadCompleteFor:loadObject.requestParameters.pageType serverProcessingTime:serverProcessTime requestURL:loadObject.requestParameters.URL.absoluteString requestUUID:loadObject.identifier isFromCache:loadObject.pageDataFromCache];
}
// Process the data retrieved from the server.
[MVMCoreLoadRequestOperation processJSONFromServer:json loadObject:loadObject completionHandler:^(MVMCoreLoadObject * _Nonnull loadObject, MVMCoreErrorObject * _Nullable error) {
if ([loadObject.operation checkAndHandleForCancellation]) {
return;
if(!self.backgroundLoad && loadObject.requestParameters.pageType && serverProcessTime) {
[[MVMCoreLoggingHandler sharedLoggingHandler] logPageLoadCompleteFor:loadObject.requestParameters.pageType serverProcessingTime:serverProcessTime requestURL:loadObject.requestParameters.URL.absoluteString requestUUID:loadObject.identifier isFromCache:loadObject.pageDataFromCache];
}
if (loadObject.pageDataFromCache || loadObject.pageType) {
// Process the data retrieved from the server.
[MVMCoreLoadRequestOperation processJSONFromServer:json loadObject:loadObject completionHandler:^(MVMCoreLoadObject * _Nonnull loadObject, MVMCoreErrorObject * _Nullable error) {
// Can continue loading with the page.
[MVMCoreLoadRequestOperation handleLoadObject:loadObject error:error];
} else {
// Something to show, or nothing was expected to show, can finish.
[MVMCoreLoadRequestOperation loadFinished:loadObject loadedViewController:nil errorObject:error];
}
if ([loadObject.operation checkAndHandleForCancellation]) {
return;
}
if (loadObject.pageDataFromCache || loadObject.pageType) {
// Can continue loading with the page.
[MVMCoreLoadRequestOperation handleLoadObject:loadObject error:error];
} else {
// Something to show, or nothing was expected to show, can finish.
[MVMCoreLoadRequestOperation loadFinished:loadObject loadedViewController:nil errorObject:error];
}
}];
}];
}];
}
}];
}
}];
});
}
}
@ -581,9 +584,14 @@
};
if (!error.nativeDrivenErrorScreen) {
// Server driven screen, create normally
[MVMCoreLoadRequestOperation createViewControllerWithLoadObject:loadObject completionHandler:completionHandler];
[MVMCoreDispatchUtility performBlockOnMainThread:^{
if ([loadObject.operation checkIfNewControllerIsNeededWithLoadObject:loadObject]) {
[MVMCoreDispatchUtility performBlockInBackground:^{
[MVMCoreLoadRequestOperation createViewControllerWithLoadObject:loadObject completionHandler:completionHandler];
}];
}
}];
} else {
// Get the proper native error screen from the delegate
[MVMCoreDispatchUtility performBlockOnMainThread:^{

View File

@ -0,0 +1,101 @@
//
// Cache.swift
// JSONCreator
//
// Created by Matt Bruce on 3/20/24.
// Copyright © 2024 Verizon Wireless. All rights reserved.
//
import Foundation
public enum CacheError: Error {
case serializationFailed
case deserializationFailed
case dataNotFound
case dataExpired
case saveFailed(Error)
case loadFailed(Error)
}
public class CachedData: Codable {
public var data: [String: AnyHashable]
public var expirationDate: Date
enum CodingKeys: CodingKey {
case data, expirationDate
}
public init(data: [String: AnyHashable], expirationDate: Date) {
self.data = data
self.expirationDate = expirationDate
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(expirationDate, forKey: .expirationDate)
let dataAsData = try JSONSerialization.data(withJSONObject: data, options: [])
try container.encode(dataAsData, forKey: .data)
}
required public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
expirationDate = try values.decode(Date.self, forKey: .expirationDate)
let dataAsData = try values.decode(Data.self, forKey: .data)
guard let jsonData = try JSONSerialization.jsonObject(with: dataAsData, options: []) as? [String: AnyHashable] else {
throw CacheError.deserializationFailed
}
data = jsonData
}
}
@objc public class DataCacheManager: NSObject {
@objc public static let shared = DataCacheManager()
// private let cache = NSCache<NSString, CachedData>()
private let fileManager = FileManager.default
private lazy var documentsDirectory = { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }()
public override init() {}
@objc public func save(data: [String: AnyHashable], forKey key: String, cacheDuration: TimeInterval, shouldPersist: Bool = true) throws {
let expirationDate = Date().addingTimeInterval(cacheDuration)
let cachedData = CachedData(data: data, expirationDate: expirationDate)
// cache.setObject(cachedData, forKey: NSString(string: key))
if shouldPersist {
let filePath = self.filePath(forKey: key)
do {
let dataToSave = try JSONEncoder().encode(cachedData)
try dataToSave.write(to: filePath, options: .atomicWrite)
} catch let error as EncodingError {
throw CacheError.serializationFailed
} catch {
throw CacheError.saveFailed(error)
}
}
}
@objc public func load(forKey key: String) throws -> [String: AnyHashable] {
let keyNSString = NSString(string: key)
// if let cachedObject = cache.object(forKey: keyNSString),
// Date() < cachedObject.expirationDate {
// return cachedObject.data
// } else {
let filePath = self.filePath(forKey: key)
do {
let data = try Data(contentsOf: filePath)
let decodedCachedData = try JSONDecoder().decode(CachedData.self, from: data)
if Date() < decodedCachedData.expirationDate {
// cache.setObject(decodedCachedData, forKey: keyNSString)
return decodedCachedData.data
} else {
throw CacheError.dataExpired
}
} catch let error as DecodingError {
throw CacheError.deserializationFailed
} catch {
throw CacheError.loadFailed(error)
}
// }
}
private func filePath(forKey key: String) -> URL {
return documentsDirectory.appendingPathComponent("\(key).json")
}
}

View File

@ -36,6 +36,8 @@ typedef void(^MVMCoreGetImageBlock)(UIImage * _Nullable, NSData * _Nullable, BOO
// Checks the set of modules to be cached for the given module
- (BOOL)shouldCacheJSONWithModule:(nonnull NSString *)module;
- (BOOL)shouldPersistentlyCache:(nonnull NSDictionary *)jsonDictionary identifier:(nonnull NSString *)identifier;
// For pages external to the mobile first framework to be added to the list to not cache
- (void)addPageTypesToNotCache:(nullable NSArray <NSString *>*)array;

View File

@ -39,6 +39,8 @@
@property (nullable, strong, nonatomic) NSCache *playerItemCache;
@property (nullable, strong, nonatomic) NSSet *itemsToPersist;
@end
@implementation MVMCoreCache
@ -65,6 +67,8 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
self.videoQueue = dispatch_queue_create("video_queue", dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
self.playerItemCache = [[NSCache alloc] init];
self.itemsToPersist = [NSSet setWithObjects:@"myFeed",@"FeedOrder",@"HabContent",@"launchModule",@"tabBar",@"DeepLinkService", nil];
}
return self;
}
@ -140,6 +144,10 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
// First checks the cache by page type.
dictionary = [weakSelf.pageTypeCache objectForKey:pageType];
if (!dictionary) {
NSError *error = nil;
dictionary = [[DataCacheManager shared] loadForKey:pageType error:&error];
}
} else {
// If no pagetype, return whole cache
@ -176,6 +184,12 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
NSDictionary *moduleDictionary = [weakSelf.moduleCache objectForKey:module];
if (moduleDictionary) {
[modulesDictionary setObject:moduleDictionary forKey:module];
} else {
NSError *error = nil;
moduleDictionary = [[DataCacheManager shared] loadForKey:module error:&error];
if (moduleDictionary) {
[modulesDictionary setObject:moduleDictionary forKey:module];
}
}
}
@ -206,6 +220,13 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
[self addModulesToCache:jsonDictionary queue:nil waitUntilFinished:NO completionBlock:NULL];
}
- (BOOL)shouldPersistentlyCache:(nonnull NSDictionary *)jsonDictionary identifier:(nonnull NSString *)identifier {
if ([self.itemsToPersist containsObject:identifier]) {
return YES;
}
return NO;
}
#pragma mark - Advanced Insertion
- (void)addPageToCache:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock {
@ -230,6 +251,10 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
NSNumber *shouldCache = [jsonDictionary optionalNumberForKey:@"cache"];
if (shouldCache == nil || shouldCache.boolValue) {
[weakSelf.pageTypeCache setObject:jsonDictionary forKey:pageType];
if ([self shouldPersistentlyCache:jsonDictionary identifier:pageType]) {
NSError *error = nil;
[[DataCacheManager shared] saveWithData:jsonDictionary forKey:pageType cacheDuration:604800 shouldPersist:YES error:&error];
}
}
}
}
@ -267,6 +292,11 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
NSNumber *shouldCache = [jsonDictionary optionalNumberForKey:@"cache"];
if (shouldCache == nil || shouldCache.boolValue) {
[weakSelf.moduleCache setObject:obj forKey:key];
if ([self shouldPersistentlyCache:obj identifier:key]) {
NSError *error = nil;
[[DataCacheManager shared] saveWithData:obj forKey:key cacheDuration:604800 shouldPersist:YES error:&error];
}
}
}
}];