Site icon AppTractor

Добавляем Image Playground в приложение

Помимо приложения для пользователей, разработчикам Image Playground предлагает фреймворк, позволяющий интегрировать генерацию изображений в приложения, встраивая компоненты, вызывающие и представляющие интерфейс Image Playground, доступный как в UIKit, так и в SwiftUI.

Чтобы углубиться в тему, мы реализуем приложение, генерирующее обложки для историй.

1. Начало работы

Подключаем фичу в SwiftUI

Чтобы воспользоваться преимуществами Image Playground, независимо от того, разрабатываете ли вы в UIKit или SwiftUI, импортируйте фреймворк Image Playground.

import ImagePlayground

Во-вторых, обязательно установите минимальное развертывание на 18.1 для iOS и 15.1 для macOS, чтобы избежать ошибок Xcode и иметь возможность создавать приложение только для поддерживаемых операционных систем.

Как вариант, установите атрибуты, чтобы убедиться в наличии Image Playgrounds в операционной системе перед его использованием.

Обработайте в точке входа в приложение ОС устройства:

import SwiftUI

@main
struct StoryCoverGenerator: App {
    var body: some Scene {
        WindowGroup {
            // 1. The available version
            if #available(iOS 18.1, macOS 15.1, *) {
                ContentView()
            // 2. Fallback on earlier versions
            } else {
                Text("Operating System not supported")
            }
        }
    }
}
  1. Убедитесь, что версия ОС не ниже iOS 18.1 или macOS 15.1.
  2. Обеспечьте резервное представление для устройств с меньшими версиями.

Сделайте то же самое для ContentView, но теперь в отношении доступности функции Image Playground.

import SwiftUI
import ImagePlayground

// 1. Define the OS availability
@available(iOS 18.1, macOS 15.1, *)
struct ContentView: View {
    
    // 2. Check the availability of the feature
    @Environment(\.supportsImagePlayground) private var supportsImagePlayground
    
    var body: some View {
        
        if supportsImagePlayground {
            // Shows the UI to use the feature
        } else {
            Text("This device does not support Image Generation features.")
        }
    }
}
  1. Добавьте атрибут @available, чтобы указать компилятору доступность этой структуры в ОС.
  2. Проверьте специальную переменную окружения для обработки отображения фичи в зависимости от типа устройства, чтобы избежать включения компонентом функции на устройствах с подходящей ОС, но не поддерживающих Apple Intelligence.

Подключаем фичу в UIKit

Давайте рассмотрим, как использовать преимущества атрибутов для обработки версии ОС и доступности функций при реализации в UIKit.

Во-первых, создайте UIViewController для обработки представления, когда функция недоступна.

import UIKit

class UnavailableViewController: UIViewController {
	// Create the UI as you see fit
}

Во-вторых, создайте UIViewController, отвечающий за генерацию обложки истории, и установите атрибуты доступности ОС.

import UIKit
import ImagePlayground

// The os availability attributes
@available(iOS 18.1,macOS 15.1, *)

// The view controller responsible for the Story Cover Generation
class StoryCoverGeneratorViewController: UIViewController {
	...
}

Теперь мы готовы обработать сцену, чтобы представить ее в соответствии с доступностью функции.

import UIKit
import ImagePlayground

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        
        guard let _ = (scene as? UIWindowScene) else { return }
        // 1. The view controller
        let vc: UIViewController
        
        // 2. Check the os version and the feature availability
        if #available(iOS 18.1, macOS 15.1, *), ImagePlaygroundViewController.isAvailable {
            // a. ViewController for the Story Cover Generator
            vc = StoryCoverGeneratorViewController()
        } else {
            // b. ViewController if unavailable
            vc = UnavailableViewController()
        }
        
        let navigationController = UINavigationController(rootViewController: vc)
        self.window?.rootViewController = navigationController
        self.window?.makeKeyAndVisible()
    }
    
}
  1. Объявите переменную, в которой будет храниться контроллер представления.
  2. Используйте атрибут для проверки версии ОС и доступности функции с помощью ImagePlaygroundViewController.isAvailable.
  3. Инициализируйте все в зависимости от того, доступна функция или нет:
    • присвойте переменной экземпляр StoryCoverGeneratorViewController, если Image Playground доступен;
    • присвойте экземпляр UnavailableViewController, если она недоступна.

2. Интеграция Image Playground

Интеграция в приложение SwiftUI

