Skip to content

Commit b0dd1f5

Browse files
Merge pull request #17909 from wordpress-mobile/issue/17873-parse-dashboard-response
My Site Dashboard: handle local and remote cards
2 parents f1de7f6 + 478fe49 commit b0dd1f5

15 files changed

Lines changed: 587 additions & 100 deletions

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewController.swift

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import UIKit
22

3+
typealias DashboardCollectionViewCell = UICollectionViewCell & Reusable & BlogDashboardCardConfigurable
4+
5+
protocol BlogDashboardCardConfigurable {
6+
func configure(blog: Blog, viewController: BlogDashboardViewController?, dataModel: NSDictionary?)
7+
}
8+
39
final class BlogDashboardViewController: UIViewController {
410

511
var blog: Blog
@@ -57,8 +63,9 @@ final class BlogDashboardViewController: UIViewController {
5763
private func setupCollectionView() {
5864
collectionView.isScrollEnabled = false
5965
collectionView.backgroundColor = .listBackground
60-
collectionView.register(QuickLinksHostCell.self, forCellWithReuseIdentifier: QuickLinksHostCell.defaultReuseID)
61-
collectionView.register(DashboardPostsCardCell.self, forCellWithReuseIdentifier: DashboardPostsCardCell.defaultReuseID)
66+
DashboardCard.allCases.forEach {
67+
collectionView.register($0.cell, forCellWithReuseIdentifier: $0.cell.defaultReuseID)
68+
}
6269

6370
view.addSubview(collectionView)
6471
view.pinSubviewToAllEdges(collectionView)

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/BlogDashboardViewModel.swift

Lines changed: 0 additions & 94 deletions
This file was deleted.

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/DashboardPostsCardCell.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import UIKit
22

3-
class DashboardPostsCardCell: UICollectionViewCell, Reusable {
4-
func configure(_ viewController: UIViewController, blog: Blog) {
3+
class DashboardPostsCardCell: UICollectionViewCell, Reusable, BlogDashboardCardConfigurable {
4+
func configure(blog: Blog, viewController: BlogDashboardViewController?, dataModel: NSDictionary?) {
5+
guard let viewController = viewController, let dataModel = dataModel else {
6+
return
7+
}
8+
9+
let hasDrafts = dataModel["show_drafts"] as? Bool ?? false
10+
let hasScheduled = dataModel["show_scheduled"] as? Bool ?? false
11+
512
let postsViewController = PostsCardViewController(blog: blog)
13+
14+
if hasDrafts {
15+
postsViewController.status = .draft
16+
} else if hasScheduled {
17+
postsViewController.status = .scheduled
18+
}
19+
620
viewController.addChild(postsViewController)
721
contentView.addSubview(postsViewController.view)
822
postsViewController.view.translatesAutoresizingMaskIntoConstraints = false

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Posts/PostsCardViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import UIKit
1313
private var ghostableTableView: UITableView?
1414
private var errorView: DashboardCardInnerErrorView?
1515

16-
private let status: BasePost.Status = .draft
16+
var status: BasePost.Status = .draft
1717

1818
// TODO: add status as an init param
1919
@objc init(blog: Blog) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import UIKit
2+
3+
class DashboardStatsCardCell: UICollectionViewCell, Reusable, BlogDashboardCardConfigurable {
4+
func configure(blog: Blog, viewController: BlogDashboardViewController?, dataModel: NSDictionary?) {
5+
6+
}
7+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Foundation
2+
3+
/// Describes all the available cards.
4+
///
5+
/// Notice that the order here matters and it will take
6+
/// precedence over the backend.
7+
///
8+
/// If the card `isRemote` the `String` should match its
9+
/// identifier on the backend.
10+
enum DashboardCard: String, CaseIterable {
11+
case quickActions
12+
case posts
13+
case todaysStats = "todays_stats"
14+
15+
/// If the card is backed by API data
16+
var isRemote: Bool {
17+
switch self {
18+
case .quickActions:
19+
return false
20+
case .posts:
21+
return true
22+
case .todaysStats:
23+
return true
24+
}
25+
}
26+
27+
var cell: DashboardCollectionViewCell.Type {
28+
switch self {
29+
case .quickActions:
30+
return HostCollectionViewCell<QuickLinksView>.self
31+
case .posts:
32+
return DashboardPostsCardCell.self
33+
case .todaysStats:
34+
return DashboardStatsCardCell.self
35+
}
36+
}
37+
38+
/// All cards that are remote
39+
static var remoteCases: [DashboardCard] {
40+
return DashboardCard.allCases.filter { $0.isRemote }
41+
}
42+
43+
/// All cards that are local
44+
static var localCases: [DashboardCard] {
45+
return DashboardCard.allCases.filter { !$0.isRemote }
46+
}
47+
}

WordPress/Classes/ViewRelated/Blog/Blog Dashboard/HostCollectionViewCell.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,9 @@ class HostCollectionViewCell<Content>: UICollectionViewCell, Hostable where Cont
4747
}
4848

4949
extension HostCollectionViewCell: Reusable { }
50+
51+
extension HostCollectionViewCell: BlogDashboardCardConfigurable {
52+
func configure(blog: Blog, viewController: BlogDashboardViewController?, dataModel: NSDictionary?) {
53+
hostedView = QuickLinksView(title: "Quick Links") as? Content
54+
}
55+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Foundation
2+
import WordPressKit
3+
4+
class BlogDashboardService {
5+
let remoteService: DashboardServiceRemote
6+
7+
init(managedObjectContext: NSManagedObjectContext, remoteService: DashboardServiceRemote? = nil) {
8+
self.remoteService = remoteService ?? DashboardServiceRemote(wordPressComRestApi: WordPressComRestApi.defaultApi(in: managedObjectContext, localeKey: WordPressComRestApi.LocaleKeyV2))
9+
}
10+
11+
func fetch(wpComID: Int, completion: @escaping (DashboardSnapshot) -> Void) {
12+
let cardsToFetch: [String] = DashboardCard.remoteCases.map { $0.rawValue }
13+
14+
remoteService.fetch(cards: cardsToFetch, forBlogID: wpComID, success: { [weak self] cards in
15+
16+
var snapshot = DashboardSnapshot()
17+
18+
DashboardCard.allCases.forEach { card in
19+
20+
if card.isRemote {
21+
22+
if card == .posts,
23+
let posts = cards[DashboardCard.posts.rawValue] as? NSDictionary,
24+
let (sections, items) = self?.parsePostCard(posts) {
25+
snapshot.appendSections(sections)
26+
sections.enumerated().forEach { key, section in
27+
snapshot.appendItems([items[key]], toSection: section)
28+
}
29+
} else {
30+
31+
if let viewModel = cards[card.rawValue] {
32+
let section = DashboardCardSection(id: card.rawValue)
33+
let item = DashboardCardModel(id: card, cellViewModel: viewModel as? NSDictionary)
34+
35+
snapshot.appendSections([section])
36+
snapshot.appendItems([item], toSection: section)
37+
}
38+
39+
}
40+
41+
} else {
42+
let section = DashboardCardSection(id: card.rawValue)
43+
let item = DashboardCardModel(id: card)
44+
45+
snapshot.appendSections([section])
46+
snapshot.appendItems([item], toSection: section)
47+
}
48+
49+
}
50+
51+
completion(snapshot)
52+
}, failure: { _ in
53+
54+
})
55+
}
56+
}
57+
58+
private extension BlogDashboardService {
59+
/// Posts are a special case: they might not be a 1-1 relation
60+
/// If the user has draft and scheduled posts, we show two cards
61+
/// One for each. This function takes care of this
62+
func parsePostCard(_ posts: NSDictionary) -> ([DashboardCardSection], [DashboardCardModel]) {
63+
var sections: [DashboardCardSection] = []
64+
var items: [DashboardCardModel] = []
65+
66+
let draftsCount = (posts["draft"] as? Array<Any>)?.count ?? 0
67+
let scheduledCount = (posts["scheduled"] as? Array<Any>)?.count ?? 0
68+
69+
let hasDrafts = draftsCount > 0
70+
let hasScheduled = scheduledCount > 0
71+
72+
if hasDrafts && hasScheduled {
73+
var draft = posts.copy() as? [String: Any]
74+
draft?["show_drafts"] = true
75+
draft?["show_scheduled"] = false
76+
sections.append(DashboardCardSection(id: "posts", subtype: "draft"))
77+
items.append(DashboardCardModel(id: .posts, cellViewModel: draft as NSDictionary?))
78+
79+
var scheduled = posts.copy() as? [String: Any]
80+
scheduled?["show_drafts"] = false
81+
scheduled?["show_scheduled"] = true
82+
sections.append(DashboardCardSection(id: "posts", subtype: "scheduled"))
83+
items.append(DashboardCardModel(id: .posts, cellViewModel: scheduled as NSDictionary?))
84+
} else {
85+
var postsWithFlags = posts.copy() as? [String: Any]
86+
postsWithFlags?["show_drafts"] = hasDrafts
87+
postsWithFlags?["show_scheduled"] = hasScheduled
88+
89+
sections.append(DashboardCardSection(id: "posts"))
90+
items.append(DashboardCardModel(id: .posts, cellViewModel: postsWithFlags as NSDictionary?))
91+
}
92+
93+
return (sections, items)
94+
}
95+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import Foundation
2+
import UIKit
3+
import CoreData
4+
5+
typealias DashboardSnapshot = NSDiffableDataSourceSnapshot<DashboardCardSection, DashboardCardModel>
6+
typealias DashboardDataSource = UICollectionViewDiffableDataSource<DashboardCardSection, DashboardCardModel>
7+
8+
class BlogDashboardViewModel {
9+
private weak var viewController: BlogDashboardViewController?
10+
11+
enum Section: CaseIterable {
12+
case quickLinks
13+
case posts
14+
}
15+
16+
typealias QuickLinksHostCell = HostCollectionViewCell<QuickLinksView>
17+
18+
private let managedObjectContext: NSManagedObjectContext
19+
private let blog: Blog
20+
21+
private lazy var service: BlogDashboardService = {
22+
return BlogDashboardService(managedObjectContext: managedObjectContext)
23+
}()
24+
25+
private lazy var dataSource: DashboardDataSource? = {
26+
guard let viewController = viewController else {
27+
return nil
28+
}
29+
30+
return DashboardDataSource(collectionView: viewController.collectionView) { [unowned self] collectionView, indexPath, identifier in
31+
32+
let cellType = identifier.id.cell
33+
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.defaultReuseID, for: indexPath)
34+
35+
if let cellConfigurable = cell as? BlogDashboardCardConfigurable {
36+
cellConfigurable.configure(blog: blog, viewController: viewController, dataModel: identifier.cellViewModel)
37+
}
38+
39+
return cell
40+
41+
}
42+
}()
43+
44+
init(viewController: BlogDashboardViewController, managedObjectContext: NSManagedObjectContext = ContextManager.shared.mainContext, blog: Blog) {
45+
self.viewController = viewController
46+
self.managedObjectContext = managedObjectContext
47+
self.blog = blog
48+
}
49+
50+
/// Call the API to return cards for the current blog
51+
func start() {
52+
guard let dotComID = blog.dotComID?.intValue else {
53+
return
54+
}
55+
56+
viewController?.showLoading()
57+
applySnapshotForInitialData()
58+
59+
service.fetch(wpComID: dotComID, completion: { [weak self] snapshot in
60+
self?.viewController?.stopLoading()
61+
self?.apply(snapshot: snapshot)
62+
})
63+
}
64+
}
65+
66+
// MARK: - Private methods
67+
68+
private extension BlogDashboardViewModel {
69+
// This is necessary when using an IntrinsicCollectionView
70+
// Otherwise, the collection view will never update its height
71+
func applySnapshotForInitialData() {
72+
let snapshot = DashboardSnapshot()
73+
dataSource?.apply(snapshot, animatingDifferences: false)
74+
}
75+
76+
func apply(snapshot: DashboardSnapshot) {
77+
dataSource?.apply(snapshot, animatingDifferences: false)
78+
}
79+
}

0 commit comments

Comments
 (0)