Merge branch 'feature/feed_cache' into 'release/11_5_0'

Feature/feed cache

### Summary
Persistently Cached Discover

### JIRA Ticket
https://onejira.verizon.com/browse/MVAPCT-48

Co-authored-by: Scott Pfeil <Scott.Pfeil3@verizonwireless.com>
Co-authored-by: Hedden, Kyle Matthew <kyle.hedden@verizonwireless.com>

See merge request https://gitlab.verizon.com/BPHV_MIPS/mvm_core/-/merge_requests/319
This commit is contained in:
Bruce, Matt R 2024-04-05 20:17:36 +00:00
commit f6f272f727
15 changed files with 401 additions and 54 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

@ -83,7 +83,7 @@ open class ActionOpenPageHandler: MVMCoreJSONActionHandlerProtocol {
}
// Adds any client parameters to the request parameters.
if let parametersToFetch = model.clientParameters,
let fetchedParameters = try await ClientParameterHandler().getClientParameters(
let fetchedParameters = await ClientParameterHandler().getClientParameters(
with: parametersToFetch,
requestParameters: requestParameters.parameters as? [String : Any] ?? [:],
actionId: MVMCoreActionHandler.getUUID(additionalData: additionalData) ?? "unknown",

View File

@ -385,6 +385,7 @@
} else {
MVMCoreLoadRequestOperation *loadOperation = [[MVMCoreLoadRequestOperation alloc] initWithRequestParameters:requestParameters dataForPage:dataForPage delegateObject:delegateObject backgroundLoad:NO];
loadOperation.identifier = requestParameters.identifier;
[loadOperation startLoadingAnimationIfNeeded];
[self.blockingLoadQueue addOperation:loadOperation];
return loadOperation;
}
@ -399,6 +400,8 @@
- (MVMCoreLoadRequestOperation *)loadObject:(nonnull MVMCoreLoadObject *)loadObject {
MVMCoreLoadRequestOperation *loadOperation = [[MVMCoreLoadRequestOperation alloc] initWithLoadObject:loadObject backgroundLoad:NO];
loadOperation.identifier = loadObject.requestParameters.identifier;
[loadOperation startLoadingAnimationIfNeeded];
[self.blockingLoadQueue addOperation:loadOperation];
return loadOperation;
}

View File

@ -38,6 +38,41 @@ public enum PopBackError: MVMError, CustomStringConvertible {
@objc
public extension MVMCoreLoadRequestOperation {
/// Attempt to navigate to the controller with the given load object. Return the controller that we navigated to if successful.
@objc
@MainActor
func goToViewController(loadObject: MVMCoreLoadObject) async -> UIViewController? {
guard loadObject.requestParameters?.replaceViewIfOnStackElseLoadWithStyle == true,
let pageType = loadObject.pageType else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "\(type(of: self)): A new controller should be made and any existing shouldn't be replaced. pageType:\(String(describing: loadObject.pageType))")
return nil
}
let template = loadObject.pageJSON?.optionalStringForKey("template")
guard let controllerMappingObject = MVMCoreViewControllerMappingObject.shared()?.getViewControllerMapping(forTemplate: template, pageType: pageType) else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "\(type(of: self)): Failed to create a new controller. template:\(String(describing: template)) page:\(pageType)")
return nil
}
var controllerType: AnyClass?
if let programmaticMapping = controllerMappingObject as? MVMCoreViewControllerProgrammaticMappingObject {
controllerType = programmaticMapping.viewControllerClass
} else if let newVC = MVMCoreViewControllerMappingObject.shared()?.createMFViewController(ofTemplate: loadObject.pageJSON?.optionalStringForKey("template"), pageType: pageType) {
// Need to create the view controller to fetch the type.
controllerType = type(of: newVC)
} else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "\(type(of: self)): Failed to create a new controller. template:\(String(describing: template)) page:\(pageType)")
return nil
}
guard let viewController = await NavigationHandler.shared().navigateToViewController(of: pageType, controllerType: controllerType) else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "\(type(of: self)): No matching controller found in the hierarchy. Will need to create a new controller. pageType:\(pageType) controllerType:\(String(describing: controllerType)).")
return nil
}
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "\(type(of: self)): Navigated to controller. pageType:\(pageType) controllerType:\(String(describing: controllerType))")
stopLoadingAnimationIfNeeded()
return viewController
}
@objc
func popBackToPage(for loadObject: MVMCoreLoadObject) {
Task(priority: .high) {
@ -71,13 +106,9 @@ public extension MVMCoreLoadRequestOperation {
func navigate(with navigationOperation: NavigationOperation, loadObject: MVMCoreLoadObject?) async {
// stop any loading animation we may have started if we are about to display
cancellable = NavigationHandler.shared().onNavigation
.filter { $0.0 == .willNavigate }
.sink { (event, operation) in
if navigationOperation == operation,
!self.backgroundLoad,
!(loadObject?.requestParameters?.noloadingOverlay ?? false) {
MVMCoreLoadingOverlayHandler.sharedLoadingOverlay()?.stopLoading(false)
}
.filter { $0.0 == .willNavigate && navigationOperation == $0.1 }
.sink { [weak self] (event, operation) in
self?.stopLoadingAnimationIfNeeded()
}
await NavigationHandler.shared().navigate(with: navigationOperation)
cancellable = nil

View File

@ -19,7 +19,6 @@
@interface MVMCoreLoadRequestOperation : MVMCoreOperation
@property (nullable, strong, nonatomic) MVMCoreRequestParameters *requestParameters;
/// For load objects as in input parameter. Does not attach self generated load objects.
@property (nullable, strong, nonatomic) MVMCoreLoadObject *loadObject;
@property (nullable, strong, nonatomic) NSDictionary *dataForPage;
@property (nullable, strong, nonatomic) DelegateObject *delegateObject;
@ -42,6 +41,12 @@
// Initializes the operation with the load object, data for page, and mvm view controller to handle the loading with. Can be used for loading a screen without going to the cache or server.
- (nullable instancetype)initWithLoadObject:(nullable MVMCoreLoadObject *)loadObject backgroundLoad:(BOOL)backgroundLoad;
/// Begins the loading animation if needed.
- (void)startLoadingAnimationIfNeeded;
/// Ends the loading animation if needed.
- (void)stopLoadingAnimationIfNeeded;
/* Checks the cache for the data and calls the completion handler with any found data.
* @param completionHandler The block that gets called with any fetched data. */
+ (void)checkCacheForDataForRequest:(nonnull MVMCoreRequestParameters *)requestParameters completionHandler:(nonnull void (^)(NSDictionary * _Nullable pageFromCache, NSDictionary * _Nullable modulesFromCache))completionHandler;
@ -90,6 +95,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

@ -34,6 +34,8 @@
@property (nonatomic, readwrite) BOOL alertToShow;
@property (strong, nonatomic, nullable) MVMCoreErrorObject *errorForAlertToShow;
@property (nonatomic, readwrite) BOOL loadingAnimationRunning;
@end
@implementation MVMCoreLoadRequestOperation
@ -64,14 +66,13 @@
- (void)cancel {
[super cancel];
[self.sessionTask cancel];
[self stopLoadingAnimationIfNeeded];
}
- (void)start {
// Adds a loading overlay if necessary.
if (!self.backgroundLoad && !self.requestParameters.noloadingOverlay) {
[[MVMCoreLoadingOverlayHandler sharedLoadingOverlay] startLoading];
}
[self startLoadingAnimationIfNeeded];
[super start];
}
@ -83,9 +84,7 @@
- (void)markAsFinished {
// stop any loading animation we may have started
if (!self.backgroundLoad && !self.requestParameters.noloadingOverlay) {
[[MVMCoreLoadingOverlayHandler sharedLoadingOverlay] stopLoading:NO];
}
[self stopLoadingAnimationIfNeeded];
MVMCoreLog(@"Load Operation finished for page type %@, background load %@", self.requestParameters.pageType, @(self.backgroundLoad));
[super markAsFinished];
@ -148,6 +147,7 @@
// Create a load object from any data we fetched.
MVMCoreLoadObject *loadObject = [self createLoadObjectWithPageFromCache:pageFromCache modulesFromCache:modulesFromCache];
self.loadObject = loadObject;
// Check if we need to go to server for missing data.
MVMCoreRequestParameters *requestForMissingData = [MVMCoreLoadRequestOperation createRequestForDataWithLoadObject:loadObject];
@ -202,6 +202,20 @@
#pragma mark - Load Functions
- (void)startLoadingAnimationIfNeeded {
if (self.loadingAnimationRunning) { return; }
if (self.backgroundLoad) { return; }
if (self.requestParameters.noloadingOverlay) { return; }
[[MVMCoreLoadingOverlayHandler sharedLoadingOverlay] startLoading];
self.loadingAnimationRunning = YES;
}
- (void)stopLoadingAnimationIfNeeded {
if (!self.loadingAnimationRunning) { return; }
[[MVMCoreLoadingOverlayHandler sharedLoadingOverlay] stopLoading:YES];
self.loadingAnimationRunning = NO;
}
+ (void)checkCacheForDataForRequest:(nonnull MVMCoreRequestParameters *)requestParameters completionHandler:(nonnull void (^)(NSDictionary * _Nullable pageFromCache, NSDictionary * _Nullable modulesFromCache))completionHandler {
if (requestParameters.neverLoadFromCache) {
@ -430,6 +444,10 @@
NSDictionary *systemParameters = [jsonDictionary dict:KeySystemParameters];
loadObject.systemParametersJSON = systemParameters.count > 0 ? systemParameters : nil;
if ([[MVMCoreObject sharedInstance].globalLoadDelegate respondsToSelector:@selector(willProcessLoadObject:)]) {
[[MVMCoreObject sharedInstance].globalLoadDelegate willProcessLoadObject:loadObject];
}
// module items are cached.
MVMCoreErrorObject *moduleCachingError = nil;
BOOL shouldContinue = [MVMCoreLoadRequestOperation cacheModules:modules loadObject:loadObject error:&moduleCachingError];
@ -581,9 +599,18 @@
};
if (!error.nativeDrivenErrorScreen) {
// Server driven screen, create normally
[MVMCoreLoadRequestOperation createViewControllerWithLoadObject:loadObject completionHandler:completionHandler];
[MVMCoreDispatchUtility performBlockOnMainThread:^{
[loadObject.operation goToViewControllerWithLoadObject:loadObject completionHandler:^(UIViewController * _Nullable viewController) {
[MVMCoreDispatchUtility performBlockInBackground:^{
if (viewController) {
[MVMCoreLoadRequestOperation loadFinished:loadObject loadedViewController:viewController errorObject:nil];
} else {
[MVMCoreLoadRequestOperation createViewControllerWithLoadObject:loadObject completionHandler:completionHandler];
}
}];
}];
}];
} else {
// Get the proper native error screen from the delegate
[MVMCoreDispatchUtility performBlockOnMainThread:^{

View File

@ -41,6 +41,9 @@
/// Checks to see if the operation has content to show.
- (BOOL)hasContentToShow:(nonnull MVMCoreLoadObject *)loadObject error:(nullable MVMCoreErrorObject *)error;
/// Notifies the delegate we are about to process the load object.
- (void)willProcessLoadObject:(nonnull MVMCoreLoadObject *)loadObject;
#if ENABLE_HARD_CODED_RESPONSE
- (nullable NSDictionary *)getJSONForRequestParameters:(nonnull MVMCoreRequestParameters *)requestParameters;
- (nonnull NSDictionary *)modifyJSON:(nonnull NSDictionary *)json;

View File

@ -19,6 +19,9 @@
- (nullable NSArray*)getAccessibilityElements; //AccessibilityElements that are owned by Manager.
/// Attempt to navigate to the controller. Return the controller that we navigated to if successful.
- (void)navigateToViewControllerOfPageType:(nonnull NSString *)pageType controllerType:(_Nullable Class)controllerType completionHandler:(void (^ __nullable)(UIViewController * _Nullable viewController))completionHandler;
@optional
/// Notifies the manager that the controller received new data.

View File

@ -0,0 +1,106 @@
//
// 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 PersistentCacheManager: NSObject {
@objc public static let shared = PersistentCacheManager()
private let fileManager = FileManager.default
@objc public lazy var cacheDirectory = { fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!.appendingPathComponent("Atomic")}()
private override init() {}
@objc public func save(data: [String: AnyHashable], forKey key: String, path: URL, expirationDate: Date) throws {
let cachedData = CachedData(data: data, expirationDate: expirationDate)
do {
try FileManager.default.createDirectory(atPath: path.deletingLastPathComponent().relativePath, withIntermediateDirectories: true, attributes: [.protectionKey: FileProtectionType.complete])
let dataToSave = try JSONEncoder().encode(cachedData)
try dataToSave.write(to: path, options: [.atomic, .completeFileProtection])
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: SAVED TO PERSISTENT CACHE, key:\(key), path:\(path)")
} catch is EncodingError {
throw CacheError.serializationFailed
} catch {
throw CacheError.saveFailed(error)
}
}
@objc public func load(forKey key: String, path: URL) throws -> [String: AnyHashable] {
do {
let data = try Data(contentsOf: path)
let decodedCachedData = try JSONDecoder().decode(CachedData.self, from: data)
if Date() < decodedCachedData.expirationDate {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: LOADED FROM PERSISTENT CACHE, key:\(key), path:\(path)")
return decodedCachedData.data
} else {
MVMCoreLoggingHandler.logDebugMessage(withDelegate: "CACHEDFEED: EXPIRED, key:\(key), path:\(path)")
throw CacheError.dataExpired
}
} catch {
// Remove item from the cache on any failure.
try fileManager.removeItem(at: path)
switch error {
case is DecodingError:
throw CacheError.deserializationFailed
default:
throw CacheError.loadFailed(error)
}
}
}
@objc public func remove(forKey key: String) throws {
let filePath = self.filePath(forKey: key)
try fileManager.removeItem(at: filePath)
}
@objc public func removeAll() throws {
try FileManager.default.removeItem(at: cacheDirectory)
}
private func filePath(forKey key: String) -> URL {
return cacheDirectory.appendingPathComponent("\(key).json")
}
}

View File

@ -12,6 +12,9 @@
#import <AVKit/AVKit.h>
@class MVMCoreErrorObject;
extern NSString * _Nonnull const KeyCachePolicy;
extern NSString * _Nonnull const KeyCacheExpiry;
//block returned when getting image
//parameters are UIImage object for the image, NSData for gif images, UIImage object for the image, A BOOL to indicate if it is a fall back image.
typedef void(^MVMCoreGetImageBlock)(UIImage * _Nullable, NSData * _Nullable, BOOL);
@ -31,10 +34,28 @@ typedef void(^MVMCoreGetImageBlock)(UIImage * _Nullable, NSData * _Nullable, BOO
#pragma mark - Page and Module Handling
// Checks the set of pageTypes to be cached for the given pageType.
- (BOOL)shouldCacheJSONWithPageType:(nonnull NSString *)pageType;
- (BOOL)shouldCachePageJSON:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType;
// Checks the set of modules to be cached for the given module
- (BOOL)shouldCacheJSONWithModule:(nonnull NSString *)module;
- (BOOL)shouldCacheModuleJSON:(nonnull NSDictionary *)jsonDictionary moduleName:(nonnull NSString *)moduleName;
/// Returns if the json is expired or not.
- (BOOL)isJSONExpired:(nonnull NSDictionary *)jsonDictionary;
/// Returns the expiry time for the object.
- (nonnull NSDate *)getExpirationDateForJSON:(nonnull NSDictionary *)jsonDictionary;
/// Checks if the page is to be persistently cached.
- (BOOL)shouldPersistentlyCachePage:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType;
/// Checks if the module is to be persistently cached.
- (BOOL)shouldPersistentlyCacheModule:(nonnull NSDictionary *)jsonDictionary module:(nonnull NSString *)module;
/// Can override the path for the page to be cached. Currently Cache/Atomic/Pages/pageType
- (nullable NSURL *)getPathForPersistentCachePage:(nonnull NSString *)pageType;
/// Can override the path for the page to be cached. Currently Cache/Atomic/Modules/moduleName
- (nullable NSURL *)getPathForPersistentCacheModule:(nonnull NSString *)moduleName;
// For pages external to the mobile first framework to be added to the list to not cache
- (void)addPageTypesToNotCache:(nullable NSArray <NSString *>*)array;
@ -53,6 +74,12 @@ typedef void(^MVMCoreGetImageBlock)(UIImage * _Nullable, NSData * _Nullable, BOO
// Gets a json dictionary from the cache with all the requested modules. Pass in the block that you want to run once the dictionary is received. This will be run on a background thread.
- (void)fetchJSONForModules:(nullable NSArray *)modules completionHandler:(nonnull void (^)(NSDictionary * _Nullable jsonDictionary))completionHandler;
/// Returns a page JSON from the persistent cache.
- (NSDictionary * _Nullable)fetchPageFromPersistentCache:(nonnull NSString *)pageType;
/// Returns a module JSON from the persistent cache.
- (NSDictionary * _Nullable)fetchModuleFromPersistentCache:(nonnull NSString *)moduleName;
#pragma mark - Advanced Fetch
// Pass in the block that you want to run once the dictionary is received and which queue to run it on. Pass in if you'd like the current thread to wait.
@ -75,6 +102,12 @@ typedef void(^MVMCoreGetImageBlock)(UIImage * _Nullable, NSData * _Nullable, BOO
// Adds a json dictionary to the cache by modules.
- (void)addModulesToCache:(nonnull NSDictionary *)jsonDictionary;
/// Adds the json to the persistent cache by pageType.
- (void)addPageToPersistentCache:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType expirationDate:(nonnull NSDate *)expirationDate;
/// Adds the json to the persistent cache by module.
- (void)addModuleToPersistentCache:(nonnull NSDictionary *)jsonDictionary moduleName:(nonnull NSString *)moduleName expirationDate:(nonnull NSDate *)expirationDate;
#pragma mark - Advanced Insertion
// Adds a json dictionary to the cache by pageType. Pass in the block that you want to run once the dictionary is received and which queue to run it on. Pass in if you'd like the current thread to wait.
@ -111,6 +144,9 @@ typedef void(^MVMCoreGetImageBlock)(UIImage * _Nullable, NSData * _Nullable, BOO
// Removes the json for modules. Pass in the block that you want to run once the dictionary is received and which queue to run it on.
- (void)removeJSONForModules:(nonnull NSArray *)modules queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock;
/// Clears the persistent JSON cache
- (void)clearPersistentJSONCache;
#pragma mark Image Functions
/// Register a bundle as one to search for images in.

View File

@ -17,6 +17,9 @@
#import "MVMCoreErrorConstants.h"
#import "MVMCoreLoggingHandlerHelper.h"
NSString * _Nonnull const KeyCachePolicy = @"cachePolicy";
NSString * _Nonnull const KeyCacheExpiry = @"expiry";
@interface MVMCoreCache ()
// The cache for json.
@ -91,12 +94,55 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
return _modulesToNotCache;
}
- (BOOL)shouldCacheJSONWithPageType:(nonnull NSString *)pageType {
return ![self.pageTypesToNotCache containsObject:pageType];
- (BOOL)shouldCachePageJSON:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType {
if ([self.pageTypesToNotCache containsObject:pageType]) { return NO; }
NSNumber *shouldCache = [jsonDictionary optionalNumberForKey:@"cache"];
return shouldCache == nil || shouldCache.boolValue;
}
- (BOOL)shouldCacheJSONWithModule:(nonnull NSString *)module {
return ![self.modulesToNotCache containsObject:module];
- (BOOL)shouldCacheModuleJSON:(nonnull NSDictionary *)jsonDictionary moduleName:(nonnull NSString *)moduleName {
if ([self.modulesToNotCache containsObject:moduleName]) { return NO; }
NSNumber *shouldCache = [jsonDictionary optionalNumberForKey:@"cache"];
return shouldCache == nil || shouldCache.boolValue;
}
- (BOOL)isJSONExpired:(nonnull NSDictionary *)jsonDictionary {
NSDate *expirationDate = [self getExpirationDateForJSON:jsonDictionary];
NSDate *now = [NSDate date];
if ([now compare:expirationDate] == NSOrderedDescending) {
[MVMCoreLoggingHandler logDebugMessageWithDelegate:[NSString stringWithFormat:@"CACHEDFEED: NEW DATA ALREADY EXPIRED %@ now:%@ expirationDate:%@",jsonDictionary,now,expirationDate]];
}
return [now compare:expirationDate] == NSOrderedDescending;
}
- (nonnull NSDate *)getExpirationDateForJSON:(nonnull NSDictionary *)jsonDictionary {
NSDictionary *cachePolicy = [jsonDictionary dict:KeyCachePolicy];
NSTimeInterval interval = [[cachePolicy string:KeyCacheExpiry] doubleValue] / 1000;
return [NSDate dateWithTimeIntervalSince1970:interval];
}
- (BOOL)shouldPersistentlyCacheJSON:(nonnull NSDictionary *)jsonDictionary {
NSDictionary *cachePolicy = [jsonDictionary dict:KeyCachePolicy];
if (!cachePolicy || ![cachePolicy boolForKey:@"persist"]) {
return NO;
}
return ![self isJSONExpired:jsonDictionary];
}
- (BOOL)shouldPersistentlyCachePage:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType {
return [self shouldPersistentlyCacheJSON:jsonDictionary];
}
- (BOOL)shouldPersistentlyCacheModule:(nonnull NSDictionary *)jsonDictionary module:(nonnull NSString *)module {
return [self shouldPersistentlyCacheJSON:jsonDictionary];
}
- (nullable NSURL *)getPathForPersistentCachePage:(nonnull NSString *)pageType {
return [[[[PersistentCacheManager shared].cacheDirectory URLByAppendingPathComponent:@"Pages"] URLByAppendingPathComponent:pageType] URLByAppendingPathExtension:@"json"];
}
- (nullable NSURL *)getPathForPersistentCacheModule:(nonnull NSString *)moduleName {
return [[[[PersistentCacheManager shared].cacheDirectory URLByAppendingPathComponent:@"Modules"] URLByAppendingPathComponent:moduleName]URLByAppendingPathExtension:@"json"];
}
- (void)addPageTypesToNotCache:(nullable NSArray <NSString *>*)array {
@ -125,6 +171,16 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
[self fetchJSONForModules:modules queue:self.completionQueue waitUntilFinished:NO completionHandler:completionHandler];
}
- (NSDictionary * _Nullable)fetchPageFromPersistentCache:(nonnull NSString *)pageType {
NSError *error = nil;
return [[PersistentCacheManager shared] loadForKey:pageType path:[self getPathForPersistentCachePage:pageType] error:&error];
}
- (NSDictionary * _Nullable)fetchModuleFromPersistentCache:(nonnull NSString *)moduleName {
NSError *error = nil;
return [[PersistentCacheManager shared] loadForKey:moduleName path:[self getPathForPersistentCacheModule:moduleName] error:&error];
}
#pragma mark - Advanced Fetch
- (void)fetchJSONForPageType:(nullable NSString *)pageType queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionHandler:(nonnull void (^)(NSDictionary * _Nullable jsonDictionary))completionHandler {
@ -140,6 +196,9 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
// First checks the cache by page type.
dictionary = [weakSelf.pageTypeCache objectForKey:pageType];
if (!dictionary) {
dictionary = [self fetchPageFromPersistentCache:pageType];
}
} else {
// If no pagetype, return whole cache
@ -176,6 +235,11 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
NSDictionary *moduleDictionary = [weakSelf.moduleCache objectForKey:module];
if (moduleDictionary) {
[modulesDictionary setObject:moduleDictionary forKey:module];
} else {
moduleDictionary = [self fetchModuleFromPersistentCache:module];
if (moduleDictionary) {
[modulesDictionary setObject:moduleDictionary forKey:module];
}
}
}
@ -206,16 +270,37 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
[self addModulesToCache:jsonDictionary queue:nil waitUntilFinished:NO completionBlock:NULL];
}
- (void)addPageToPersistentCache:(nonnull NSDictionary *)jsonDictionary pageType:(nonnull NSString *)pageType expirationDate:(nonnull NSDate *)expirationDate {
NSError *error = nil;
[[PersistentCacheManager shared] saveWithData:jsonDictionary forKey:pageType path:[self getPathForPersistentCachePage:pageType] expirationDate:expirationDate error:&error];
if (error) {
[[MVMCoreLoggingHandler sharedLoggingHandler] addErrorToLog:[MVMCoreErrorObject createErrorObjectForNSError:error location:[NSString stringWithFormat:@"%s_%@",__PRETTY_FUNCTION__,pageType]]];
}
}
- (void)addModuleToPersistentCache:(nonnull NSDictionary *)jsonDictionary moduleName:(nonnull NSString *)moduleName expirationDate:(nonnull NSDate *)expirationDate {
NSError *error = nil;
[[PersistentCacheManager shared] saveWithData:jsonDictionary forKey:moduleName path:[self getPathForPersistentCacheModule:moduleName] expirationDate:expirationDate error:&error];
if (error) {
[[MVMCoreLoggingHandler sharedLoggingHandler] addErrorToLog:[MVMCoreErrorObject createErrorObjectForNSError:error location:[NSString stringWithFormat:@"%s_%@",__PRETTY_FUNCTION__,moduleName]]];
}
}
#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 {
if (![self shouldCachePageJSON:jsonDictionary pageType:pageType]) {
if (completionBlock) {
completionBlock();
}
return;
}
NSBlockOperation *addOperation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOperation = addOperation;
__weak typeof(self) weakSelf = self;
[addOperation addExecutionBlock:^{
if (!weakOperation.isCancelled && [[MVMCoreCache sharedCache] shouldCacheJSONWithPageType:pageType]) {
if (!weakOperation.isCancelled) {
// There must be a dictionary and page type to cache.
if (jsonDictionary && pageType) {
@ -227,10 +312,13 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
}
// Adds json to cache with page type key.
NSNumber *shouldCache = [jsonDictionary optionalNumberForKey:@"cache"];
if (shouldCache == nil || shouldCache.boolValue) {
[weakSelf.pageTypeCache setObject:jsonDictionary forKey:pageType];
[weakSelf.pageTypeCache setObject:jsonDictionary forKey:pageType];
if (![self shouldPersistentlyCachePage:jsonDictionary pageType:pageType]) {
[[PersistentCacheManager shared] removeForKey:pageType error:nil];
return;
}
[self addPageToPersistentCache:jsonDictionary pageType:pageType expirationDate:[self getExpirationDateForJSON:jsonDictionary]];
}
}
if (completionBlock) {
@ -241,11 +329,12 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
}
- (void)addModuleToCache:(nonnull NSDictionary *)jsonDictionary module:(nonnull NSString *)module queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock {
[self addModulesToCache:@{module:jsonDictionary} queue:queue waitUntilFinished:waitUntilFinished completionBlock:completionBlock];
}
- (void)addModulesToCache:(nonnull NSDictionary *)jsonDictionary queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock {
if (![self shouldCacheModuleJSON:jsonDictionary moduleName:module]) {
if (completionBlock) {
completionBlock();
}
return;
}
NSBlockOperation *addOperation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOperation = addOperation;
__weak typeof(self) weakSelf = self;
@ -253,23 +342,19 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
if (!weakOperation.isCancelled) {
[jsonDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if (!weakSelf.moduleCache) {
if ([[MVMCoreCache sharedCache] shouldCacheJSONWithModule:key]) {
// Create the cache if necessary.
weakSelf.moduleCache = [NSMutableDictionary dictionary];
}
if (!weakSelf.moduleCache) {
[weakSelf.moduleCache setObject:jsonDictionary forKey:module];
// Create the cache if necessary.
weakSelf.moduleCache = [NSMutableDictionary dictionary];
}
// Adds json to cache with page type key.
NSNumber *shouldCache = [jsonDictionary optionalNumberForKey:@"cache"];
if (shouldCache == nil || shouldCache.boolValue) {
[weakSelf.moduleCache setObject:obj forKey:key];
}
}
}];
if (![self shouldPersistentlyCacheModule:jsonDictionary module:module]) {
[[PersistentCacheManager shared] removeForKey:module error:nil];
return;
}
[self addModuleToPersistentCache:jsonDictionary moduleName:module expirationDate:[self getExpirationDateForJSON:jsonDictionary]];
}
if (completionBlock) {
[(queue ?: weakSelf.completionQueue) addOperations:@[[NSBlockOperation blockOperationWithBlock:completionBlock]] waitUntilFinished:waitUntilFinished];
@ -278,6 +363,18 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
[self.moduleQueue addOperations:@[addOperation] waitUntilFinished:waitUntilFinished];
}
- (void)addModulesToCache:(nonnull NSDictionary *)jsonDictionary queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock {
[jsonDictionary enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[self addModuleToCache:obj module:key];
}];
__weak typeof(self) weakSelf = self;
[self.moduleQueue addOperations:@[[NSBlockOperation blockOperationWithBlock:^{
if (completionBlock) {
[(queue ?: weakSelf.completionQueue) addOperations:@[[NSBlockOperation blockOperationWithBlock:completionBlock]] waitUntilFinished:waitUntilFinished];
}
}]] waitUntilFinished:waitUntilFinished];
}
#pragma mark - Simple Deletion
- (void)removeJSONForPageType:(nonnull NSString *)pageType {
@ -292,6 +389,10 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
[self removeJSONForModules:modules queue:nil waitUntilFinished:NO completionBlock:NULL];
}
- (void)clearPersistentJSONCache {
[[PersistentCacheManager shared] removeAllAndReturnError:nil];
}
- (void)clearMFCache {
[self.pageTypeQueue cancelAllOperations];
[self.moduleQueue cancelAllOperations];
@ -329,6 +430,7 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
}
}];
[self.pageTypeQueue addOperations:@[removeOperation] waitUntilFinished:waitUntilFinished];
[[PersistentCacheManager shared] removeForKey:pageType error:nil];
}
- (void)removeJSONForModule:(nonnull NSString *)module queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock {
@ -352,6 +454,7 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
}
}];
[self.moduleQueue addOperations:@[removeOperation] waitUntilFinished:waitUntilFinished];
[[PersistentCacheManager shared] removeForKey:module error:nil];
}
- (void)removeJSONForModules:(nonnull NSArray *)modules queue:(nullable NSOperationQueue *)queue waitUntilFinished:(BOOL)waitUntilFinished completionBlock:(nullable void (^)(void))completionBlock {
@ -371,6 +474,7 @@ static NSString * const STATIC_CACHE_COMPONENT = @"StaticCache.txt";
// Removes json from cache with module key.
[weakSelf.moduleCache removeObjectForKey:obj];
}
[[PersistentCacheManager shared] removeForKey:obj error:nil];
}
}];
if (completionBlock) {

View File

@ -189,6 +189,13 @@ public class NavigationHandler {
await navigate(with: .pop(navigationController: navigationController, animated: animated), delegateObject: delegateObject)
}
}
/// Attempts to go to navigate to a viewcontroller of pageType and controllerType. Returns the view controller if successful
@MainActor
func navigateToViewController(of pageType: String, controllerType: AnyClass?) async -> UIViewController? {
// TODO: Need to manage for present view controllers.
return await MVMCoreObject.sharedInstance()?.viewControllerManager?.navigate(toViewControllerOfPageType: pageType, controllerType: controllerType)
}
}
extension UINavigationController {

View File

@ -24,6 +24,9 @@ public class MVMCoreObject: NSObject {
public var loadingProtocol: MVMCoreLoadingOverlayDelegateProtocol?
public var loggingDelegate: MVMCoreLoggingDelegateProtocol?
/// The main manager of the view controllers in the application.
public weak var viewControllerManager: MVMCoreViewManagerProtocol?
/// A reference to the calling application delegate that should be set. For a normal app, could be the UIApplicationDelegate. For watch, could be WKExtensionDelegate. For iMessage, could be MSMessagesAppViewController. etc, etc. Useful for the framework to call delegate specific functions.
public weak var applicationDelegate: AnyObject?

View File

@ -47,6 +47,9 @@
// For pages external to the mobile first framework to be added to the view controller mapping.
- (void)addToTemplateViewControllerMapping:(nullable NSDictionary <NSString *,NSObject <MVMCoreViewControllerMappingProtocol>*>*)map;
// Transition function: A mix of new and legacy.
- (nullable NSObject <MVMCoreViewControllerMappingProtocol>*)getViewControllerMappingForTemplate:(nullable NSString *)templateID pageType:(nullable NSString *)pageType;
// Transition function: A mix of new and legacy.
- (nullable UIViewController <MVMCoreViewControllerProtocol> *)createMFViewControllerOfTemplate:(nullable NSString *)templateID pageType:(nullable NSString *)pageType;

View File

@ -46,6 +46,16 @@
}
}
- (nullable NSObject <MVMCoreViewControllerMappingProtocol>*)getViewControllerMappingForTemplate:(nullable NSString *)templateID pageType:(nullable NSString *)pageType {
if (templateID) {
return [self getViewControllerMappingForTemplate:templateID];
} else if (pageType) {
return [self getViewControllerMappingForPageType:pageType];
} else {
return nil;
}
}
// Transition function: A mix of new and legacy.
- (nullable UIViewController <MVMCoreViewControllerProtocol> *)createMFViewControllerOfTemplate:(nullable NSString *)templateID pageType:(nullable NSString *)pageType {
if (templateID) {