Метод imagePlaygroundSheet(isPresented:concept:sourceImageURL:onCompletion:onCancellation:) позволяет представить экран, на котором пользователь создает изображения из указанных входных данных.

Она принимает несколько параметров:

  1. isPresented — ожидается значение Bool, которое включает отображение представления листа, что приводит к открытию интерфейса Image Playground;
  2. concept — строка, описывающая содержимое, в соответствии с которым будет создаваться изображение;
  3. sourceImage ожидает URL-адрес файла в зависимости от того, должен ли промпт содержать начальную визуальную подсказку или нет. После того как эта функция открыта, ее можно переопределить непосредственно из интерфейса Image Playground. Метод позволяет сделать то же самое, используя изображение вместо URL.
  4. onCompletion , замыкание без возвращаемого значения, которое получает сгенерированное изображение с данными параметрами:
    • url , URL, указывающий на временный файл изображения
    • onCancellation, замыкание для обработки отмены процесса создания изображения, когда пользователь выходит из интерфейса создания без выбора изображения. После выполнения этого замыкания система автоматически закрывает экран.
import SwiftUI
import ImagePlayground

@available(iOS 18.1,macOS 15.1, *)
struct ContentView: View {
    // Enabling the sheet
    @State var isPresented: Bool = false
    
    var body: some View {
        
        VStack {
            // Application UI
        }
        
        // ImagePlayground Sheet Presenter
        .imagePlaygroundSheet(
            isPresented: $isPresented,
            concept: "a red cat with blue gloves"
        ) { url in
            // Use the URL
        }
    }
}

Другой метод, позволяющий описать изображение с помощью большего количества концепций, — это imagePlaygroundSheet(isPresented:concepts:sourceImageURL:onCompletion:onCancellation:), который требует объект [ImagePlaygroundConcept], коллекцию текстовых элементов, описывающих содержимое, по которым надо сгенерировать изображение.

.imagePlaygroundSheet(
    isPresented: $isPresented,
    
    // Image playground concepts
    concepts: [
        ImagePlaygroundConcept.text("red cat with blue gloves"),
        ImagePlaygroundConcept.text("big park with a pink river")
    ]) { url in
        // Use the URL
    }

Метод text(_:) создает концепты изображения из коротких строк, описывающих содержание изображения, которое пользователь хочет сгенерировать.

Имейте в виду, что:

  1. Метод лучше работает с отдельными словами или короткими предложениями;
  2. Если строка слишком длинная, она автоматически разбивает ее на более короткие концепции, выбирая только самые важные.

Когда речь идет о более длинных строках, следует использовать метод extracted(from:title:), который способен выбрать несколько концептов.

// ImagePlayground Sheet Presenter
.imagePlaygroundSheet(
    isPresented: $isPresented,
    
    // Image playground concepts
    concepts: [
        ImagePlaygroundConcept.extracted(
            from: "A red cat, named Rusty, strutted through the park, with his blue gloves catching everyone’s attention. He waved at a squirrel juggling acorns and joined a duck painting by the pond. Spotting a kite tangled in a tree, Rusty climbed up, saved the kite, and became the park’s unexpected hero.",
            title: "Rusty, the blue gloves cat")
    ]) { url in
        // Use the URL
    }

Этот метод возвращает объект ImagePlaygroundConcept и принимает 2 параметра:

  1. Текст, длинный объект String, из которого выбираются концепты для игровой площадки — если его длина не является минимально необходимой для модели, строка может быть использована как есть;
  2. Опциональный заголовок, короткая строка, представляющая текст в сжатом виде, что помогает модели извлекать из него основные понятия.

Теперь давайте объединим все в нашем представлении, чтобы сгенерировать возможные обложки на основе истории, предложенной пользователем.

import SwiftUI
import ImagePlayground

@available(iOS 18.1, macOS 15.1, *)
struct ContentView: View {
    // Enabling the sheet
    @State var isPresented: Bool = false
    
    var storyPlaceholder: String = "Write your story here..."
    
    // URL of image created by AI
    @State var imageURL: URL?
    
    // Story prompt
    @State var story: String = ""
    
    // Determine the availability of the feature.
    @Environment(\.supportsImagePlayground) private var supportsImagePlayground
    
