Skip to content

Commit a6b45fd

Browse files
authored
Merge pull request #21370 from wordpress-mobile/task/phpicker-site-icon
Integrate PHPickerViewController in Site Icon picker
2 parents d264d82 + 5eccbcd commit a6b45fd

File tree

8 files changed

+419
-91
lines changed

8 files changed

+419
-91
lines changed

WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import Gridicons
22
import UIKit
33

44
@objc protocol BlogDetailHeaderViewDelegate {
5-
func siteIconTapped()
5+
func makeSiteIconMenu() -> UIMenu?
6+
func didShowSiteIconMenu()
67
func siteIconReceivedDroppedImage(_ image: UIImage?)
78
func siteIconShouldAllowDroppedImages() -> Bool
89
func siteTitleTapped()
@@ -107,11 +108,12 @@ class BlogDetailHeaderView: UIView {
107108

108109
// MARK: - Initializers
109110

110-
required init(items: [ActionRow.Item]) {
111+
required init(items: [ActionRow.Item], delegate: BlogDetailHeaderViewDelegate) {
111112
titleView = TitleView(frame: .zero)
112113

113114
super.init(frame: .zero)
114115

116+
self.delegate = delegate
115117
setupChildViews(items: items)
116118
}
117119

@@ -122,11 +124,14 @@ class BlogDetailHeaderView: UIView {
122124
// MARK: - Child View Initialization
123125

124126
private func setupChildViews(items: [ActionRow.Item]) {
125-
titleView.siteIconView.tapped = { [weak self] in
126-
QuickStartTourGuide.shared.visited(.siteIcon)
127-
self?.titleView.siteIconView.spotlightIsShown = false
127+
assert(delegate != nil)
128128

129-
self?.delegate?.siteIconTapped()
129+
if let menu = delegate?.makeSiteIconMenu() {
130+
titleView.siteIconView.setMenu(menu) { [weak self] in
131+
self?.delegate?.didShowSiteIconMenu()
132+
WPAnalytics.track(.siteSettingsSiteIconTapped)
133+
self?.titleView.siteIconView.spotlightIsShown = false
134+
}
130135
}
131136

132137
titleView.siteIconView.dropped = { [weak self] images in

WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class SiteIconView: UIView {
4040

4141
private var dropInteraction: UIDropInteraction?
4242

43+
/// Set the menu to be displayed when the button is tapped. The menu replaces
44+
/// teh default on tap action.
45+
func setMenu(_ menu: UIMenu, onMenuTriggered: @escaping () -> Void) {
46+
button.menu = menu
47+
button.showsMenuAsPrimaryAction = true
48+
button.addAction(UIAction { _ in onMenuTriggered() }, for: .menuActionTriggered)
49+
}
50+
4351
private let button: UIButton = {
4452
let button = UIButton(frame: .zero)
4553
button.backgroundColor = UIColor.secondaryButtonBackground

WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension SitePickerViewController {
4040
blogDetailHeaderView.toggleSpotlightOnSiteIcon()
4141
}
4242

43-
private func showNoticeAsNeeded() {
43+
func showNoticeAsNeeded() {
4444
if let tourToSuggest = QuickStartTourGuide.shared.tourToSuggest(for: blog) {
4545
QuickStartTourGuide.shared.suggest(tourToSuggest, for: blog)
4646
}

WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift

Lines changed: 64 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,79 @@ import WordPressFlux
33
import WordPressShared
44
import SwiftUI
55
import SVProgressHUD
6+
import Gridicons
7+
import PhotosUI
68

79
extension SitePickerViewController {
810

9-
func showSiteIconSelectionAlert() {
10-
let alert = UIAlertController(title: SiteIconAlertStrings.title,
11-
message: nil,
12-
preferredStyle: .actionSheet)
13-
14-
alert.popoverPresentationController?.sourceView = blogDetailHeaderView.blavatarImageView.superview
15-
alert.popoverPresentationController?.sourceRect = blogDetailHeaderView.blavatarImageView.frame
16-
alert.popoverPresentationController?.permittedArrowDirections = .any
17-
18-
alert.addDefaultActionWithTitle(SiteIconAlertStrings.Actions.chooseImage) { [weak self] _ in
19-
NoticesDispatch.unlock()
20-
self?.updateSiteIcon()
21-
}
22-
23-
alert.addDefaultActionWithTitle(SiteIconAlertStrings.Actions.createWithEmoji) { [weak self] _ in
24-
NoticesDispatch.unlock()
25-
self?.showEmojiPicker()
11+
func makeSiteIconMenu() -> UIMenu? {
12+
guard siteIconShouldAllowDroppedImages() else {
13+
return nil
2614
}
27-
28-
alert.addDestructiveActionWithTitle(SiteIconAlertStrings.Actions.removeSiteIcon) { [weak self] _ in
29-
NoticesDispatch.unlock()
30-
self?.removeSiteIcon()
31-
}
32-
33-
alert.addCancelActionWithTitle(SiteIconAlertStrings.Actions.cancel) { [weak self] _ in
34-
NoticesDispatch.unlock()
35-
self?.startAlertTimer()
36-
}
37-
38-
present(alert, animated: true)
15+
return UIMenu(children: [
16+
UIDeferredMenuElement.uncached { [weak self] in
17+
$0(self?.makeUpdateSiteIconActions() ?? [])
18+
}
19+
])
3920
}
4021

41-
func showUpdateSiteIconAlert() {
42-
let alert = UIAlertController(title: nil,
43-
message: nil,
44-
preferredStyle: .actionSheet)
45-
46-
alert.popoverPresentationController?.sourceView = blogDetailHeaderView.blavatarImageView.superview
47-
alert.popoverPresentationController?.sourceRect = blogDetailHeaderView.blavatarImageView.frame
48-
alert.popoverPresentationController?.permittedArrowDirections = .any
49-
50-
alert.addDefaultActionWithTitle(SiteIconAlertStrings.Actions.changeSiteIcon) { [weak self] _ in
51-
NoticesDispatch.unlock()
52-
self?.updateSiteIcon()
53-
}
54-
55-
if blog.hasIcon {
56-
alert.addDestructiveActionWithTitle(SiteIconAlertStrings.Actions.removeSiteIcon) { [weak self] _ in
57-
NoticesDispatch.unlock()
58-
self?.removeSiteIcon()
22+
func didShowSiteIconMenu() {
23+
if QuickStartTourGuide.shared.isCurrentElement(.siteIcon) {
24+
// There is no good way to determine when `UIMenu` is cancelled,
25+
// so we wait until nothing is presented by the site picker.
26+
NoticesDispatch.lock()
27+
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
28+
if self.presentedViewController == nil {
29+
NoticesDispatch.unlock()
30+
self.showNoticeAsNeeded()
31+
timer.invalidate()
32+
}
5933
}
6034
}
35+
QuickStartTourGuide.shared.visited(.siteIcon)
36+
}
6137

62-
alert.addCancelActionWithTitle(SiteIconAlertStrings.Actions.cancel) { [weak self] _ in
63-
NoticesDispatch.unlock()
64-
self?.startAlertTimer()
38+
private func makeUpdateSiteIconActions() -> [UIAction] {
39+
let presenter = makeSiteIconPresenter()
40+
let mediaMenu = MediaPickerMenu(viewConroller: self, filter: .images)
41+
var actions: [UIAction] = []
42+
if FeatureFlag.nativePhotoPicker.enabled {
43+
actions += [
44+
mediaMenu.makePhotosAction(delegate: presenter),
45+
mediaMenu.makeCameraAction(delegate: presenter),
46+
mediaMenu.makeMediaAction(blog: blog, delegate: presenter)
47+
]
48+
} else {
49+
actions.append(UIAction(
50+
title: SiteIconAlertStrings.Actions.changeSiteIcon,
51+
image: UIImage(systemName: "photo.on.rectangle"),
52+
handler: { [weak self] _ in
53+
guard let self else { return }
54+
presenter.presentPickerFrom(self)
55+
}
56+
))
6557
}
66-
67-
present(alert, animated: true)
58+
if FeatureFlag.siteIconCreator.enabled {
59+
actions.append(UIAction(
60+
title: SiteIconAlertStrings.Actions.createWithEmoji,
61+
image: UIImage(systemName: "face.smiling"),
62+
handler: { [weak self] _ in self?.showEmojiPicker() }
63+
))
64+
}
65+
if blog.hasIcon {
66+
actions.append(UIAction(
67+
title: SiteIconAlertStrings.Actions.removeSiteIcon,
68+
image: UIImage(systemName: "trash"),
69+
attributes: [.destructive],
70+
handler: { [weak self] _ in self?.removeSiteIcon() }
71+
))
72+
}
73+
return actions
6874
}
6975

70-
func updateSiteIcon() {
71-
siteIconPickerPresenter = SiteIconPickerPresenter(blog: blog)
72-
siteIconPickerPresenter?.onCompletion = { [ weak self] media, error in
76+
private func makeSiteIconPresenter() -> SiteIconPickerPresenter {
77+
let presenter = SiteIconPickerPresenter(blog: blog)
78+
presenter.onCompletion = { [ weak self] media, error in
7379
if error != nil {
7480
self?.showErrorForSiteIconUpdate()
7581
} else if let media = media {
@@ -82,13 +88,12 @@ extension SitePickerViewController {
8288
self?.siteIconPickerPresenter = nil
8389
self?.startAlertTimer()
8490
}
85-
86-
siteIconPickerPresenter?.onIconSelection = { [weak self] in
91+
presenter.onIconSelection = { [weak self] in
8792
self?.blogDetailHeaderView.updatingIcon = true
8893
self?.dismiss(animated: true)
8994
}
90-
91-
siteIconPickerPresenter?.presentPickerFrom(self)
95+
self.siteIconPickerPresenter = presenter
96+
return presenter
9297
}
9398

9499
func showEmojiPicker() {

WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ final class SitePickerViewController: UIViewController {
2222
let mediaService: MediaService
2323

2424
private(set) lazy var blogDetailHeaderView: BlogDetailHeaderView = {
25-
let headerView = BlogDetailHeaderView(items: [])
25+
let headerView = BlogDetailHeaderView(items: [], delegate: self)
2626
headerView.translatesAutoresizingMaskIntoConstraints = false
2727
return headerView
2828
}()
@@ -51,7 +51,6 @@ final class SitePickerViewController: UIViewController {
5151

5252
private func setupHeaderView() {
5353
blogDetailHeaderView.blog = blog
54-
blogDetailHeaderView.delegate = self
5554
view.addSubview(blogDetailHeaderView)
5655
view.pinSubviewToAllEdges(blogDetailHeaderView)
5756
}
@@ -71,25 +70,6 @@ final class SitePickerViewController: UIViewController {
7170

7271
extension SitePickerViewController: BlogDetailHeaderViewDelegate {
7372

74-
func siteIconTapped() {
75-
guard siteIconShouldAllowDroppedImages() else {
76-
// Gracefully ignore the tap for users that can not upload files or
77-
// blogs that do not have capabilities since those will not support the REST API icon update
78-
return
79-
}
80-
81-
WPAnalytics.track(.siteSettingsSiteIconTapped)
82-
83-
NoticesDispatch.lock()
84-
85-
guard FeatureFlag.siteIconCreator.enabled else {
86-
showUpdateSiteIconAlert()
87-
return
88-
}
89-
90-
showSiteIconSelectionAlert()
91-
}
92-
9373
func siteIconReceivedDroppedImage(_ image: UIImage?) {
9474
if !siteIconShouldAllowDroppedImages() {
9575
// Gracefully ignore the drop for users that can not upload files or

WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import WPMediaPicker
44
import WordPressShared
55
import MobileCoreServices
66
import UniformTypeIdentifiers
7+
import PhotosUI
78

89
/// Encapsulates the interactions required to capture a new site icon image, crop it and resize it.
910
///
10-
class SiteIconPickerPresenter: NSObject {
11+
final class SiteIconPickerPresenter: NSObject {
1112

1213
// MARK: - Public Properties
1314

@@ -48,6 +49,9 @@ class SiteIconPickerPresenter: NSObject {
4849
return pickerViewController
4950
}()
5051

52+
private var dataSource: AnyObject?
53+
private var mediaCapturePresenter: AnyObject?
54+
5155
// MARK: - Public methods
5256

5357
/// Designated Initializer
@@ -87,7 +91,7 @@ class SiteIconPickerPresenter: NSObject {
8791

8892
/// Shows a new ImageCropViewController for the given image.
8993
///
90-
fileprivate func showImageCropViewController(_ image: UIImage) {
94+
func showImageCropViewController(_ image: UIImage, presentingViewController: UIViewController? = nil) {
9195
DispatchQueue.main.async {
9296
SVProgressHUD.dismiss()
9397
let imageCropViewController = ImageCropViewController(image: image)
@@ -131,7 +135,17 @@ class SiteIconPickerPresenter: NSObject {
131135
}
132136
}
133137
}
134-
self.mediaPickerViewController.show(after: imageCropViewController)
138+
if let presentingViewController {
139+
imageCropViewController.shouldShowCancelButton = true
140+
imageCropViewController.onCancel = { [weak presentingViewController] in
141+
// Dismiss the crop controller but not the picker
142+
presentingViewController?.dismiss(animated: true)
143+
}
144+
let navigationController = UINavigationController(rootViewController: imageCropViewController)
145+
presentingViewController.present(navigationController, animated: true)
146+
} else {
147+
self.mediaPickerViewController.show(after: imageCropViewController)
148+
}
135149
}
136150
}
137151

@@ -173,6 +187,39 @@ class SiteIconPickerPresenter: NSObject {
173187
}
174188
}
175189

190+
extension SiteIconPickerPresenter: PHPickerViewControllerDelegate {
191+
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
192+
guard let result = results.first else {
193+
picker.presentingViewController?.dismiss(animated: true)
194+
return
195+
}
196+
WPAnalytics.track(.siteSettingsSiteIconGalleryPicked)
197+
self.showLoadingMessage()
198+
self.originalMedia = nil
199+
MediaPickerMenu.loadImage(for: result) { [weak self] image, error in
200+
if let image {
201+
self?.showImageCropViewController(image, presentingViewController: picker)
202+
} else {
203+
DDLogError("Failed to load image: \(String(describing: error))")
204+
self?.showErrorLoadingImageMessage()
205+
}
206+
}
207+
}
208+
}
209+
210+
extension SiteIconPickerPresenter: ImagePickerControllerDelegate {
211+
func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
212+
guard let presentingViewController = picker.presentingViewController else {
213+
return
214+
}
215+
presentingViewController.dismiss(animated: true) {
216+
if let image = info[.originalImage] as? UIImage {
217+
self.showImageCropViewController(image, presentingViewController: presentingViewController)
218+
}
219+
}
220+
}
221+
}
222+
176223
extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate {
177224

178225
func mediaPickerControllerWillBeginLoadingData(_ picker: WPMediaPickerViewController) {
@@ -206,6 +253,7 @@ extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate {
206253
/// Retrieves the chosen image and triggers the ImageCropViewController display.
207254
///
208255
func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) {
256+
dataSource = nil
209257
mediaLibraryDataSource.searchCancelled()
210258
if assets.isEmpty {
211259
return
@@ -240,7 +288,11 @@ extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate {
240288
self?.showErrorLoadingImageMessage()
241289
return
242290
}
243-
self?.showImageCropViewController(image)
291+
if FeatureFlag.nativePhotoPicker.enabled {
292+
self?.showImageCropViewController(image, presentingViewController: picker)
293+
} else {
294+
self?.showImageCropViewController(image)
295+
}
244296
})
245297
default:
246298
break

0 commit comments

Comments
 (0)