Skip to content

Commit c0a2a49

Browse files
authored
Merge pull request #21482 from wordpress-mobile/issue/21480-reader-site-header-ui
Reader: Create new site header for the reader
2 parents 6162280 + 40d1e4d commit c0a2a49

File tree

4 files changed

+254
-9
lines changed

4 files changed

+254
-9
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import SwiftUI
2+
3+
class ReaderSiteHeaderView: UIView, ReaderStreamHeader {
4+
5+
weak var delegate: ReaderStreamHeaderDelegate?
6+
7+
private lazy var headerViewModel: ReaderSiteHeaderViewModel = {
8+
ReaderSiteHeaderViewModel(onFollowTap: { [weak self] completion in
9+
guard let self else {
10+
return
11+
}
12+
self.delegate?.handleFollowActionForHeader(self, completion: completion)
13+
})
14+
}()
15+
16+
init() {
17+
super.init(frame: .zero)
18+
backgroundColor = .secondarySystemGroupedBackground
19+
setupHeader()
20+
}
21+
22+
required init?(coder: NSCoder) {
23+
fatalError("init(coder:) has not been implemented")
24+
}
25+
26+
func enableLoggedInFeatures(_ enable: Bool) {
27+
headerViewModel.isFollowHidden = !enable
28+
}
29+
30+
func configureHeader(_ topic: ReaderAbstractTopic) {
31+
guard let siteTopic = topic as? ReaderSiteTopic else {
32+
assertionFailure("This header should only be used for site topics.")
33+
return
34+
}
35+
headerViewModel.imageUrl = siteTopic.siteBlavatar
36+
headerViewModel.title = siteTopic.title
37+
headerViewModel.siteUrl = URL(string: siteTopic.siteURL)?.host ?? ""
38+
headerViewModel.siteDetails = siteTopic.siteDescription
39+
headerViewModel.postCount = siteTopic.postCount.doubleValue.abbreviatedString()
40+
headerViewModel.followerCount = siteTopic.subscriberCount.doubleValue.abbreviatedString()
41+
headerViewModel.isFollowingSite = siteTopic.following
42+
}
43+
44+
private func setupHeader() {
45+
weak var weakSelf = self
46+
let header = ReaderSiteHeader(viewModel: weakSelf?.headerViewModel ?? ReaderSiteHeaderViewModel())
47+
let view = UIView.embedSwiftUIView(header)
48+
addSubview(view)
49+
pinSubviewToAllEdges(view)
50+
}
51+
52+
}
53+
54+
// MARK: - ReaderSiteHeader
55+
56+
struct ReaderSiteHeader: View {
57+
58+
@StateObject var viewModel: ReaderSiteHeaderViewModel
59+
60+
var body: some View {
61+
VStack(alignment: .leading, spacing: 12) {
62+
AsyncImage(url: URL(string: viewModel.imageUrl)) { phase in
63+
switch phase {
64+
case .success(let image):
65+
image
66+
.resizable()
67+
.frame(width: 72.0, height: 72.0)
68+
.clipShape(Circle())
69+
default:
70+
Image(Constants.defaultSiteImage)
71+
.resizable()
72+
.frame(width: 72.0, height: 72.0)
73+
.clipShape(Circle())
74+
}
75+
}
76+
VStack(alignment: .leading, spacing: 4) {
77+
Text(viewModel.title)
78+
.font(Font(WPStyleGuide.serifFontForTextStyle(.title2, fontWeight: .semibold)))
79+
Text(viewModel.siteUrl)
80+
.font(.subheadline)
81+
}
82+
if !viewModel.siteDetails.isEmpty {
83+
Text(viewModel.siteDetails)
84+
.lineLimit(3)
85+
.font(.subheadline)
86+
.foregroundColor(.secondary)
87+
}
88+
countsDisplay
89+
if !viewModel.isFollowHidden {
90+
followButton
91+
.padding(.top, 4)
92+
}
93+
}
94+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
95+
.padding(EdgeInsets(top: 8, leading: 16, bottom: 16, trailing: 16))
96+
.background(Color(UIColor.listForeground))
97+
}
98+
99+
private var countsDisplay: some View {
100+
let countsString = String(format: Constants.countsFormat, viewModel.postCount, viewModel.followerCount)
101+
let stringItems = countsString.components(separatedBy: " ")
102+
103+
return stringItems.reduce(Text(""), {
104+
var text = Text($1)
105+
if $1 == viewModel.postCount || $1 == viewModel.followerCount {
106+
text = text.font(.subheadline)
107+
} else {
108+
text = text.font(.subheadline).foregroundColor(.secondary)
109+
}
110+
return $0 + text + Text(" ")
111+
})
112+
}
113+
114+
@ViewBuilder
115+
private var followButton: some View {
116+
if viewModel.isFollowingSite {
117+
Button {
118+
viewModel.updateFollowStatus()
119+
} label: {
120+
Image(uiImage: Constants.followingIcon ?? UIImage())
121+
.padding(.leading, -2.0)
122+
.padding(.trailing, 2.0)
123+
Text(WPStyleGuide.FollowButton.Text.followingStringForDisplay)
124+
.padding(.leading, 2.0)
125+
.padding(.trailing, -2.0)
126+
.foregroundColor(.secondary)
127+
.font(.callout)
128+
}
129+
.disabled(!viewModel.isFollowEnabled)
130+
.padding(.horizontal, 12.0)
131+
.padding(.vertical, 6.0)
132+
.overlay(
133+
RoundedRectangle(cornerRadius: 4)
134+
.stroke(Color(UIColor.primaryButtonBorder), lineWidth: 1)
135+
)
136+
} else {
137+
Button {
138+
viewModel.updateFollowStatus()
139+
} label: {
140+
Image(uiImage: Constants.followIcon ?? UIImage())
141+
.padding(.leading, -2.0)
142+
.padding(.trailing, 2.0)
143+
Text(WPStyleGuide.FollowButton.Text.followStringForDisplay)
144+
.padding(.leading, 2.0)
145+
.padding(.trailing, -2.0)
146+
.foregroundColor(.white)
147+
.font(.callout.weight(.semibold))
148+
}
149+
.disabled(!viewModel.isFollowEnabled)
150+
.padding(.horizontal, 12.0)
151+
.padding(.vertical, 6.0)
152+
.background(Color(UIColor.primary))
153+
.cornerRadius(4)
154+
}
155+
}
156+
157+
struct Constants {
158+
static let defaultSiteImage = "blavatar-default"
159+
static let iconSide = WPStyleGuide.fontSizeForTextStyle(.callout)
160+
static let followIconSize = CGSize(width: iconSide, height: iconSide)
161+
static let followIcon = UIImage.gridicon(.readerFollow, size: followIconSize).imageWithTintColor(.white)
162+
static let followingIcon = UIImage.gridicon(.readerFollowing, size: followIconSize).imageWithTintColor(.buttonIcon)
163+
static let countsFormat = NSLocalizedString("reader.site.header.counts",
164+
value: "%1$@ posts • %2$@ followers",
165+
comment: "The formatted number of posts and followers for a site. " +
166+
"'%1$@' is a placeholder for the site post count. " +
167+
"'%2$@' is a placeholder for the site follower count. " +
168+
"Example: `5,000 posts • 10M followers`")
169+
}
170+
171+
}
172+
173+
// MARK: - ReaderSiteHeaderViewModel
174+
175+
class ReaderSiteHeaderViewModel: ObservableObject {
176+
177+
@Published var imageUrl: String
178+
@Published var title: String
179+
@Published var siteUrl: String
180+
@Published var siteDetails: String
181+
@Published var postCount: String
182+
@Published var followerCount: String
183+
@Published var isFollowingSite: Bool
184+
@Published var isFollowHidden: Bool
185+
@Published var isFollowEnabled: Bool
186+
187+
private let onFollowTap: (_ completion: @escaping () -> Void) -> Void
188+
189+
init(imageUrl: String = "",
190+
title: String = "",
191+
siteUrl: String = "",
192+
siteDetails: String = "",
193+
postCount: String = "",
194+
followerCount: String = "",
195+
isFollowingSite: Bool = false,
196+
isFollowHidden: Bool = false,
197+
isFollowEnabled: Bool = true,
198+
onFollowTap: @escaping (_ completion: @escaping () -> Void) -> Void = { _ in }) {
199+
self.imageUrl = imageUrl
200+
self.title = title
201+
self.siteUrl = siteUrl
202+
self.siteDetails = siteDetails
203+
self.postCount = postCount
204+
self.followerCount = followerCount
205+
self.isFollowingSite = isFollowingSite
206+
self.isFollowHidden = isFollowHidden
207+
self.isFollowEnabled = isFollowEnabled
208+
self.onFollowTap = onFollowTap
209+
}
210+
211+
func updateFollowStatus() {
212+
isFollowEnabled = false
213+
onFollowTap { [weak self] in
214+
self?.isFollowEnabled = true
215+
}
216+
}
217+
218+
}

WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController+Helper.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ extension ReaderStreamViewController {
4747
}
4848

4949
if ReaderHelpers.isTopicSite(topic) && !isContentFiltered {
50-
return Bundle.main.loadNibNamed("ReaderSiteStreamHeader", owner: nil, options: nil)?.first as? ReaderSiteStreamHeader
50+
if FeatureFlag.readerImprovements.enabled {
51+
return ReaderSiteHeaderView()
52+
} else {
53+
return Bundle.main.loadNibNamed("ReaderSiteStreamHeader", owner: nil, options: nil)?.first as? ReaderSiteStreamHeader
54+
}
5155
}
5256

5357
return nil

WordPress/Classes/ViewRelated/Reader/ReaderStreamViewController.swift

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ import Combine
216216

217217
private var readerTopicChangesObserver: AnyCancellable?
218218

219+
private weak var streamHeader: ReaderStreamHeader?
220+
219221
// MARK: - Factory Methods
220222

221223
/// Convenience method for instantiating an instance of ReaderStreamViewController
@@ -591,23 +593,36 @@ import Combine
591593
return
592594
}
593595

596+
// The container view is so that the header respects the safe area boundaries and expands
597+
// the header's background color to the screen's edges.
598+
let containerView = UIView()
599+
containerView.translatesAutoresizingMaskIntoConstraints = false
600+
containerView.backgroundColor = header.backgroundColor
601+
header.translatesAutoresizingMaskIntoConstraints = false
602+
containerView.addSubview(header)
603+
594604
if let tableHeaderView = tableView.tableHeaderView {
595-
header.isHidden = tableHeaderView.isHidden
605+
containerView.isHidden = tableHeaderView.isHidden
596606
}
597607