    var body: some View {
        
        if supportsImagePlayground {
            ScrollView {
                VStack(alignment: .center) {
                    ZStack {
                        // Image loaded from Image Playground generation
                        if let url = imageURL {
                            AsyncImage(url: url) { image in
                                image.resizable()
                                    .scaledToFill()
                                    .frame(width: 200, height: 300)
                                    .clipped()
                                    .cornerRadius(8)
                            } placeholder: {
                                ProgressView()
                            }
                        } else {
                            ZStack(alignment: .center){
                                RoundedRectangle(cornerRadius: 8)
                                    .fill(Color.red)
                                    .frame(width: 200, height: 300)
                            }
                            
                            if imageURL == nil {
                                Text("""
                                    Story
                                    Cover
                                    Generator 
                                """)
                                .font(.title)
                                .bold()
                            }
                        }
                    }
                    .shadow(color: .black.opacity(0.3), radius: 8, x: 4, y: 4)
                    .padding()
                    
                    VStack(alignment: .leading) {
                        Text("Story:")
                            .font(.title3)
                            .bold()
                            .padding(.bottom, 5)
                        Text("Describe your story in no more than 250 words")
                            .font(.subheadline)
                            .bold()
                            .foregroundStyle(.secondary)
                            .padding(.bottom)
                        TextField(storyPlaceholder, text: $story, axis: .vertical)
                            .lineLimit(15)
                            .padding(5)
                            .background(Color.clear)
                            .overlay(
                                RoundedRectangle(cornerRadius: 12)
                                    .stroke(Color.red.opacity(0.5), lineWidth: 1)
                            )
                    }
                    .padding()
                }
                .padding()
                
                Button {
                    isPresented = self.checkStoryLength(story: story)
                } label: {
                    Text("Generate")
                        .foregroundStyle(.red)
                }
                
                // ImagePlayground Sheet Presenter
                .imagePlaygroundSheet(
                    isPresented: $isPresented,
                    concepts: [ImagePlaygroundConcept.extracted(from: story, title: nil)]) { url in
                    imageURL = url
                }
                .padding()
                
            }
        } else {
            Text("This device does not support Image Generation feature.")
                .foregroundStyle(.red)
        }
    }
    
    // Check the lenght of Story
    private func checkStoryLength(story: String) -> Bool {
        if story.components(separatedBy: " ").count >= 250 {
            return false
        }
        
        return true
    }
}

В этом приложении пользователи могут ввести в текстовое поле рассказ длиной до 250 слов и сгенерировать изображение, соответствующее этому рассказу.

Когда пользователь нажимает кнопку Generate, приложение проверяет длину рассказа, а затем отображает интерфейс Image Playground. Условная проверка среды гарантирует, что функция доступна на поддерживаемых устройствах, а в противном случае выводит сообщение об ошибке.

Интеграция в приложение UIKit

Чтобы представить интерфейс Image Playground в UIKit, используйте ImagePlaygroundViewController. Расширьте класс StoryCoverGeneratorViewController новым методом, отвечающим за представление листа Image Playground.

extension StoryCoverGeneratorViewController {
    
    @IBAction
    private func openImagePlayground(with story: String) {
        // 1. Initialize the playground
        let playground = ImagePlaygroundViewController()
        
        // 2. Delegation
        playground.delegate = self
        
        // 2. Set extracted concepts from the story in the playground
        playground.concepts = [.extracted(from: story, title: nil)]
        
        // 3. Present the ImagePlaygroundViewController
        present(playground, animated: true, completion: nil)
    }
    
}

Метод openImagePlayground(with:), получающий сюжет в виде значения String, работает следующим образом:

  1. Инициализирует ImagePlaygroundViewController;
  2. Устанавливает свойство делегата для перехвата событий жизненного цикла;
  3. Извлекает концепции из параметра с помощью метода extracted(from:title:) и сохраняет их в свойстве conceptsproperty контроллера ImagePlaygroundViewController;
  4. Показывает ImagePlaygroundViewController.

Расширьте класс StoryCoverGeneratorViewController, чтобы он соответствовал протоколу ImagePlaygroundViewController.Delegate.

// Conforming to ImagePlaygroundViewController.Delegate
extension StoryCoverGeneratorViewController: ImagePlaygroundViewController.Delegate {
    
