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

Add Image Playground Support #23688

Merged
merged 8 commits into from
Dec 19, 2024
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
2 changes: 2 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

25.6
-----
* [**] Add Image Playground support (part of Apple Intelligence suite) for adding images to your posts, generated featured image, site icons, and more [#23688]
* [**] Enhance the Gravatar Quick Editor by adding features that allow users to delete and share their avatars. [#23868]
* [**] Add the capability to create an avatar using Apple Image Playground through the Gravatar Quick Editor. [#23868]
* [*] [internal] Update Gravatar SDK to 3.0.0 [#23701]
* [*] Use the Gravatar Quick Editor to update the avatar [#23729]
* [*] (Hidden under a feature flag) User Management for self-hosted sites. [#23768]
* [*] Add URL and ID to the Media details screen, add IDs for posts [#23887]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,5 @@ enum MediaSource {
case camera
case mediaEditor
case tenor
case imagePlayground
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ extension HomeSiteHeaderViewController {
var actions = [
mediaMenu.makePhotosAction(delegate: presenter),
mediaMenu.makeCameraAction(delegate: presenter),
mediaMenu.makeImagePlaygroundAction(delegate: presenter),
mediaMenu.makeSiteMediaAction(blog: blog, delegate: presenter)
]
if FeatureFlag.siteIconCreator.enabled {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,14 @@ extension SiteIconPickerPresenter: SiteMediaPickerViewControllerDelegate {
}
}
}

extension SiteIconPickerPresenter: ImagePlaygroundPickerDelegate {
func imagePlaygroundViewController(_ picker: UIViewController, didCreateImageAt imageURL: URL) {
if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) {
showImageCropViewController(image, presentingViewController: picker)
} else {
DDLogError("Failed to load image created by ImagePlayground")
showErrorLoadingImageMessage()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WordPressShared
import React
import AutomatticTracks
import Combine
import ImagePlayground

class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelegate, PublishingEditor {
let errorDomain: String = "GutenbergViewController.errorDomain"
Expand Down Expand Up @@ -613,6 +614,8 @@ extension GutenbergViewController: GutenbergBridgeDelegate {
externalMediaPicker.presentStockPhotoPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback)
case .tenor:
externalMediaPicker.presentTenorPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback)
case .imagePlayground:
externalMediaPicker.presentImagePlayground(origin: self, post: post, callback: callback)
case .otherApps, .allFiles:
filesAppMediaPicker.presentPicker(origin: self, filters: filter, allowedTypesOnBlog: post.blog.allowedTypeIdentifiers, multipleSelection: allowMultipleSelection, callback: callback)
default: break
Expand Down Expand Up @@ -1132,10 +1135,11 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
func gutenbergMediaSources() -> [Gutenberg.MediaSource] {
post.managedObjectContext?.performAndWait {
[
MediaPickerMenu.isImagePlaygroundAvailable ? .imagePlayground : nil,
post.blog.supports(.stockPhotos) ? .stockPhotos : nil,
post.blog.supports(.tenor) ? .tenor : nil,
.otherApps,
.allFiles,
.allFiles
].compactMap { $0 }
} ?? []
}
Expand Down Expand Up @@ -1293,6 +1297,7 @@ extension GutenbergViewController: PostEditorNavigationBarManagerDelegate {
extension Gutenberg.MediaSource {
static let stockPhotos = Gutenberg.MediaSource(id: "wpios-stock-photo-library", label: .freePhotosLibrary, types: [.image])
static let otherApps = Gutenberg.MediaSource(id: "wpios-other-files", label: .otherApps, types: [.image, .video, .audio, .other])
static let imagePlayground = Gutenberg.MediaSource(id: "wpios-image-playground", label: MediaPickerMenu.imagePlaygroundLocalizedTitle, types: [.image])
static let allFiles = Gutenberg.MediaSource(id: "wpios-all-files", label: .otherApps, types: [.any])
static let tenor = Gutenberg.MediaSource(id: "wpios-tenor", label: .tenor, types: [.image])
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Gutenberg
import ImagePlayground

class GutenbergExternalMediaPicker {
class GutenbergExternalMediaPicker: NSObject {
private var mediaPickerCallback: MediaPickerDidPickMediaCallback?
private let mediaInserter: GutenbergMediaInserterHelper
private unowned var gutenberg: Gutenberg
Expand All @@ -9,6 +10,14 @@ class GutenbergExternalMediaPicker {
init(gutenberg: Gutenberg, mediaInserter: GutenbergMediaInserterHelper) {
self.mediaInserter = mediaInserter
self.gutenberg = gutenberg
super.init()
}

func presentImagePlayground(origin: UIViewController, post: AbstractPost, callback: @escaping MediaPickerDidPickMediaCallback) {
mediaPickerCallback = callback

MediaPickerMenu(viewController: origin)
.showImagePlayground(delegate: self)
}

func presentTenorPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) {
Expand Down Expand Up @@ -90,3 +99,13 @@ extension GutenbergExternalMediaPicker: ExternalMediaPickerViewDelegate {
}
}
}

extension GutenbergExternalMediaPicker: ImagePlaygroundPickerDelegate {
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) {
if let callback = mediaPickerCallback {
let itemProvider = MediaPickerMenu.makeItemProvider(with: imageURL)
mediaInserter.insertFromDevice([itemProvider], callback: callback)
}
viewController.presentingViewController?.dismiss(animated: true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import UIKit
import ImagePlayground

extension MediaPickerMenu {
static var isImagePlaygroundAvailable: Bool {
guard #available(iOS 18.1, *) else {
return false
}
return ImagePlaygroundViewController.isAvailable
}

static var imagePlaygroundLocalizedTitle: String {
Strings.imagePlayground
}

func makeImagePlaygroundAction(delegate: ImagePlaygroundPickerDelegate) -> UIAction {
UIAction(
title: Strings.imagePlayground,
image: UIImage(systemName: "apple.image.playground"),
attributes: [],
handler: { _ in showImagePlayground(delegate: delegate) }
)
}

func showImagePlayground(delegate: ImagePlaygroundPickerDelegate) {
guard let presentingViewController else { return }

guard #available(iOS 18.1, *) else {
return wpAssertionFailure("Not available on this platform. Use `isImagePlaygroundAvailable`.")
}

let controller = _ImagePlaygroundController()
controller.delegate = delegate

let imagePlaygroundVC = ImagePlaygroundViewController()
imagePlaygroundVC.delegate = controller
objc_setAssociatedObject(imagePlaygroundVC, &MediaPickerMenu.strongDelegateKey, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double-check, should controller or imagePlaygroundVC be stored here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

controller is the value that needs to be retained. ImagePlaygroundViewController is kept in memory by UIKit due to being presented on screen.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, this line should be ...?

Suggested change
objc_setAssociatedObject(imagePlaygroundVC, &MediaPickerMenu.strongDelegateKey, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_setAssociatedObject(controller, &MediaPickerMenu.strongDelegateKey, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)

Copy link
Contributor

@kean kean Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value to be retained is a third parameter. The test is that the delegate gets called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙈 Of course... Not sure how I misread that...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forget the signature literally every time I use it 😆


presentingViewController.present(imagePlaygroundVC, animated: true)
}

/// ImagePlayground returns heic images that are not supported by many WordPress
/// sites. The only exporter that currently supports transcoding images is
/// ``ItemProviderMediaExporter``, which is why we use it and which is why
/// we fallback to "public.heic" (should never happen as these URLs have
/// proper extensions).
static func makeItemProvider(with imageURL: URL) -> NSItemProvider {
let provider = NSItemProvider()
let typeIdentifier = imageURL.typeIdentifier ?? "public.heic"
provider.registerFileRepresentation(forTypeIdentifier: typeIdentifier, visibility: .all) { completion in
completion(imageURL, false, nil)
return nil
}
return provider
}

private static var strongDelegateKey: UInt8 = 0
}

// Uses the following workaround https://mastodon.social/@_inside/113640137011009924
// to make it compatible with a mixed Objective-C and Swift target.
private final class _ImagePlaygroundController: NSObject {
weak var delegate: ImagePlaygroundPickerDelegate?
}

@available(iOS 18.1, *)
extension _ImagePlaygroundController: ImagePlaygroundViewController.Delegate {
func imagePlaygroundViewController(_ imagePlaygroundViewController: ImagePlaygroundViewController, didCreateImageAt imageURL: URL) {
delegate?.imagePlaygroundViewController(imagePlaygroundViewController, didCreateImageAt: imageURL)
}

func imagePlaygroundViewControllerDidCancel(_ imagePlaygroundViewController: ImagePlaygroundViewController) {
delegate?.imagePlaygroundViewControllerDidCancel(imagePlaygroundViewController)
}
}

protocol ImagePlaygroundPickerDelegate: AnyObject {
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL)
func imagePlaygroundViewControllerDidCancel(_ viewController: UIViewController)
}

extension ImagePlaygroundPickerDelegate {
func imagePlaygroundViewControllerDidCancel(_ viewController: UIViewController) {
viewController.presentingViewController?.dismiss(animated: true)
}
}

private enum Strings {
static let imagePlayground = NSLocalizedString("mediaPicker.imagePlayground", value: "Image Playground", comment: "A name of the action in the context menu")
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,8 @@ extension MediaPickerMenu {
}
}

// MARK: - Helpers

extension MediaPickerMenu.MediaFilter {
init?(_ mediaType: WPMediaType) {
switch mediaType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import UIKit
import Photos
import PhotosUI

final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDelegate, ImagePickerControllerDelegate, ExternalMediaPickerViewDelegate, UIDocumentPickerDelegate {
final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDelegate, ImagePickerControllerDelegate, ExternalMediaPickerViewDelegate, UIDocumentPickerDelegate, ImagePlaygroundPickerDelegate {
let blog: Blog
let coordinator: MediaCoordinator

Expand All @@ -19,6 +19,7 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel
]),
UIMenu(options: [.displayInline], children: [
menu.makeCameraAction(delegate: self),
menu.makeImagePlaygroundAction(delegate: self),
makeDocumentPickerAction(from: viewController)
])
]
Expand Down Expand Up @@ -59,6 +60,18 @@ final class SiteMediaAddMediaMenuController: NSObject, PHPickerViewControllerDel
}
}

// MARK: - ImagePlaygroundPickerDelegate

func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) {
viewController.presentingViewController?.dismiss(animated: true)

coordinator.addMedia(
from: MediaPickerMenu.makeItemProvider(with: imageURL),
to: blog,
analyticsInfo: MediaAnalyticsInfo(origin: .mediaLibrary(.imagePlayground), selectionMethod: .fullScreenPicker)
)
}

// MARK: - ImagePickerControllerDelegate

func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ extension PostSettingsViewController: PHPickerViewControllerDelegate, ImagePicke
return UIMenu(children: [
menu.makePhotosAction(delegate: self),
menu.makeCameraAction(delegate: self),
menu.makeImagePlaygroundAction(delegate: self),
menu.makeSiteMediaAction(blog: self.apost.blog, delegate: self)
])
}
Expand Down Expand Up @@ -66,6 +67,18 @@ extension PostSettingsViewController: SiteMediaPickerViewControllerDelegate {
}
}

extension PostSettingsViewController: ImagePlaygroundPickerDelegate {
func imagePlaygroundViewController(_ viewController: UIViewController, didCreateImageAt imageURL: URL) {
dismiss(animated: true)

if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) {
setFeaturedImage(with: image)
} else {
wpAssertionFailure("failed to read the image created by ImagePlayground")
}
}
}

// MARK: - PostSettingsViewController (Featured Image Upload)

extension PostSettingsViewController {
Expand Down
6 changes: 6 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,8 @@
24C69A8B2612421900312D9A /* UserSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C69A8A2612421900312D9A /* UserSettingsTests.swift */; };
24C69AC22612467C00312D9A /* UserSettingsTestsObjc.m in Sources */ = {isa = PBXBuildFile; fileRef = 24C69AC12612467C00312D9A /* UserSettingsTestsObjc.m */; };
24CDE3412C5863A1005E5E43 /* TestKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CDE3402C5863A1005E5E43 /* TestKeychain.swift */; };
24E55D4E2CC9A5C8008D071D /* ImagePlayground.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24E55D4D2CC9A5C8008D071D /* ImagePlayground.framework */; };
24E55D4F2CC9A5CD008D071D /* ImagePlayground.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24E55D4D2CC9A5C8008D071D /* ImagePlayground.framework */; };
296890780FE971DC00770264 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296890770FE971DC00770264 /* Security.framework */; };
2FAE97090E33B21600CA8540 /* defaultPostTemplate_old.html in Resources */ = {isa = PBXBuildFile; fileRef = 2FAE97040E33B21600CA8540 /* defaultPostTemplate_old.html */; };
2FAE970C0E33B21600CA8540 /* xhtml1-transitional.dtd in Resources */ = {isa = PBXBuildFile; fileRef = 2FAE97070E33B21600CA8540 /* xhtml1-transitional.dtd */; };
Expand Down Expand Up @@ -2204,6 +2206,7 @@
24C69A8A2612421900312D9A /* UserSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsTests.swift; sourceTree = "<group>"; };
24C69AC12612467C00312D9A /* UserSettingsTestsObjc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UserSettingsTestsObjc.m; sourceTree = "<group>"; };
24CDE3402C5863A1005E5E43 /* TestKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestKeychain.swift; sourceTree = "<group>"; };
24E55D4D2CC9A5C8008D071D /* ImagePlayground.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImagePlayground.framework; path = System/Library/Frameworks/ImagePlayground.framework; sourceTree = SDKROOT; };
28A0AAE50D9B0CCF005BE974 /* WordPress_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WordPress_Prefix.pch; sourceTree = "<group>"; };
293E283D7339E7B6D13F6E09 /* Pods-JetpackShareExtension.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackShareExtension.release-internal.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackShareExtension/Pods-JetpackShareExtension.release-internal.xcconfig"; sourceTree = "<group>"; };
296890770FE971DC00770264 /* Security.framework */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -3793,6 +3796,7 @@
4AD953C72C21451700D0EEFA /* WordPressAuthenticator.framework in Frameworks */,
374CB16215B93C0800DD0EBC /* AudioToolbox.framework in Frameworks */,
E10B3655158F2D7800419A93 /* CoreGraphics.framework in Frameworks */,
24E55D4F2CC9A5CD008D071D /* ImagePlayground.framework in Frameworks */,
E10B3654158F2D4500419A93 /* UIKit.framework in Frameworks */,
E10B3652158F2D3F00419A93 /* QuartzCore.framework in Frameworks */,
E1A386CB14DB063800954CF8 /* MediaPlayer.framework in Frameworks */,
Expand Down Expand Up @@ -3948,6 +3952,7 @@
FABB26392602FC2C00C8785C /* ImageIO.framework in Frameworks */,
FABB263A2602FC2C00C8785C /* WebKit.framework in Frameworks */,
FABB263B2602FC2C00C8785C /* libsqlite3.tbd in Frameworks */,
24E55D4E2CC9A5C8008D071D /* ImagePlayground.framework in Frameworks */,
FABB263F2602FC2C00C8785C /* CoreServices.framework in Frameworks */,
9C86CF3E1EAC13181A593D00 /* Pods_Apps_Jetpack.framework in Frameworks */,
);
Expand Down Expand Up @@ -4502,6 +4507,7 @@
29B97323FDCFA39411CA2CEA /* Frameworks */ = {
isa = PBXGroup;
children = (
24E55D4D2CC9A5C8008D071D /* ImagePlayground.framework */,
0C43FF802C3601770084B698 /* UIKit.framework */,
3FA640652670CEFE0064401E /* XCTest.framework */,
F111B88B2658102700057942 /* Combine.framework */,
Expand Down