Skip to content

Commit 5a19927

Browse files
authored
Reader: Safe Area and Empty State View (#23755)
* Fix an issue with stream not adjusting insets due to the lack of child-parent configuration: * Replae NoResultsVC with EmptyStateView * EmptyStateView to update for dynamic type * Remove redundant tableView.contentInsetAdjustmentBehavior = .always * Disable Reader tests for now
1 parent b13db78 commit 5a19927

File tree

6 files changed

+157
-237
lines changed

6 files changed

+157
-237
lines changed

Modules/Sources/WordPressUI/Views/EmptyStateView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ public struct EmptyStateView<Label: View, Description: View, Actions: View>: Vie
44
@ViewBuilder let label: () -> Label
55
@ViewBuilder var description: () -> Description
66
@ViewBuilder var actions: () -> Actions
7+
8+
@ScaledMetric(relativeTo: .title) var maxWidthCompact = 320
9+
@ScaledMetric(relativeTo: .title) var maxWidthRegular = 420
710
@Environment(\.horizontalSizeClass) var horizontalSizeClass
811

912
public init(
@@ -30,7 +33,8 @@ public struct EmptyStateView<Label: View, Description: View, Actions: View>: Vie
3033
}
3134
actions()
3235
}
33-
.frame(maxWidth: horizontalSizeClass == .compact ? 300 : 420)
36+
.frame(maxWidth: horizontalSizeClass == .compact ? maxWidthCompact : maxWidthRegular)
37+
.padding()
3438
}
3539
}
3640

@@ -85,6 +89,5 @@ private struct EmptyStateViewLabelStyle: LabelStyle {
8589
Text("Create Tag")
8690
}
8791
.buttonStyle(.borderedProminent)
88-
8992
}
9093
}

WordPress/Classes/Extensions/NoResultsViewController+FollowedSites.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,7 @@ extension NoResultsViewController {
3838
controller.labelStackViewSpacing = 8
3939
controller.labelButtonStackViewSpacing = 18
4040
controller.loadViewIfNeeded()
41-
controller.setupReaderButtonStyles()
4241

4342
return controller
4443
}
4544
}
46-
47-
extension NoResultsViewController {
48-
49-
func setupReaderButtonStyles() {
50-
actionButton.primaryNormalBackgroundColor = .label
51-
actionButton.primaryTitleColor = .systemBackground
52-
actionButton.primaryHighlightBackgroundColor = .label
53-
}
54-
55-
}
Lines changed: 122 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import Foundation
2+
import SwiftUI
3+
import WordPressUI
4+
5+
// MARK: - ReaderHeader
26