    // 1. The delegate stub returning the generated image to the delegate
    func imagePlaygroundViewController(_ imagePlaygroundViewController: ImagePlaygroundViewController, didCreateImageAt imageURL: URL) {
        // 2. Set the image 
        if let image = UIImage(contentsOfFile: imageURL.path) {
            imageView.image = image
        } else {
            print("Error loading image from URL: \(imageURL)")
        }
        
        // 3. Dismiss the sheet
        dismiss(animated: true, completion: nil)
    }
    
}
  1. Реализуйте метод imagePlaygroundViewController(_:didCreateImageAt:), возвращающий делегату URL сгенерированного изображения;
  2. Установите изображение, которое будет отображаться в пользовательском интерфейсе;
  3. Выйти из экрана Image Playground.

Интеграция в наше приложение Story Cover Generator будет выглядеть следующим образом:

import UIKit
import ImagePlayground

class StoryCoverGeneratorViewController: UIViewController {
    
    // Image Generated View that will display the generated image
    private lazy var imageView: UIImageView = { ... }()
    
    // Cover Label
    private let coverLabel: UILabel = { ... }()
    
    // Headline
    private let storyLabel: UILabel = { ... }()
    
    // Subheadline
    private let instructionLabel: UILabel = { ... }()
    
    // TextField
    private let storyTextField: UITextField = { ... }()
    
    // Generate Button
    private let generateButton: UIButton = { ... }()
    
    // Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    // Setting up the UI: add the subviews and the layout constraints
    private func setupUI() { ... }
    
    // 1. Method triggered on the button tap
    @objc private func generateButtonTapped() {
        // 2. Check the length of story input
        if let storyText = storyTextField.text, storyText.components(separatedBy: " ").count <= 250 {
            // 3. Present the Image Playground interface
            openImagePlayground(with: storyText)
        } else {
            // 4. Present Error Alert
            let alert = UIAlertController(title: "Invalid Input", message: "Please limit your story to 250 words.", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
            present(alert, animated: true, completion: nil)
        }
    }
}

extension StoryCoverGeneratorViewController: ImagePlaygroundViewController.Delegate {
    
    // Initialize and present the Image Playground interface.
    @IBAction
    private func openImagePlayground(with story: String) {
        // Initialize the playground, get set up to be notified of lifecycle events.
        let playground = ImagePlaygroundViewController()
        playground.delegate = self
        // Set extracted concepts from the story in the playground
        playground.concepts = [.extracted(from: story, title: nil)]
        present(playground, animated: true, completion: nil)
    }
    
    // The delegate stub returning the generated image to the delegate
    func imagePlaygroundViewController(_ imagePlaygroundViewController: ImagePlaygroundViewController, didCreateImageAt imageURL: URL) {
        // Add the image to the image view
        if let image = UIImage(contentsOfFile: imageURL.path) {
            imageView.image = image
        } else {
            print("Error loading image from URL: \(imageURL)")
        }
        
        dismiss(animated: true, completion: nil)
    }
}
  1. Создайте метод, который будет срабатывать при нажатии на кнопку «Сгенерировать».
  2. Проверьте длину вводимого сюжета.
  3. Представьте интерфейс Image Playground с помощью метода openImagePlayground(with story:).
  4. Выдавайте предупреждение об ошибке, если история длиннее, чем ожидалось.
import UIKit
import ImagePlayground

class StoryCoverGeneratorViewController: UIViewController {
    
    // Properties of the view controller
    
    // Generate Button
    private let generateButton: UIButton = {
        
        // Code that creates the button
        
        // 1. Add the action to the target
        button.addTarget(self, action: #selector(generateButtonTapped), for: .touchUpInside)
        return button
    }()
    
    // Methods of the view controller
}

Добавьте этот метод в качестве действия к цели кнопки Generate.

Заключение

Интеграция Apple Image Playground с SwiftUI или UIKit открывает новые возможности для создания динамических визуальных эффектов, интерактивного контента и продвинутых инструментов дизайна. Разработчики могут легко создавать визуально насыщенный опыт, сохраняя при этом бесшовную интеграцию с разными фреймворками в своих приложениях.

На данный момент возможности ограничены, так как Apple будет расширять возможности на базе Apple Intelligence в течение года. Многообещающим является тот факт, что даже если эти возможности ограничены на данный момент, в дальнейшем они будут только улучшаться.

Если в ваших приложениях есть сценарии использования, в которых могут быть использованы возможности генерации изображений, имеет смысл начать готовиться к будущему, в котором качество результатов, создаваемых этими инструментами, будет гораздо выше.

Источник

Exit mobile version