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 (Apple Intelligence) image playground to the Quick Editor #565

Merged
merged 11 commits into from
Nov 22, 2024
4 changes: 4 additions & 0 deletions Demo/Gravatar-Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,11 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Manual;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Demo/Gravatar-Demo/Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "Accessing camera to create an avatar.";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
Expand All @@ -482,6 +484,7 @@
MARKETING_VERSION = 1.0;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "Gravatar Demo App Development";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Gravatar Demo App Development";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -498,6 +501,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Demo/Gravatar-Demo/Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "Accessing camera to create an avatar.";
Copy link
Contributor Author

@pinarol pinarol Nov 20, 2024

Choose a reason for hiding this comment

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

I guess NSCameraUsageDescription was removed during the merge of two demo apps. So I am adding it back.

INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UIMainStoryboardFile = Main;
Expand Down
3 changes: 3 additions & 0 deletions Sources/GravatarUI/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,6 @@
/* An option in a menu that display the user's Photo Library and allow them to choose a photo from it */
"SystemImagePickerView.Source.PhotoLibrary.title" = "Choose a Photo";

/* An option to show the image playground */
"SystemImagePickerView.Source.Playground.title" = "Playground";

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ImagePlayground
import PhotosUI
import SwiftUI

Expand All @@ -15,27 +16,37 @@ private struct ImagePicker<Label, ImageEditor: ImageEditorView>: View where Labe
enum SourceType: CaseIterable, Identifiable {
case photoLibrary
case camera
case playground

static var allCases: [SourceType] {
var cases: [SourceType] = [.camera, .photoLibrary]
if #available(iOS 18.2, *) {
if EnvironmentValues().supportsImagePlayground {
Copy link
Contributor

Choose a reason for hiding this comment

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

Works flawlessly

cases.append(.playground)
}
}
return cases
}

var id: Int {
self.hashValue
}
}

@State var isPresented = false
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This isn't used from anywhere so I am removing it.

@State private var sourceType: SourceType?

@ViewBuilder var label: () -> Label
let onImageSelected: (UIImage) -> Void
var customEditor: ImageEditorBlock<ImageEditor>?
@State var imagePickerSelectedItem: ImagePickerItem?
@State var playgroundSelectedItem: ImagePickerItem?

var body: some View {
VStack {
Menu {
ForEach(SourceType.allCases) { source in
Button {
sourceType = source
isPresented = true
} label: {
SwiftUI.Label(source.localizedTitle, systemImage: source.iconName)
}
Expand All @@ -44,29 +55,58 @@ private struct ImagePicker<Label, ImageEditor: ImageEditorView>: View where Labe
label()
}
}
.sheet(item: $sourceType, content: { source in
// This allows to present different kind of pickers for different sources.
displayImagePicker(for: source)
.sheet(item: $imagePickerSelectedItem, content: { item in
if let customEditor {
customEditor(item.image) { editedImage in
self.onImageEdited(editedImage)
}
} else {
ImageCropper(inputImage: item.image) { croppedImage in
Task {
await self.onImageEdited(croppedImage)
}
} onCancel: {
imagePickerSelectedItem = nil
}.ignoresSafeArea()
}
})
.imagePlaygroundSheetIfAvailable(
isPresented: Binding(
get: { sourceType == .playground },
set: { if !$0 { sourceType = nil } }
),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doing this to be able to add the new case playground to SourceType and still have these be managed by a single sourceType variable.

(The system offers a modifier for the playground not a View. But our existing image sources are Views. So they are displayed with different methods.)

sourceImage: nil,
onCompletion: { url in
if let image = UIImage(contentsOfFile: url.relativePath) {
playgroundSelectedItem = ImagePickerItem(id: url.absoluteString, image: image)
}
},
onCancellation: {}
)
.sheet(item: $playgroundSelectedItem, content: { item in
imageEditor(with: item)
})
.sheet(
item: Binding(
get: { sourceType != .playground ? sourceType : nil },
set: { sourceType = $0 }
),
content: { source in
// This allows to present different kind of pickers for different sources.
displayImagePicker(for: source)
.sheet(item: $imagePickerSelectedItem, content: { item in
imageEditor(with: item)
})
}
)
}

@ViewBuilder
func imageEditor(with item: ImagePickerItem) -> some View {
if let customEditor {
customEditor(item.image) { editedImage in
self.onImageEdited(editedImage)
}
} else {
ImageCropper(inputImage: item.image) { croppedImage in
Task {
await self.onImageEdited(croppedImage)
}
} onCancel: {
imagePickerSelectedItem = nil
playgroundSelectedItem = nil
}.ignoresSafeArea()
}
}

private func onImageEdited(_ image: UIImage) {
imagePickerSelectedItem = nil
playgroundSelectedItem = nil
sourceType = nil
onImageSelected(image)
}
Expand All @@ -87,6 +127,8 @@ private struct ImagePicker<Label, ImageEditor: ImageEditorView>: View where Labe
} onCancel: {
sourceType = nil
}.ignoresSafeArea()
case .playground:
EmptyView()
}
}

Expand All @@ -105,6 +147,8 @@ extension ImagePicker.SourceType {
"camera"
case .photoLibrary:
"photo.on.rectangle.angled"
case .playground:
"apple.image.playground"
}
}

Expand All @@ -122,13 +166,12 @@ extension ImagePicker.SourceType {
value: "Take a Photo",
comment: "An option in a menu that will display the camera for taking a picture"
)
}
}

func map() -> UIImagePickerController.SourceType {
switch self {
case .photoLibrary: .photoLibrary
case .camera: .camera
case .playground:
SDKLocalizedString(
"SystemImagePickerView.Source.Playground.title",
value: "Playground",
comment: "An option to show the image playground"
)
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions Sources/GravatarUI/SwiftUI/View+Additions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,18 @@ extension View {
}
}
}

@ViewBuilder
public func imagePlaygroundSheetIfAvailable(
isPresented: Binding<Bool>,
sourceImage: Image? = nil,
onCompletion: @escaping (URL) -> Void,
onCancellation: (() -> Void)? = nil
) -> some View {
if #available(iOS 18.2, *) {
self.imagePlaygroundSheet(isPresented: isPresented, sourceImage: sourceImage, onCompletion: onCompletion, onCancellation: onCancellation)
} else {
self
}
}
}