Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommend App: Add presenter to encapsulate display logic #17006

Merged
merged 17 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
case siteIconCreator
case editorOnboardingHelpMenu
case unifiedCommentsAndNotificationsList
case recommendAppToOthers

/// Returns a boolean indicating if the feature is enabled
var enabled: Bool {
Expand Down Expand Up @@ -43,6 +44,8 @@ enum FeatureFlag: Int, CaseIterable, OverrideableFlag {
return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest]
case .unifiedCommentsAndNotificationsList:
return true
case .recommendAppToOthers:
return false
}
}

Expand Down Expand Up @@ -91,6 +94,8 @@ extension FeatureFlag {
return "Editor Onboarding Help Menu"
case .unifiedCommentsAndNotificationsList:
return "Unified List for Comments and Notifications"
case .recommendAppToOthers:
return "Recommend App to Others"
}
}

Expand Down
129 changes: 129 additions & 0 deletions WordPress/Classes/ViewRelated/Sharing/ShareAppContentPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/// Encapsulates the logic required to fetch, prepare, and present the contents for sharing the app to others.
///
/// The contents for sharing is first fetched from the API, so the share presentation logic is not synchronously executed.
/// Callers are recommended to listen to progress changes by implementing `didUpdateLoadingState(_ loading:)` as this class' delegate.
///
class ShareAppContentPresenter {

// MARK: Public Properties

weak var delegate: ShareAppContentPresenterDelegate?

/// Tracks content fetch state.
private(set) var isLoading: Bool = false {
didSet {
guard isLoading != oldValue else {
return
}
delegate?.didUpdateLoadingState(isLoading)
}
}

// MARK: Private Properties

/// The API used for fetching the share app link. Anonymous profile is allowed.
private let api: WordPressComRestApi

private lazy var remote: ShareAppContentServiceRemote = {
ShareAppContentServiceRemote(wordPressComRestApi: api)
}()

/// In-memory cache. As long as the same presenter instance is used, there's no need to re-fetch the content everytime `shareContent` is called.
private var cachedContent: RemoteShareAppContent? = nil

// MARK: Initialization

/// Instantiates the presenter. When the provided account is nil, the presenter will default to anonymous API.
init(account: WPAccount? = nil) {
self.api = account?.wordPressComRestV2Api ?? .anonymousApi(userAgent: WPUserAgent.wordPress(), localeKey: WordPressComRestApi.LocaleKeyV2)
}

// MARK: Public Methods

/// Fetches the content needed for sharing, and presents the share sheet through the provided `sender` instance.
///
/// - Parameters:
/// - appName: The name of the app to be shared. Fetched contents will differ depending on the provided value.
/// - sender: The view that will be presenting the share sheet.
/// - sourceView: The view to be the anchor for the popover view on iPad.
/// - completion: A closure that's invoked after the process completes.
func present(for appName: ShareAppName, in sender: UIViewController, sourceView: UIView? = nil, completion: (() -> Void)? = nil) {
let anchorView = sourceView ?? sender.view
if let content = cachedContent {
presentShareSheet(with: content, in: sender, sourceView: anchorView)
completion?()
return
}

guard !isLoading else {
completion?()
return
}

isLoading = true

remote.getContent(for: appName) { [weak self] result in
guard let self = self else { return }

switch result {
case .success(let content):
self.cachedContent = content
self.presentShareSheet(with: content, in: sender, sourceView: anchorView)

case .failure:
self.showFailureNotice(in: sender)
}

self.isLoading = false
completion?()
}
}
}

// MARK: - Delegate Definition

protocol ShareAppContentPresenterDelegate: AnyObject {
/// Delegate method called everytime the presenter updates its loading state.
///
/// - Parameter loading: The presenter's latest loading state.
func didUpdateLoadingState(_ loading: Bool)
}

// MARK: - Private Helpers

private extension ShareAppContentPresenter {
/// Presents the share sheet by using `UIActivityViewController`. Contents to be shared will be constructed from the provided `content`.
///
/// - Parameters:
/// - content: The model containing information metadata for the sharing activity.
/// - viewController: The view controller that will be presenting the activity.
/// - sourceView: The view set to be the anchor for the popover.
func presentShareSheet(with content: RemoteShareAppContent, in viewController: UIViewController, sourceView: UIView?) {
guard let linkURL = content.linkURL() else {
return
}

let activityItems = [
ShareAppTextActivityItemSource(message: content.message) as Any,
linkURL as Any
]

let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
activityViewController.popoverPresentationController?.sourceView = sourceView
viewController.present(activityViewController, animated: true, completion: nil)
dvdchr marked this conversation as resolved.
Show resolved Hide resolved
}

/// Shows a notice indicating that the share intent failed.
///
func showFailureNotice(in viewController: UIViewController) {
viewController.displayNotice(title: .failureNoticeText, message: nil)
}
}

// MARK: Localized Strings

private extension String {
static let failureNoticeText = NSLocalizedString("Something went wrong. Please try again.",
comment: "Error message shown when user tries to share the app with others, "
+ "but failed due to unknown errors.")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// A text-type UIActivityItemSource for the share app activity.
///
/// Provides additional subject string so the subject line is filled when sharing the app via mail.
///
final class ShareAppTextActivityItemSource: NSObject {
private let message: String

init(message: String) {
self.message = message
}
}

// MARK: - UIActivityItemSource

extension ShareAppTextActivityItemSource: UIActivityItemSource {
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
// informs the activity controller that the activity type is text.
return String()
}

func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return message
}

func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
return .defaultSubjectText
}
}

// MARK: - Localized Strings

private extension String {
static let defaultSubjectText = NSLocalizedString("WordPress Apps - Apps for any screen",
comment: "Subject line for when sharing the app with others through mail or any other activity types "
+ "that support contains a subject field.")
}
Loading