598-
tableView.tableHeaderView = header
608+
tableView.tableHeaderView = containerView
609+
streamHeader = header as? ReaderStreamHeader
599610

600611
// This feels somewhat hacky, but it is the only way I found to insert a stack view into the header without breaking the autolayout constraints.
601-
let centerConstraint = header.centerXAnchor.constraint(equalTo: tableView.centerXAnchor)
602-
let topConstraint = header.topAnchor.constraint(equalTo: tableView.topAnchor)
603-
let headerWidthConstraint = header.widthAnchor.constraint(equalTo: tableView.widthAnchor)
612+
let centerConstraint = containerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor)
613+
let topConstraint = containerView.topAnchor.constraint(equalTo: tableView.topAnchor)
614+
let headerWidthConstraint = containerView.widthAnchor.constraint(equalTo: tableView.widthAnchor)
604615
headerWidthConstraint.priority = UILayoutPriority(999)
605616
centerConstraint.priority = UILayoutPriority(999)
606617

607618
NSLayoutConstraint.activate([
608619
centerConstraint,
609620
headerWidthConstraint,
610-
topConstraint
621+
topConstraint,
622+
header.topAnchor.constraint(equalTo: containerView.topAnchor),
623+
header.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
624+
header.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
625+
header.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
611626
])
612627
tableView.tableHeaderView?.layoutIfNeeded()
613628
tableView.tableHeaderView = tableView.tableHeaderView
@@ -689,6 +704,8 @@ import Combine
689704
return
690705
}
691706
title = NSLocalizedString("Topic", comment: "Topic page title")
707+
} else if FeatureFlag.readerImprovements.enabled && ReaderHelpers.topicType(topic) == .site {
708+
title = ""
692709
} else {
693710
title = topic.title
694711
}
@@ -839,10 +856,10 @@ import Combine
839856
assertionFailure("A reader topic is required")
840857
return
841858
}
842-
guard let header = tableView.tableHeaderView as? ReaderStreamHeader else {
859+
guard let streamHeader else {
843860
return
844861
}
845-
header.configureHeader(topic)
862+
streamHeader.configureHeader(topic)
846863
}
847864

