Skip to content

Commit 9d4e482

Browse files
jkmasselkeancrazytonyli
committed
Add Image Playground Support (#23688)
* Add ImagePlayground support in Gutenberg * Integrate ImagePlayground in GutenberG * Move ImagePlayground code to MediaPickerMenu * Integrate SiteIcon picker * Integrate in PostSettingsViewController * Integrate in Media Picker * Update WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu+ImagePlayground.swift Co-authored-by: Tony Li <[email protected]> * Update release notes --------- Co-authored-by: kean <[email protected]> Co-authored-by: Alex Grebenyuk <[email protected]> Co-authored-by: Tony Li <[email protected]>
1 parent 343dee4 commit 9d4e482

File tree

11 files changed

+166
-3
lines changed

11 files changed

+166
-3
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
25.6
55
-----
6+
* [**] Add Image Playground support (part of Apple Intelligence suite) for adding images to your posts, generated featured image, site icons, and more [#23688]
67
* [**] Enhance the Gravatar Quick Editor by adding features that allow users to delete and share their avatars. [#23868]
78
* [**] Add the capability to create an avatar using Apple Image Playground through the Gravatar Quick Editor. [#23868]
9+
* [*] [internal] Update Gravatar SDK to 3.0.0 [#23701]
810
* [*] Use the Gravatar Quick Editor to update the avatar [#23729]
911
* [*] (Hidden under a feature flag) User Management for self-hosted sites. [#23768]
1012
* [*] Add URL and ID to the Media details screen, add IDs for posts [#23887]

WordPress/Classes/Utility/Analytics/WPAppAnalytics+Media.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,5 @@ enum MediaSource {
181181
case camera
182182
case mediaEditor
183183
case tenor
184+
case imagePlayground
184185
}

WordPress/Classes/ViewRelated/Blog/My Site/Header/HomeSiteHeaderViewController+SiteIcon.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ extension HomeSiteHeaderViewController {
2626
var actions = [
2727
mediaMenu.makePhotosAction(delegate: presenter),
2828
mediaMenu.makeCameraAction(delegate: presenter),
29+
mediaMenu.makeImagePlaygroundAction(delegate: presenter),
2930
mediaMenu.makeSiteMediaAction(blog: blog, delegate: presenter)
3031
]
3132
if FeatureFlag.siteIconCreator.enabled {

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,14 @@ extension SiteIconPickerPresenter: SiteMediaPickerViewControllerDelegate {
158158
}
159159
}
160160
}
161+
162+
extension SiteIconPickerPresenter: ImagePlaygroundPickerDelegate {
163+
func imagePlaygroundViewController(_ picker: UIViewController, didCreateImageAt imageURL: URL) {
164+
if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) {
165+
showImageCropViewController(image, presentingViewController: picker)
166+
} else {
167+
DDLogError("Failed to load image created by ImagePlayground")
168+
showErrorLoadingImageMessage()
169+
}
170+
}
171+
}

WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import WordPressShared
66
import React
77
import AutomatticTracks
88
import Combine
9+
import ImagePlayground
910

1011
class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelegate, PublishingEditor {
1112
let errorDomain: String = "GutenbergViewController.errorDomain"
@@ -613,6 +614,8 @@ extension GutenbergViewController: GutenbergBridgeDelegate {
613614
externalMediaPicker.presentStockPhotoPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback)
614615
case .tenor:
615616
externalMediaPicker.presentTenorPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback)
617+
case .imagePlayground:
618+
externalMediaPicker.presentImagePlayground(origin: self, post: post, callback: callback)
616619
case .otherApps, .allFiles:
617620
filesAppMediaPicker.presentPicker(origin: self, filters: filter, allowedTypesOnBlog: post.blog.allowedTypeIdentifiers, multipleSelection: allowMultipleSelection, callback: callback)
618621
default: break
@@ -1132,10 +1135,11 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
11321135
func gutenbergMediaSources() -> [Gutenberg.MediaSource] {
11331136
post.managedObjectContext?.performAndWait {
11341137
[
1138+
MediaPickerMenu.isImagePlaygroundAvailable ? .imagePlayground : nil,
11351139
post.blog.supports(.stockPhotos) ? .stockPhotos : nil,
11361140
post.blog.supports(.tenor) ? .tenor : nil,
11371141
.otherApps,
1138-
.allFiles,
1142+
.allFiles
11391143
].compactMap { $0 }
11401144
} ?? []
11411145
}
@@ -1293,6 +1297,7 @@ extension GutenbergViewController: PostEditorNavigationBarManagerDelegate {
12931297
extension Gutenberg.MediaSource {
12941298
static let stockPhotos = Gutenberg.MediaSource(id: "wpios-stock-photo-library", label: .freePhotosLibrary, types: [.image])
12951299
static let otherApps = Gutenberg.MediaSource(id: "wpios-other-files", label: .otherApps, types: [.image, .video, .audio, .other])
1300+
static let imagePlayground = Gutenberg.MediaSource(id: "wpios-image-playground", label: MediaPickerMenu.imagePlaygroundLocalizedTitle, types: [.image])
12961301
static let allFiles = Gutenberg.MediaSource(id: "wpios-all-files", label: .otherApps, types: [.any])
12971302
static let tenor = Gutenberg.MediaSource(id: "wpios-tenor", label: .tenor, types: [.image])
12981303
}

WordPress/Classes/ViewRelated/Gutenberg/Utils/GutenbergExternalMeidaPicker.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Gutenberg
2+
import ImagePlayground
23

3-
class GutenbergExternalMediaPicker {
4+
class GutenbergExternalMediaPicker: NSObject {
45
private var mediaPickerCallback: MediaPickerDidPickMediaCallback?
56
private let mediaInserter: GutenbergMediaInserterHelper
67
private unowned var gutenberg: Gutenberg
@@ -9,6 +10,14 @@ class GutenbergExternalMediaPicker {
910
init(gutenberg: Gutenberg, mediaInserter: GutenbergMediaInserterHelper) {
1011
self.mediaInserter = mediaInserter
1112
self.gutenberg = gutenberg
13+
super.init()
14+
}
15+
16+
func presentImagePlayground(origin: UIViewController, post: AbstractPost, callback: @escaping MediaPickerDidPickMediaCallback) {
17+
mediaPickerCallback = callback
18+
19+
MediaPickerMenu(viewController: origin)
20+
.showImagePlayground(delegate: self)
1221
}
1322

1423
func presentTenorPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) {
@@ -90,3 +99,13 @@ extension GutenbergExternalMediaPicker: ExternalMediaPickerViewDelegate {
9099
}
91100
}
92101
}
102+
103+
extension GutenbergExternalMediaPicker: ImagePlaygroundPickerDelegate {
104+
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) {
105+
if let callback = mediaPickerCallback {
106+
let itemProvider = MediaPickerMenu.makeItemProvider(with: imageURL)
107+
mediaInserter.insertFromDevice([itemProvider], callback: callback)
108+
}
109+
viewController.presentingViewController?.dismiss(animated: true)
110+
}
111+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import UIKit
2+
import ImagePlayground
3+
4+
extension MediaPickerMenu {
5+
static var isImagePlaygroundAvailable: Bool {
6+
guard #available(iOS 18.1, *) else {
7+
return false
8+
}
9+
return ImagePlaygroundViewController.isAvailable
10+
}
11+
12+
static var imagePlaygroundLocalizedTitle: String {
13+
Strings.imagePlayground
14+
}
15+
16+
func makeImagePlaygroundAction(delegate: ImagePlaygroundPickerDelegate) -> UIAction {
17+
UIAction(
18+
title: Strings.imagePlayground,
19+
image: UIImage(systemName: "apple.image.playground"),
20+
attributes: [],
21+
handler: { _ in showImagePlayground(delegate: delegate) }
22+
)
23+
}
24+
25+
func showImagePlayground(delegate: ImagePlaygroundPickerDelegate) {
26+
guard let presentingViewController else { return }
27+
28+
guard #available(iOS 18.1, *) else {
29+
return wpAssertionFailure("Not available on this platform. Use `isImagePlaygroundAvailable`.")
30+
}
31+
32+
let controller = _ImagePlaygroundController()
33+
controller.delegate = delegate
34+
35+
let imagePlaygroundVC = ImagePlaygroundViewController()
36+
imagePlaygroundVC.delegate = controller
37+
objc_setAssociatedObject(imagePlaygroundVC, &MediaPickerMenu.strongDelegateKey, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
38+
39+
presentingViewController.present(imagePlaygroundVC, animated: true)
40+
}
41+
42+
/// ImagePlayground returns heic images that are not supported by many WordPress
43+
/// sites. The only exporter that currently supports transcoding images is
44+
/// ``ItemProviderMediaExporter``, which is why we use it and which is why
45+
/// we fallback to "public.heic" (should never happen as these URLs have
46+
/// proper extensions).
47+
static func makeItemProvider(with imageURL: URL) -> NSItemProvider {
48+
let provider = NSItemProvider()
49+
let typeIdentifier = imageURL.typeIdentifier ?? "public.heic"
50+
provider.registerFileRepresentation(forTypeIdentifier: typeIdentifier, visibility: .all) { completion in
51+
completion(imageURL, false, nil)
52+
return nil
53+
}
54+
return provider
55+
}
56+
57+
private static var strongDelegateKey: UInt8 = 0
58+
}
59+
60+
// Uses the following workaround https://mastodon.social/@_inside/113640137011009924
61+
// to make it compatible with a mixed Objective-C and Swift target.
62+
private final class _ImagePlaygroundController: NSObject {
63+
weak var delegate: ImagePlaygroundPickerDelegate?
64+
}
65+
66+
@available(iOS 18.1, *)
67+
extension _ImagePlaygroundController: ImagePlaygroundViewController.Delegate {
68+
func imagePlaygroundViewController(_ imagePlaygroundViewController: ImagePlaygroundViewController, didCreateImageAt imageURL: URL) {
69+
delegate?.imagePlaygroundViewController(imagePlaygroundViewController, didCreateImageAt: imageURL)
70+
}
71+
72+
func imagePlaygroundViewControllerDidCancel(_ imagePlaygroundViewController: ImagePlaygroundViewController) {
73+
delegate?.imagePlaygroundViewControllerDidCancel(imagePlaygroundViewController)
74+
}
75+
}
76+
77+
protocol ImagePlaygroundPickerDelegate: AnyObject {
78+
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL)
79+
func imagePlaygroundViewControllerDidCancel(_ viewController: UIViewController)
80+
}
81+
82+
extension ImagePlaygroundPickerDelegate {
83+
func imagePlaygroundViewControllerDidCancel(_ viewController: UIViewController) {
84+
viewController.presentingViewController?.dismiss(animated: true)
85+
}
86+
}
87+
88+
private enum Strings {
89+
static let imagePlayground = NSLocalizedString("mediaPicker.imagePlayground", value: "Image Playground", comment: "A name of the action in the context menu")
90+
}

WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift renamed to WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPickerMenu.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ extension MediaPickerMenu {
265265
}
266266
}
267267

268+
// MARK: - Helpers
269+
268270
extension MediaPickerMenu.MediaFilter {
269271
init?(_ mediaType: WPMediaType) {
270272
switch mediaType {

WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaAddMediaMenuController.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import UIKit
22
import Photos
33
import PhotosUI
44

5-
final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDelegate, ImagePickerControllerDelegate, ExternalMediaPickerViewDelegate, UIDocumentPickerDelegate {
5+
final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDelegate, ImagePickerControllerDelegate, ExternalMediaPickerViewDelegate, UIDocumentPickerDelegate, ImagePlaygroundPickerDelegate {
66
let blog: Blog
77
let coordinator: MediaCoordinator
88

@@ -19,6 +19,7 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel
1919
]),
2020
UIMenu(options: [.displayInline], children: [
2121
menu.makeCameraAction(delegate: self),
22+
menu.makeImagePlaygroundAction(delegate: self),
2223
makeDocumentPickerAction(from: viewController)
2324
])
2425
]
@@ -59,6 +60,18 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel
5960
}
6061
}
6162

63+
// MARK: - ImagePlaygroundPickerDelegate
64+
65+
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) {
66+
viewController.presentingViewController?.dismiss(animated: true)
67+
68+
coordinator.addMedia(
69+
from: MediaPickerMenu.makeItemProvider(with: imageURL),
70+
to: blog,
71+
analyticsInfo: MediaAnalyticsInfo(origin: .mediaLibrary(.imagePlayground), selectionMethod: .fullScreenPicker)
72+
)
73+
}
74+
6275
// MARK: - ImagePickerControllerDelegate
6376

6477
func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {

WordPress/Classes/ViewRelated/Post/PostSettingsViewController+FeaturedImageUpload.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePicke
3131
return UIMenu(children: [
3232
menu.makePhotosAction(delegate: self),
3333
menu.makeCameraAction(delegate: self),
34+
menu.makeImagePlaygroundAction(delegate: self),
3435
menu.makeSiteMediaAction(blog: self.apost.blog, delegate: self)
3536
])
3637
}
@@ -66,6 +67,18 @@ extension PostSettingsViewController: SiteMediaPickerViewControllerDelegate {
6667
}
6768
}
6869

70+
extension PostSettingsViewController: ImagePlaygroundPickerDelegate {
71+
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) {
72+
dismiss(animated: true)
73+
74+
if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) {
75+
setFeaturedImage(with: image)
76+
} else {
77+
wpAssertionFailure("failed to read the image created by ImagePlayground")
78+
}
79+
}
80+
}
81+
6982
// MARK: - PostSettingsViewController (Featured Image Upload)
7083

7184
extension PostSettingsViewController {

0 commit comments

Comments
 (0)