Skip to content

Commit

Permalink
Merge pull request #17945 from wordpress-mobile/issue/17873-persist-d…
Browse files Browse the repository at this point in the history
…ashboard-response

My Site Dashboard: cache the response
  • Loading branch information
leandroalonso authored Feb 14, 2022
2 parents 96c9f19 + 4d76493 commit 57adae5
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ final class BlogDashboardViewController: UIViewController {
func update(blog: Blog) {
self.blog = blog
viewModel.blog = blog
viewModel.applySnapshotForInitialData()
viewModel.loadCardsFromCache()
viewModel.loadCards()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Foundation

class BlogDashboardPersistence {
func persist(cards: NSDictionary, for wpComID: Int) {
do {
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = directory.appendingPathComponent(filename(for: wpComID))
let data = try JSONSerialization.data(withJSONObject: cards, options: [])
try data.write(to: fileURL, options: .atomic)
} catch {
// In case of an error, nothing is done
}
}

func getCards(for wpComID: Int) -> NSDictionary? {
do {
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = directory.appendingPathComponent(filename(for: wpComID))
let data = try Data(contentsOf: fileURL)
return try JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary
} catch {
return nil
}
}

private func filename(for blogID: Int) -> String {
"cards_\(blogID).json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,84 @@ import Foundation
import WordPressKit

class BlogDashboardService {
let remoteService: DashboardServiceRemote
private let remoteService: DashboardServiceRemote
private let persistence: BlogDashboardPersistence

init(managedObjectContext: NSManagedObjectContext, remoteService: DashboardServiceRemote? = nil) {
init(managedObjectContext: NSManagedObjectContext, remoteService: DashboardServiceRemote? = nil, persistence: BlogDashboardPersistence = BlogDashboardPersistence()) {
self.remoteService = remoteService ?? DashboardServiceRemote(wordPressComRestApi: WordPressComRestApi.defaultApi(in: managedObjectContext, localeKey: WordPressComRestApi.LocaleKeyV2))
self.persistence = persistence
}

func fetch(wpComID: Int, completion: @escaping (DashboardSnapshot) -> Void) {
/// Fetch cards from remote
func fetch(wpComID: Int, completion: @escaping (DashboardSnapshot) -> Void, failure: (() -> Void)? = nil) {
let cardsToFetch: [String] = DashboardCard.remoteCases.map { $0.rawValue }

remoteService.fetch(cards: cardsToFetch, forBlogID: wpComID, success: { [weak self] cards in

var snapshot = DashboardSnapshot()
self?.persistence.persist(cards: cards, for: wpComID)

DashboardCard.allCases.forEach { card in
guard let snapshot = self?.parse(cards) else {
return
}

completion(snapshot)

}, failure: { _ in
failure?()
})
}

if card.isRemote {
/// Fetch cards from local
func fetchLocal(wpComID: Int) -> DashboardSnapshot {
if let cards = persistence.getCards(for: wpComID) {
let snapshot = parse(cards)
return snapshot
}

if card == .posts,
let posts = cards[DashboardCard.posts.rawValue] as? NSDictionary,
let (sections, items) = self?.parsePostCard(posts) {
snapshot.appendSections(sections)
sections.enumerated().forEach { key, section in
snapshot.appendItems([items[key]], toSection: section)
}
} else {
return DashboardSnapshot()
}
}

if let viewModel = cards[card.rawValue] {
let section = DashboardCardSection(id: card.rawValue)
let item = DashboardCardModel(id: card, cellViewModel: viewModel as? NSDictionary)
private extension BlogDashboardService {
func parse(_ cards: NSDictionary) -> DashboardSnapshot {
var snapshot = DashboardSnapshot()

snapshot.appendSections([section])
snapshot.appendItems([item], toSection: section)
}
DashboardCard.allCases.forEach { card in

}
if card.isRemote {

if card == .posts,
let posts = cards[DashboardCard.posts.rawValue] as? NSDictionary {
let (sections, items) = parsePostCard(posts)
snapshot.appendSections(sections)
sections.enumerated().forEach { key, section in
snapshot.appendItems([items[key]], toSection: section)
}
} else {

let section = DashboardCardSection(id: card.rawValue)
let item = DashboardCardModel(id: card)
if let viewModel = cards[card.rawValue] {
let section = DashboardCardSection(id: card.rawValue)
let item = DashboardCardModel(id: card, cellViewModel: viewModel as? NSDictionary)

snapshot.appendSections([section])
snapshot.appendItems([item], toSection: section)
}

snapshot.appendSections([section])
snapshot.appendItems([item], toSection: section)
}

} else {
let section = DashboardCardSection(id: card.rawValue)
let item = DashboardCardModel(id: card)

snapshot.appendSections([section])
snapshot.appendItems([item], toSection: section)
}

completion(snapshot)
}, failure: { _ in
}

})
return snapshot
}
}

private extension BlogDashboardService {
/// Posts are a special case: they might not be a 1-1 relation
/// If the user has draft and scheduled posts, we show two cards
/// One for each. This function takes care of this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ class BlogDashboardViewModel {

/// Apply the initial configuration when the view loaded
func viewDidLoad() {
// This is necessary when using an IntrinsicCollectionView
// Otherwise, the collection view will never update its height
applySnapshotForInitialData()
loadCardsFromCache()
}

/// Call the API to return cards for the current blog
Expand All @@ -58,11 +56,17 @@ class BlogDashboardViewModel {
service.fetch(wpComID: dotComID, completion: { [weak self] snapshot in
self?.viewController?.stopLoading()
self?.apply(snapshot: snapshot)
}, failure: { [weak self] in
self?.viewController?.stopLoading()
})
}

func applySnapshotForInitialData() {
let snapshot = DashboardSnapshot()
func loadCardsFromCache() {
guard let dotComID = blog.dotComID?.intValue else {
return
}

let snapshot = service.fetchLocal(wpComID: dotComID)
apply(snapshot: snapshot)
}

Expand Down
10 changes: 10 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,8 @@
8BBBCE702717651200B277AC /* JetpackModuleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBBCE6F2717651200B277AC /* JetpackModuleHelper.swift */; };
8BBBCE712717651200B277AC /* JetpackModuleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBBCE6F2717651200B277AC /* JetpackModuleHelper.swift */; };
8BBBEBB224B8F8C0005E358E /* ReaderCardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBBEBB124B8F8C0005E358E /* ReaderCardTests.swift */; };
8BBC778B27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBC778A27B5531700DBA087 /* BlogDashboardPersistence.swift */; };
8BBC778C27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BBC778A27B5531700DBA087 /* BlogDashboardPersistence.swift */; };
8BC12F72231FEBA1004DDA72 /* PostCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC12F71231FEBA1004DDA72 /* PostCoordinatorTests.swift */; };
8BC12F7523201917004DDA72 /* PostService+MarkAsFailedAndDraftIfNeeded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC12F732320181E004DDA72 /* PostService+MarkAsFailedAndDraftIfNeeded.swift */; };
8BC12F7723201B86004DDA72 /* PostService+MarkAsFailedAndDraftIfNeededTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BC12F7623201B86004DDA72 /* PostService+MarkAsFailedAndDraftIfNeededTests.swift */; };
Expand All @@ -1490,6 +1492,7 @@
8BDC4C39249BA5CA00DE0A2D /* ReaderCSS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BDC4C38249BA5CA00DE0A2D /* ReaderCSS.swift */; };
8BE69512243E674300FF492F /* PrepublishingHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE69511243E674300FF492F /* PrepublishingHeaderViewTests.swift */; };
8BE7C84123466927006EDE70 /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE7C84023466927006EDE70 /* I18n.swift */; };
8BE9AB8827B6B5A300708E45 /* BlogDashboardPersistenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BE9AB8727B6B5A300708E45 /* BlogDashboardPersistenceTests.swift */; };
8BEE845A27B1DC9D0001A93C /* dashboard-200-with-drafts-and-scheduled.json in Resources */ = {isa = PBXBuildFile; fileRef = 8BEE845927B1DC9D0001A93C /* dashboard-200-with-drafts-and-scheduled.json */; };
8BEE846227B1E0540001A93C /* DashboardCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE845E27B1DE040001A93C /* DashboardCardSection.swift */; };
8BEE846327B1E0560001A93C /* DashboardCardSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEE845E27B1DE040001A93C /* DashboardCardSection.swift */; };
Expand Down Expand Up @@ -6124,6 +6127,7 @@
8BB185D424B66FE600A4CCE8 /* ReaderCard+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReaderCard+CoreDataClass.swift"; sourceTree = "<group>"; };
8BBBCE6F2717651200B277AC /* JetpackModuleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackModuleHelper.swift; sourceTree = "<group>"; };
8BBBEBB124B8F8C0005E358E /* ReaderCardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCardTests.swift; sourceTree = "<group>"; };
8BBC778A27B5531700DBA087 /* BlogDashboardPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersistence.swift; sourceTree = "<group>"; };
8BC12F71231FEBA1004DDA72 /* PostCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCoordinatorTests.swift; sourceTree = "<group>"; };
8BC12F732320181E004DDA72 /* PostService+MarkAsFailedAndDraftIfNeeded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostService+MarkAsFailedAndDraftIfNeeded.swift"; sourceTree = "<group>"; };
8BC12F7623201B86004DDA72 /* PostService+MarkAsFailedAndDraftIfNeededTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostService+MarkAsFailedAndDraftIfNeededTests.swift"; sourceTree = "<group>"; };
Expand All @@ -6149,6 +6153,7 @@
8BDC4C38249BA5CA00DE0A2D /* ReaderCSS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderCSS.swift; sourceTree = "<group>"; };
8BE69511243E674300FF492F /* PrepublishingHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PrepublishingHeaderViewTests.swift; path = WordPressTest/PrepublishingHeaderViewTests.swift; sourceTree = SOURCE_ROOT; };
8BE7C84023466927006EDE70 /* I18n.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = I18n.swift; sourceTree = "<group>"; };
8BE9AB8727B6B5A300708E45 /* BlogDashboardPersistenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogDashboardPersistenceTests.swift; sourceTree = "<group>"; };
8BEE845927B1DC9D0001A93C /* dashboard-200-with-drafts-and-scheduled.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "dashboard-200-with-drafts-and-scheduled.json"; sourceTree = "<group>"; };
8BEE845E27B1DE040001A93C /* DashboardCardSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCardSection.swift; sourceTree = "<group>"; };
8BEE846027B1DE0E0001A93C /* DashboardCardModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardCardModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -11374,6 +11379,7 @@
isa = PBXGroup;
children = (
8B6214E227B1B2F3001DF7B6 /* BlogDashboardService.swift */,
8BBC778A27B5531700DBA087 /* BlogDashboardPersistence.swift */,
);
path = Service;
sourceTree = "<group>";
Expand All @@ -11382,6 +11388,7 @@
isa = PBXGroup;
children = (
8B6214E527B1B446001DF7B6 /* BlogDashboardServiceTests.swift */,
8BE9AB8727B6B5A300708E45 /* BlogDashboardPersistenceTests.swift */,
);
path = Dashboard;
sourceTree = "<group>";
Expand Down Expand Up @@ -18140,6 +18147,7 @@
5D6C4B081B603E03005E3C43 /* WPContentSyncHelper.swift in Sources */,
73C8F06021BEED9100DDDF7E /* SiteAssemblyStep.swift in Sources */,
82C420761FE44BD900CFB15B /* SiteSettingsViewController+Swift.swift in Sources */,
8BBC778B27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */,
F5E29038243FAB0300C19CA5 /* FilterTableData.swift in Sources */,
E6A3384C1BB08E3F00371587 /* ReaderGapMarker.m in Sources */,
E1CFC1571E0AC8FF001DF9E9 /* Pattern.swift in Sources */,
Expand Down Expand Up @@ -19381,6 +19389,7 @@
D848CC1720FF38EA00A9038F /* FormattableCommentRangeTests.swift in Sources */,
246D0A0325E97D5D0028B83F /* Blog+ObjcTests.m in Sources */,
9A9D34FF2360A4E200BC95A3 /* StatsPeriodAsyncOperationTests.swift in Sources */,
8BE9AB8827B6B5A300708E45 /* BlogDashboardPersistenceTests.swift in Sources */,
B5EFB1C91B333C5A007608A3 /* NotificationSettingsServiceTests.swift in Sources */,
179501CD27A01D4100882787 /* PublicizeAuthorizationURLComponentsTests.swift in Sources */,
4089C51422371EE30031CE78 /* TodayStatsTests.swift in Sources */,
Expand Down Expand Up @@ -20404,6 +20413,7 @@
FABB24142602FC2C00C8785C /* ThemeBrowserSectionHeaderView.swift in Sources */,
FABB24152602FC2C00C8785C /* SiteIconPickerPresenter.swift in Sources */,
C79C307D26EA919F00E88514 /* ReferrerDetailsViewModel.swift in Sources */,
8BBC778C27B5531700DBA087 /* BlogDashboardPersistence.swift in Sources */,
FABB24172602FC2C00C8785C /* BlogSettings.swift in Sources */,
FABB24182602FC2C00C8785C /* WKWebView+UserAgent.swift in Sources */,
FABB24192602FC2C00C8785C /* JetpackCapabilitiesService.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import XCTest

@testable import WordPress

class BlogDashboardPersistenceTests: XCTestCase {
private var persistence: BlogDashboardPersistence!

override func setUp() {
super.setUp()

persistence = BlogDashboardPersistence()
}

func testSaveData() {
persistence.persist(cards: cardsResponse, for: 1234)

let directory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = directory.appendingPathComponent("cards_1234.json")
let data: Data = try! Data(contentsOf: fileURL)
let cardsDictionary = try! JSONSerialization.jsonObject(with: data, options: []) as! NSDictionary

XCTAssertEqual(cardsDictionary, cardsResponse)
}

func testGetCards() {
persistence.persist(cards: cardsResponse, for: 1235)

let persistedCards = persistence.getCards(for: 1235)

XCTAssertEqual(persistedCards, cardsResponse)
}
}

private extension BlogDashboardPersistenceTests {
var cardsResponse: NSDictionary {
let fileURL: URL = Bundle(for: BlogDashboardPersistenceTests.self).url(forResource: "dashboard-200-with-drafts-and-scheduled.json", withExtension: nil)!
let data: Data = try! Data(contentsOf: fileURL)
return try! JSONSerialization.jsonObject(with: data, options: []) as! NSDictionary
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import Nimble
class BlogDashboardServiceTests: XCTestCase {
private var service: BlogDashboardService!
private var remoteServiceMock: DashboardServiceRemoteMock!
private var persistenceMock: BlogDashboardPersistenceMock!

override func setUp() {
super.setUp()

remoteServiceMock = DashboardServiceRemoteMock()
service = BlogDashboardService(managedObjectContext: TestContextManager().newDerivedContext(), remoteService: remoteServiceMock)
persistenceMock = BlogDashboardPersistenceMock()
service = BlogDashboardService(managedObjectContext: TestContextManager().newDerivedContext(), remoteService: remoteServiceMock, persistence: persistenceMock)
}

func testCallServiceWithCorrectIDAndCards() {
Expand Down Expand Up @@ -118,8 +120,43 @@ class BlogDashboardServiceTests: XCTestCase {

waitForExpectations(timeout: 3, handler: nil)
}

func testPersistCardsResponse() {
let expect = expectation(description: "Parse todays stats")
remoteServiceMock.respondWith = .withDraftAndSchedulePosts

service.fetch(wpComID: 123456) { snapshot in
XCTAssertEqual(self.persistenceMock.didCallPersistWithCards,
self.dictionary(from: "dashboard-200-with-drafts-and-scheduled.json"))
XCTAssertEqual(self.persistenceMock.didCallPersistWithWpComID, 123456)

expect.fulfill()
}

waitForExpectations(timeout: 3, handler: nil)
}

func testFetchCardsFromPersistence() {
persistenceMock.respondWith = dictionary(from: "dashboard-200-with-drafts-and-scheduled.json")!

let snapshot = service.fetchLocal(wpComID: 123456)

let draftsSection = snapshot.sectionIdentifiers.first(where: { $0.subtype == "draft" })
let scheduledSection = snapshot.sectionIdentifiers.first(where: { $0.subtype == "scheduled" })
XCTAssertNotNil(draftsSection)
XCTAssertNotNil(scheduledSection)
XCTAssertEqual(persistenceMock.didCallGetCardsWithWpComID, 123456)
}

func dictionary(from file: String) -> NSDictionary? {
let fileURL: URL = Bundle(for: BlogDashboardServiceTests.self).url(forResource: file, withExtension: nil)!
let data: Data = try! Data(contentsOf: fileURL)
return try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary
}
}

// MARK: - Mocks

class DashboardServiceRemoteMock: DashboardServiceRemote {
enum Response: String {
case withDraftAndSchedulePosts = "dashboard-200-with-drafts-and-scheduled.json"
Expand All @@ -144,3 +181,22 @@ class DashboardServiceRemoteMock: DashboardServiceRemote {
}
}
}

class BlogDashboardPersistenceMock: BlogDashboardPersistence {
var didCallPersistWithCards: NSDictionary?
var didCallPersistWithWpComID: Int?

override func persist(cards: NSDictionary, for wpComID: Int) {
didCallPersistWithCards = cards
didCallPersistWithWpComID = wpComID
}

var didCallGetCardsWithWpComID: Int?
var respondWith: NSDictionary = [:]

override func getCards(for wpComID: Int) -> NSDictionary? {
didCallGetCardsWithWpComID = wpComID

return respondWith
}
}

0 comments on commit 57adae5

Please sign in to comment.