848865
func showManageSites(animated: Bool = true) {

WordPress/WordPress.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,8 @@
20692069
83A1B19E28AFE86A00E737AC /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D690151F828FF000200E30 /* FeatureFlag.swift */; };
20702070
83A1B1A028AFE89700E737AC /* AppConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA25F9FD2609AA830005E08F /* AppConfiguration.swift */; };
20712071
83A1B1A328AFE89F00E737AC /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1D690141F828FF000200E30 /* BuildConfiguration.swift */; };
2072+
83A337A12A9FA525009ED60C /* ReaderSiteHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A337A02A9FA525009ED60C /* ReaderSiteHeaderView.swift */; };
2073+
83A337A22A9FA525009ED60C /* ReaderSiteHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A337A02A9FA525009ED60C /* ReaderSiteHeaderView.swift */; };
20722074
83B1D037282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */; };
20732075
83B1D038282C62620061D911 /* BloggingPromptsAttribution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */; };
20742076
83BFAE482A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83BFAE472A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift */; };
@@ -7398,6 +7400,7 @@
73987400
83914BD32A2EA03A0017A588 /* PostSettingsViewController+JetpackSocial.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PostSettingsViewController+JetpackSocial.swift"; sourceTree = "<group>"; };
73997401
839435922847F2200019A94F /* WordPress 143.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "WordPress 143.xcdatamodel"; sourceTree = "<group>"; };
74007402
839B150A2795DEE0009F5E77 /* UIView+Margins.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Margins.swift"; sourceTree = "<group>"; };
7403+
83A337A02A9FA525009ED60C /* ReaderSiteHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSiteHeaderView.swift; sourceTree = "<group>"; };
74017404
83B1D036282C62620061D911 /* BloggingPromptsAttribution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloggingPromptsAttribution.swift; sourceTree = "<group>"; };
74027405
83BFAE472A6EBF1F00C7B683 /* DashboardJetpackSocialCardCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardJetpackSocialCardCell.swift; sourceTree = "<group>"; };
74037406
83BFAE4F2A6EBF9900C7B683 /* DashboardJetpackSocialCardCellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DashboardJetpackSocialCardCellTests.swift; sourceTree = "<group>"; };
@@ -16947,6 +16950,7 @@
1694716950
children = (
1694816951
E6D2E1681B8AAD9B0000ED14 /* ReaderListStreamHeader.swift */,
1694916952
E6D2E1621B8AAA340000ED14 /* ReaderListStreamHeader.xib */,
16953+
83A337A02A9FA525009ED60C /* ReaderSiteHeaderView.swift */,
1695016954
E6D2E1641B8AAD7E0000ED14 /* ReaderSiteStreamHeader.swift */,
1695116955
E6D2E15E1B8A9C830000ED14 /* ReaderSiteStreamHeader.xib */,
1695216956
E6D2E16B1B8B423B0000ED14 /* ReaderStreamHeader.swift */,
@@ -21015,6 +21019,7 @@
2101521019
B5015C581D4FDBB300C9449E /* NotificationActionsService.swift in Sources */,
2101621020
3F2ABE1A2770EF3E005D8916 /* Blog+VideoLimits.swift in Sources */,
2101721021
8C6A22E425783D2000A79950 /* JetpackScanService.swift in Sources */,
21022+
83A337A12A9FA525009ED60C /* ReaderSiteHeaderView.swift in Sources */,
2101821023
FA6402D129C325C1007A235C /* MovedToJetpackEventsTracker.swift in Sources */,
2101921024
F48D44BA2989A58C0051EAA6 /* ReaderSiteService.swift in Sources */,
2102021025
F5E63129243BC8190088229D /* FilterSheetView.swift in Sources */,
@@ -24986,6 +24991,7 @@
2498624991
8B55F9CA2614D8BC007D618E /* RoundRectangleView.swift in Sources */,
2498724992
FABB24E62602FC2C00C8785C /* ReaderSiteSearchViewController.swift in Sources */,
2498824993
F4D82972293109A600038726 /* DashboardMigrationSuccessCell+Jetpack.swift in Sources */,
24994+
83A337A22A9FA525009ED60C /* ReaderSiteHeaderView.swift in Sources */,
2498924995
FABB24E82602FC2C00C8785C /* ReaderPost.m in Sources */,
2499024996
FABB24E92602FC2C00C8785C /* MediaLibraryMediaPickingCoordinator.swift in Sources */,
2499124997
CECEEB562823164800A28ADE /* MediaCacheSettingsViewController.swift in Sources */,

0 commit comments

Comments
 (0)