37
extension ReaderStreamViewController {
48
// Convenience type for Reader's headers
59
typealias ReaderHeader = UIView & ReaderStreamHeader
610

7-
// A simple struct defining a title and message for use with a NoResultsViewController
8-
public struct NoResultsResponse {
9-
var title: String
10-
var message: String
11-
}
12-
1311
func headerForStream(_ topic: ReaderAbstractTopic?, isLoggedIn: Bool, container: UITableViewController) -> UIView? {
1412
if let topic, ReaderHelpers.topicIsFollowing(topic) {
1513
return ReaderHeaderView.makeForFollowing()
@@ -44,19 +42,26 @@ extension ReaderStreamViewController {
4442
}
4543
return nil
4644
}
45+
}
4746

48-
static let defaultResponse = NoResultsResponse(
49-
title: NSLocalizedString("No recent posts", comment: "A message title"),
50-
message: NSLocalizedString("No posts have been made recently", comment: "A default message shown when the reader can find no post to display"))
51-
52-
/// Returns a NoResultsResponse instance appropriate for the specified ReaderTopic
53-
///
54-
/// - Parameter topic: A ReaderTopic.
55-
///
56-
/// - Returns: An NoResultsResponse instance.
57-
///
58-
public class func responseForNoResults(_ topic: ReaderAbstractTopic) -> NoResultsResponse {
59-
// if following
47+
// MARK: - EmptyStateView (ReaderAbstractTopic)
48+
49+
extension ReaderStreamViewController {
50+
func makeEmptyStateView(for topic: ReaderAbstractTopic) -> UIView {
51+
let response = ReaderStreamViewController.responseForNoResults(topic)
52+
return UIHostingView(view: EmptyStateView(
53+
response.title,
54+
image: "wp-illustration-reader-empty",
55+
description: response.message
56+
))
57+
}
58+
59+
private struct NoResultsResponse {
60+
var title: String
61+
var message: String
62+
}
63+
64+
private class func responseForNoResults(_ topic: ReaderAbstractTopic) -> NoResultsResponse {
6065
if ReaderHelpers.topicIsFollowing(topic) {
6166
return NoResultsResponse(
6267
title: NSLocalizedString("Welcome to the Reader", comment: "A message title"),
@@ -67,24 +72,18 @@ extension ReaderStreamViewController {
6772
)
6873
)
6974
}
70-
71-
// if liked
7275
if ReaderHelpers.topicIsLiked(topic) {
7376
return NoResultsResponse(
7477
title: NSLocalizedString("Nothing liked yet", comment: "A message title"),
7578
message: NSLocalizedString("Posts that you like will appear here.", comment: "A message explaining the Posts I Like feature in the reader")
7679
)
7780
}
78-
79-
// if tag
8081
if ReaderHelpers.isTopicTag(topic) {
8182
return NoResultsResponse(
8283
title: NSLocalizedString("No recent posts", comment: "A message title"),
8384
message: NSLocalizedString("No posts have been made recently with this tag.", comment: "Message shown whent the reader finds no posts for the chosen tag")
8485
)
8586
}
86-
87-
// if site (blog)
8887
if ReaderHelpers.isTopicSite(topic) {
8988
return NoResultsResponse(
9089
title: NSLocalizedString("No posts", comment: "A message title"),
@@ -95,8 +94,6 @@ extension ReaderStreamViewController {
9594
)
9695
)
9796
}
98-
99-
// if list
10097
if ReaderHelpers.isTopicList(topic) {
10198
return NoResultsResponse(
10299
title: NSLocalizedString("No recent posts", comment: "A message title"),
@@ -107,42 +104,105 @@ extension ReaderStreamViewController {
107104
)
108105
)
109106
}
110-
111-
// if search topic
112107
if ReaderHelpers.isTopicSearchTopic(topic) {
113108
let message = NSLocalizedString("No posts found matching %@ in your language.", comment: "Message shown when the reader finds no posts for the specified search phrase. The %@ is a placeholder for the search phrase.")
114109
return NoResultsResponse(
115110
title: NSLocalizedString("No posts found", comment: "A message title"),
116111
message: NSString(format: message as NSString, topic.title) as String
117112
)
118113
}
119-
120-
// Default message
121114
return defaultResponse
122115
}
116+
117+
private static let defaultResponse = NoResultsResponse(
118+
title: NSLocalizedString("No recent posts", comment: "A message title"),
119+
message: NSLocalizedString("No posts have been made recently", comment: "A default message shown when the reader can find no post to display")
120+
)
123121
}
124122

125-
// MARK: - No Results for saved posts
123+
// MARK: - EmptyStateView (EmptyStateViewType)
124+
126125
extension ReaderStreamViewController {
126+
enum EmptyStateViewType {
127+
case discover
128+
case noSavedPosts
129+
case noFollowedSites
130+
case noConnection
131+
case steamLoadingFailed
132+
}
127133

128-
func configureNoResultsViewForSavedPosts() {
134+
func makeEmptyStateView(_ type: EmptyStateViewType) -> UIView {
135+
UIHostingView(view: _makeEmptyStateView(type))
136+
}
129137

130-
let noResultsResponse = NoResultsResponse(title: NSLocalizedString("No saved posts",
131-
comment: "Message displayed in Reader Saved Posts view if a user hasn't yet saved any posts."),
132-
message: NSLocalizedString("Tap [bookmark-outline] to save a post to your list.",
133-
comment: "A hint displayed in the Saved Posts section of the Reader. The '[bookmark-outline]' placeholder will be replaced by an icon at runtime – please leave that string intact."))
138+
@ViewBuilder
139+
private func _makeEmptyStateView(_ type: EmptyStateViewType) -> some View {
140+
switch type {
141+
case .steamLoadingFailed:
142+
EmptyStateView(
143+
ResultsStatusText.loadingErrorTitle,
144+
systemImage: "exclamationmark.circle",
145+
description: ResultsStatusText.loadingErrorMessage
146+
)
147+
case .noSavedPosts:
148+
EmptyStateView(label: {
149+
Label(NSLocalizedString("No saved posts", comment: "Message displayed in Reader Saved Posts view if a user hasn't yet saved any posts."), image: "wp-illustration-reader-empty")
150+
}, description: {
151+
// Had to use UIKit because Text(AttributedString()) won't render the attachment
152+
HostedAttributedLabel(text: self.makeSavedPostsEmptyViewDescription())
153+
.fixedSize()
154+
}, actions: {
155+
EmptyView()
156+
})
157+
case .discover:
158+
EmptyStateView(
159+
ReaderStreamViewController.defaultResponse.title,
160+
image: "wp-illustration-reader-empty",
161+
description: ReaderStreamViewController.defaultResponse.message
162+
)
163+
case .noConnection:
164+
EmptyStateView(
165+
ResultsStatusText.noConnectionTitle,
166+
systemImage: "network.slash",
167+
description: noConnectionMessage()
168+
)
169+
case .noFollowedSites:
170+
EmptyStateView(label: {
171+
Label(Strings.noFollowedSitesTitle, systemImage: "checkmark.circle")
172+
}, description: {
173+
Text(Strings.noFollowedSitesSubtitle)
174+
}, actions: {
175+
Button(Strings.noFollowedSitesButtonTitle) {
176+
RootViewCoordinator.sharedPresenter.showReader(path: .discover)
177+
}
178+
.buttonStyle(.primary)
179+
})
180+
}
181+
}
134182

135-
var messageText = NSMutableAttributedString(string: noResultsResponse.message)
183+
private func makeSavedPostsEmptyViewDescription() -> NSAttributedString {
184+
let details = NSLocalizedString("Tap [bookmark-outline] to save a post to your list.", comment: "A hint displayed in the Saved Posts section of the Reader. The '[bookmark-outline]' placeholder will be replaced by an icon at runtime – please leave that string intact.")
185+
let string = NSMutableAttributedString(string: details, attributes: [
186+
.font: UIFont.preferredFont(forTextStyle: .subheadline)
187+
])
188+
let icon = UIImage.gridicon(.bookmarkOutline, size: CGSize(width: 18, height: 18))
189+
string.replace("[bookmark-outline]", with: icon)
190+
string.addAttribute(.foregroundColor, value: UIColor.secondaryLabel, range: NSRange(location: 0, length: string.length))
191+
return string
192+
}
193+
}
136194

137-
// Get attributed string styled for No Results so it gets the correct font attributes added to it.
138-
// The font is used by the attributed string `replace(_:with:)` method below to correctly position the icon.
139-
let styledText = resultsStatusView.applyMessageStyleTo(attributedString: messageText)
140-
messageText = NSMutableAttributedString(attributedString: styledText)
195+
private struct HostedAttributedLabel: UIViewRepresentable {
196+
let text: NSAttributedString
141197

142-
let icon = UIImage.gridicon(.bookmarkOutline, size: CGSize(width: 18, height: 18))
143-
messageText.replace("[bookmark-outline]", with: icon)
198+
func makeUIView(context: Context) -> UILabel {
199+
let label = UILabel()
200+
label.attributedText = text
201+
return label
202+
}
144203

145-
resultsStatusView.configureForLocalData(title: noResultsResponse.title, attributedSubtitle: messageText, image: "wp-illustration-reader-empty")
204+
func updateUIView(_ uiView: UILabel, context: Context) {
205+
// Do nothing
146206
}
147207
}
148208

@@ -152,3 +212,21 @@ extension ReaderStreamViewController {
152212
WPAnalytics.trackReader(.readerSavedListShown, properties: ["source": ReaderSaveForLaterOrigin.readerMenu.viewAllPostsValue])
153213
}
154214
}
215+
216+
private struct Strings {
217+
static let noFollowedSitesTitle = NSLocalizedString(
218+
"reader.no.blogs.title",
219+
value: "No blog subscriptions",
220+
comment: "Title for the no followed blogs result screen"
221+
)
222+
static let noFollowedSitesSubtitle = NSLocalizedString(
223+
"reader.no.blogs.subtitle",
224+
value: "Subscribe to blogs in Discover and you’ll see their latest posts here. Or search for a blog that you like already.",
225+
comment: "Subtitle for the no followed blogs result screen"
226+
)
227+
static let noFollowedSitesButtonTitle = NSLocalizedString(
228+
"reader.no.blogs.button",
229+
value: "Discover Blogs",
230+
comment: "Title for button on the no followed blogs result screen"
231+
)
232+
}

0 commit comments

Comments
 (0)