Мне хотелось посмотреть, как работает ИИ Редактор кода Cursor AI на примере создания iOS приложения с выборкой данных с ресурса, который не требует API key и платной подписки. И этим ресурсом оказались публичные фотографии с Flickr.com.
Задача
Создать UI iOS приложения со строкой поиска вверху и сеткой Grid
под ней для отображения миниатюр фотографий наподобие:
Пользователь должен иметь возможность вводить текст в строку поиска и видеть набор фотографий, теги которых tags
соответствуют строке поиска. Строка поиска может содержать одно слово (например, “rose”) или разделенные пробелами слова(например, “forest bird” (лес птица)).
Функциональные требования:
- Список фотографий извлекается с помощью API из Flickr типа: https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=porcupine (замените слово «porcupine» на то, которое ввел пользователь).
- При выполнении поиска отображается индикатор хода выполнения, не блокируя UI.
- При нажатии на изображение должно быть показано
View
с подробной информации о фотографии.
Используемые технологии:
- Swift
- SwiftUI
- Async/Await
Настройка: Mac, Xcode и Cursor AI
Вам нужен Mac — любой: Mac Studio, MacBook, iMac.
Вам нужно будет загрузить Xcode из Apple App Store.
Это приложение Apple, и оно поможет вам писать код. Но вы не будете писать ни единой строки кода, Cursor AI сделает это за вас, но он вам понадобится для создания iOS приложения и предварительного просмотра результатов Preview
.
Загрузите Cursor AI.
Запустите Xcode и Cursor AI одновременно:
Первое, что вам нужно сделать, — это создать новый проект в Xcode:
Убедитесь, что вы создаете iOS приложение:
… и назовите его PhotomaniaCursor:
Просто нажмите Next (“Далее”), а затем вам будет предложено задать место для хранения файлы приложения? Я выберу свой рабочий стол desktop, потому что это просто.
Я нажму Create (“Создать”), и вы увидите, что там создана папка PhotomaniaCursor.
Xcode создал все начальные файлы для моего приложения, и на предварительном просмотре Preview
вы видите Hello World! Да, верно, он дает вам пример кода, который отображает Hello World! на экране iPhone, что довольно круто.
Это показывает, как легко начать создавать iOS приложение, но мы не собираемся писать код.
Мы собираемся использовать Cursor AI, чтобы сделать это.
Запускаем Cursor AI, и если это первый раз, то открываем папку с нашим Xcode проектом.
И, конечно, если я зайду на свой рабочий стол desktop, то там найду папку PhotomaniaCursor.
Нажимает на Open (“Открыть”) и все файлы подсоединяют к Cursor AI.
Если я их открою, то вы увидите там все, что я делал в Xcode. Всё это теперь привязано к Cursor AI.
Начнем с того, что запустим Composer с помощью Shift + Cmd + I. Вы получите Untitled Composer.
Вы можете использовать его, чтобы начать писать код.
Но, в первую очередь, я хочу убедиться, что следую лучшим рекомендациям Apple при создании моего iOS приложения. Вы можете научить Cursor AI лучшим практикам Apple, чтобы ваше приложение стало идеальным.
Как вы это делаете?
Введите символ @ в Cursor Composer:
… и начните вводить @Docs:
Вы увидите @Docs выглядит так.
Теперь вы видите, что я уже обучала Cursor AI некоторым предыдущим документам, но я могу добавить новый документ здесь:
Я могу добавить веб-страницу. У меня на Chrome открыто несколько разных веб-сайтов.
Во-первых, у меня есть документация Swift https://developer.apple.com/documentation/swift/
Итак, я скопирую URL-адрес Apple Swift, и вставлю его сюда:
Дадим этому документу имя Apple Swift и кликнем “Confirm” (“Подтвердить”):
И он начнет обучать Cursor AI языку программирования Swift.
Теперь я хочу обучить Cursor AI еще нескольким документам, включая “UI design Do’s and Don’ts” (“Советы по дизайну пользовательского интерфейса”) от Apple https://developer.apple.com/design/tips/ :
Чтобы Cursor АI также следовал этим рекомендациям.
До сих пор мы обучались на документации @Apple Swift. Теперь у нас “UI design Do’s and Don’ts” (“Советы по дизайну пользовательского интерфейса Apple”) , то есть @Apple UI Design Tips:
Cursor АI будет ссылаться на них при написании кода.
Итак, сегодня у нас все отлично. Мы не используем старые данные для обучения. Это последние новости на сайте Apple.
Еще один тег @ мы сделаем перед тем, как вставить описание нашего приложения.
И это @Codebase:
Codebase очень важна, потому что у нас есть существующие файлы, созданные Xcode, и мы хотим, чтобы Composer видел и редактировал их, создавая наше приложение:
Описание приложения для Cursor AI
Теперь, когда все отмечено, я могу добавить описание приложения. Кстати, Cursor AI понимает только английский язык
Я хотела бы создать приложение Flickr Photos для iOS.
Оно должно делать следующее.
- Взять фотографии
Photos
c https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1 - Показать фотографии
Photos
с использованиемLazyVGrid
и использовать.searchable
для поиска фотографий. - Добавить
PhotoDetailView
для подробной информации о фотографии. - Использовать
NavigationStack
.
Это все, что мне нужно сделать, чтобы начать создавать свое приложение для iOS.
Я нажимаю Enter, и Cursor Composer приступает к работе.
Вы увидите, что он пишет код прямо у меня на глазах. Я пока не знаю, что делает этот код. Он просто создает мое приложение в соответствии с лучшими Apple принципами дизайна UI @Apple UI Design Tips.
Давайте кликнем Accept All, то есть мы принимаем весь созданный Cursor AI код:
В Xcode мы сразу увидим, что все созданные Cursor AI файлы добавлены и предварительный просмотр Preview
обновлен:
Мы видим, что Cursor AI создал новые файлы прямо в нашем проекте Xcode: FlickrPhoto
, FlickrViewModel
, PhotoDetailView
и PhotoGridItem
. В дальнейшем нам не придется копировать или обновлять их вручную, Composer Cursor AI все будет делать в этом проекте автоматически. И приложение работает.
Да, фотографии выбраны c Flickr.com и размещены в сетке LazyVGid
, которая не идеальна и нуждается в настройке, поисковая строка также присутствует на UI. Но если я попытаюсь начать набирать текст в поисковой строке для выбора фотографий, то посмотрите, что будет происходить:
Версия 1.
То есть выборка фотографий идет не с сервера Flickr.com, a из того списка, который есть на экране.
Строка поиска в качестве tags для URL
Я прошу Cursor AI о том, чтобы строка поиска searchText
использовалась при формировании запроса к Flickr.com в качестве tags
. Вот моя подсказка (Prompt):
Для удобства я переведу эту подсказку (Prompt) и ответ Cursor AI на русский язык, и далее будут предоставлять только ответы Cursor AI на русском языке.
Используй searchText
в качестве tags
в URL для поиска фотографий в коде:
if !searchText.isEmpty {
// Convert search text to comma-separated tags and properly encode for URL
let tags = searchText
.components(separatedBy: " ")
.filter { !$0.isEmpty }
.joined(separator: ",")
if let encodedTags = tags.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
urlString += "&tags=\(encodedTags)"
}
}
Я помогу вам изменить FlickrViewModel
, чтобы включить поиск на основе тегов tags
в URL API. Вот обновленная версия FlickrViewModel
:
И нам следует обновить ContentView
, чтобы запустить новый поиск при изменении searchText
:
Ключевые изменения:
- Удалена локальная фильтрация в
filteredPhotos
, так как теперь мы используем API для выборки. - Добавлен поиск на основе тегов
tags
в конструкциюURL
- Добавлен модификатор
.onChange
для запуска нового поиска при измененииsearchText
- Выделен базовый URL
baseURL
для лучшей читабельности - Добавлено правильное кодирование URL для тегов t
ags
.
Эта реализация теперь будет искать фотографии с помощью системы тегов Flickr, которая более эффективна, чем фильтрация на стороне клиента, и будет возвращать более релевантные результаты.
Если все объяснения нас устраивают, опять кликаем Accept All.
Переключаемся на Xcode. И опять, нам не пришлось копировать какие-либо файлы или обновлять их вручную, Cursor AI все сделал автоматически и предварительный просмотр Preview
обновлен. Поиск фотографий по тегам tags
, задаваемым в поисковой строке searchText
теперь работает правильно.
Версия 2.
Однако сетка Grid
все еще выглядит не совсем хорошо. Кроме того, в подробном фото есть непонятный текст с описанием description
.
Индикатор загрузки и isLoading
Давайте уберем из PhotoDetailView
описание description
и добавим использование индикатора isLoading
из FlickrViewModel
в ContentView
:
Убери Text(photo.description)
из PhotoDetailView
и используй isLoading
из FlickrViewModel
в ContentView
Я помогу вам изменить оба View
.
Сначала давайте обновим PhotoDetailView
, чтобы удалить описание фото description
:
import SwiftUI
struct PhotoDetailView: View {
let photo: FlickrPhoto
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
AsyncImage(url: URL(string: photo.media.originalURL)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
ProgressView()
}
.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 8) {
Text(photo.title)
.font(.title2)
.bold()
Text("By \(photo.author)")
.font(.subheadline)
.foregroundColor(.secondary)
Text("Taken on \(photo.dateTaken)")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding()
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
Теперь давайте обновим ContentView
, чтобы лучше обрабатывать состояние загрузки фотографий:
struct ContentView: View {
@StateObject private var viewModel = FlickrViewModel()
let columns = [
GridItem(.adaptive(minimum: 150), spacing: 20)
]
var body: some View {
NavigationStack {
ZStack {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(viewModel.filteredPhotos) { photo in
NavigationLink(destination:
PhotoDetailView(photo: photo)) {
PhotoGridItem(photo: photo)
}
}
}
.padding()
}
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
.frame(maxWidth: .infinity,
maxHeight: .infinity)
.background(Color.black.opacity(0.1))
}
}
.navigationTitle("Flickr Photos")
.searchable(text: $viewModel.searchText,
prompt: "Search photos")
.onChange(of: viewModel.searchText) { oldValue, newValue in
Task {
await viewModel.fetchPhotos()
}
}
.alert("Error", isPresented:
.constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
Ключевые изменения:
Удален Text
с описанием photo.description
из PhotoDetailView
Улучшено состояние загрузки в ContentView
за счет:
- Перемещения индикатора загрузки в
ZStack
- Добавления полупрозрачного фона
- Более заметного показа индикатора загрузки с помощью
scaleEffect
- Центрирования его в
View
Теперь индикатор загрузки будет более заметным и предоставит пользователям лучшую обратную связь во время загрузки фотографий.
Если все объяснения нас устраивают, опять кликаем Accept All.
В Xcode мы видим, что предварительный просмотр Preview
обновлен:
Версия 3.
Поиск фотографий по тегам tags
, задаваемым в поисковой строке searchText
по-прежнему работает правильно, виден индикатор загрузки фотографий и исчезло описание description
в PhotoDetailView
.
Однако сетка Grid
все еще выглядит не совсем хорошо, попробуем её улучшить.
Улучшение сетки LazyVGrid
Продолжаем работать с Cursor AI. Я прошу его улучшить UI и прикладываю screenshot того, что мне не нравится,
LazyVGrid
выглядит не очень хорошо. Создай более привлекательный UI
Я помогу улучшить макет LazyVGrid
, чтобы сделать его более визуально привлекательным. Вот обновленный ContentView
с улучшенным стилем сетки:
struct ContentView: View {
@StateObject private var viewModel = FlickrViewModel()
// Using 2 columns with fixed size for better proportions
let columns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1)
]
var body: some View {
NavigationStack {
ZStack {
Color.black.opacity(0.1) // Background color for grid
ScrollView {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(viewModel.filteredPhotos) { photo in
NavigationLink(destination:
PhotoDetailView(photo: photo)) {
PhotoGridItem(photo: photo)
.aspectRatio(1, contentMode: .fill) // Square aspect ratio
}
}
}
}
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.1))
}
}
.navigationTitle("Flickr Photos")
.searchable(text: $viewModel.searchText,
prompt: "Search photos")
.onChange(of: viewModel.searchText) { oldValue, newValue in
Task {
await viewModel.fetchPhotos()
}
}
.alert("Error", isPresented:
.constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
И давайте обновим PhotoGridItem
, чтобы он соответствовал новому стилю сетки:
import SwiftUI
struct PhotoGridItem: View {
let photo: FlickrPhoto
var body: some View {
AsyncImage(url: URL(string: photo.media.m)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
.frame(minHeight: 100)
.background(Color.gray.opacity(0.1))
}
.clipped()
.contentShape(Rectangle())
}
}
Ключевые улучшения:
- Изменен на фиксированную сетку из 2 столбцов для согласованности
- Добавлено квадратное соотношение сторон для элементов сетки
- Уменьшено расстояние между элементами до 1 point для более современного вида
- Добавлен фоновый цвет
- Упрощен PhotoGridItem для лучшей производительности
- Элементы сетки теперь полностью заполняют свое пространство
- Добавлено обрезание для предотвращения переполнения изображения
- Улучшен внешний вид заполнителя
Это создаст более похожий на Instagram макет сетки, визуально привлекательный и согласованный. Фотографии будут отображаться в квадратном формате с минимальным интервалом, создавая современный вид.
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 4.
Надо сказать, что какая-то систематизация фотографий в сетке Grid
появилась, но все равно фотографии “наезжают” друг на друга. Всё дело в строке кода .aspectRatio(contentMode: .fill),
которая позволяет фотографии в сетке LazyVGrid
заполнить собой всё пространство.
Используем .scaledToFill()
Давайте заменим её на .scaledToFill()
.
В LazyVGri
d ипользуй .scaledToFill()
вместо .aspectRatio
Я обновлю оба файла PhotoGridItem
и ContentView
, чтобы использовать .scaledToFill()
для лучшего представления фотографий:
import SwiftUI
struct PhotoGridItem: View {
let photo: FlickrPhoto
var body: some View {
AsyncImage(url: URL(string: photo.media.m)) { image in
image
.resizable()
.scaledToFill()
} placeholder: {
ProgressView()
.frame(minHeight: 100)
.background(Color.gray.opacity(0.1))
}
.clipped()
.contentShape(Rectangle())
}
}
struct ContentView: View {
@StateObject private var viewModel = FlickrViewModel()
let columns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1)
]
var body: some View {
NavigationStack {
ZStack {
Color.black.opacity(0.1)
ScrollView {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(viewModel.filteredPhotos) { photo in
NavigationLink(destination:
PhotoDetailView(photo: photo)) {
PhotoGridItem(photo: photo)
.frame(height: UIScreen.main.bounds.width / 2) // Make height equal to half screen width
.clipped()
}
}
}
}
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.1))
}
}
.navigationTitle("Flickr Photos")
.searchable(text: $viewModel.searchText, prompt: "Search photos")
.onChange(of: viewModel.searchText) { oldValue, newValue in
Task {
await viewModel.fetchPhotos()
}
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
Ключевые изменения:
- Заменено
.aspectRatio
на.scaledToFill()
вPhotoGridItem
- Добавлена явная высота
height
вframe
вContentView
на основе ширины экранаscreenWidth
для квадратных ячеек - Сохранено
.clipped()
, чтобы изображения не переполняли свои контейнеры - Удалено
.aspectRatio(1, contentMode: .fit)
изNavigationLink
Это создаст сетку, более похожую на Instagram, где изображения полностью заполняют свои квадраты, сохраняя при этом соотношение сторон, а любые переполнения обрезаются.
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 5.
Все замечательно, никто ни на кого не наезжает, но фотографии в одной строке неравноправны — одни больше, другие меньше. Я хотела бы, чтобы они все были квадратные фиксированного размера, например, 108 х 108 и чтобы было три столбца в LazyVGrid
, a не два.
Используем 3 столбца в LazyVGrid
Просим об этом Cursor AI:
Используй 3 столбца в LazyVGrid
и frame(width: 108, height : 108)
для используй PhotoGridView
Я обновлю оба файла, чтобы использовать 3 столбца с элементами размером 108×108:
struct ContentView: View {
@StateObject private var viewModel = FlickrViewModel()
let columns = [
GridItem(.adaptive(minimum: 108), spacing: 1),
GridItem(.adaptive(minimum: 108), spacing: 1),
GridItem(.adaptive(minimum: 108), spacing: 1)
]
var body: some View {
NavigationStack {
ZStack {
Color.black.opacity(0.1)
ScrollView {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(viewModel.filteredPhotos) { photo in
NavigationLink(destination:
PhotoDetailView(photo: photo)) {
PhotoGridItem(photo: photo)
}
}
}
}
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
.frame(maxWidth: .infinity,
maxHeight: .infinity)
.background(Color.black.opacity(0.1))
}
}
.navigationTitle("Flickr Photos")
.searchable(text: $viewModel.searchText,
prompt: "Search photos")
.onChange(of: viewModel.searchText) { oldValue, newValue in
Task {
await viewModel.fetchPhotos()
}
}
.alert("Error", isPresented:
.constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
import SwiftUI
struct PhotoGridItem: View {
let photo: FlickrPhoto
var body: some View {
AsyncImage(url: URL(string: photo.media.m)) { image in
image
.resizable()
.scaledToFill()
.frame(width: 108, height: 108)
} placeholder: {
ProgressView()
.frame(width: 108, height: 108)
.background(Color.gray.opacity(0.1))
}
.clipped()
.contentShape(Rectangle())
}
}
Ключевые изменения:
- Обновлён размер рамки
PhotoGridItem
до 108×108 - Изменена сетка
LazyVGrid
, чтобы использовать 3 столбца с.adaptive(minimum: 108)
- Добавлен третий
GridItem
в массив столбцовcolumns
- Сохранен интервал в 1 point между элементами
Это создаст более компактную сетку с 3 столбцами columns
квадратных изображений, похожую на макет сетки Instagram.
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 6
Ну, наконец-то, получилось так, как мне хочется. В результате вот какие различия в столбцах columns
сетки LazyVGrid
мы получили:
Было:
let columns = [
GridItem(.flexible(), spacing: 1),
GridItem(.flexible(), spacing: 1)
]
Стало:
let columns = [
GridItem(.adaptive(minimum: 108), spacing: 1),
GridItem(.adaptive(minimum: 108), spacing: 1),
GridItem(.adaptive(minimum: 108), spacing: 1)
]
Добавляем тэги tags в PhotoDetailView
Теперь давайте добавим тэги tags
в PhotoDetailView
:
Добавь Text("Tags: \(photo.tags)")
в PhotoDetailView
Я обновляю PhotoDetailView
, включив информацию о тегах:
import SwiftUI
struct PhotoDetailView: View {
let photo: FlickrPhoto
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
AsyncImage(url: URL(string: photo.media.originalURL)) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
} placeholder: {
ProgressView()
}
.frame(maxWidth: .infinity)
VStack(alignment: .leading, spacing: 8) {
Text(photo.title)
.font(.title2)
.bold()
Text("By \(photo.author)")
.font(.subheadline)
.foregroundColor(.secondary)
Text("Taken on \(photo.dateTaken)")
.font(.subheadline)
.foregroundColor(.secondary)
if !photo.tags.isEmpty {
Text("Tags: \(photo.tags)")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding()
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
Ключевые изменения:
- Добавлено условное
Text
для отображения теговphoto.tags
- Использован тот же стиль, что и для другой вторичной информации (шрифт
.subheadline
и цвет.secondary
) - Показывает теги, только если они есть, то есть массив
photo.tags
не пустой - Сохранены одинаковые интервалы и отступы с другими UI элементами.
Кроме того, в PhotoDetailView
, для image
внутри AsyncImage
вручную заменим .aspectRatio(contentMode: .fit)
на .aspectRatio(contentMode: .fill)
, ведь Cursor AI — это редактор, так что правим код прямо там.
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 7
Все замечательно, но при более внимательном рассмотрении кода ContentView
, мы замечаем, что LazyVGrid
использует массив filteredPhotos
:
… который определен в FlickrViewModel
:
Удаляем массив filteredPhotos
Ясно, что этот массив — “паразит” filteredPhotos
остался после Версии 1, когда была выполнена попытка отфильтровать уже выбранный из Flickr.com массив фотографий на основе данных строки поиска searchText
. Мы перенесли выборку в API и необходимость в фильтрации отпала, но массив filteredPhotos
остался. Он не нужен, мы от него избавимся, но не вручную , a с помощью Composer Cursor AI:
Используй viewModel.photos
вместо filteredPhotos
в LazyVGrid
и убери filteredPhotos
из ViewModel
Я помогу вам обновить оба файла, чтобы удалить свойство filteredPhotos
и использовать фотографии photos
напрямую:
@MainActor
class FlickrViewModel: ObservableObject {
@Published var photos: [FlickrPhoto] = []
@Published var searchText = ""
@Published var isLoading = false
@Published var errorMessage: String?
private let baseURL = "https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1"
func fetchPhotos() async {
isLoading = true
errorMessage = nil
do {
var urlString = baseURL
if !searchText.isEmpty {
let tags = searchText
.components(separatedBy: " ")
.filter { !$0.isEmpty }
.joined(separator: ",")
if let encodedTags = tags.addingPercentEncoding(
withAllowedCharacters: .urlQueryAllowed) {
urlString += "&tags=\(encodedTags)"
}
}
guard let url = URL(string: urlString) else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
let result = try
JSONDecoder().decode(FlickrFeedResult.self, from: data)
photos = result.items
isLoading = false
} catch {
errorMessage = error.localizedDescription
isLoading = false
}
}
}
struct ContentView: View {
@StateObject private var viewModel = FlickrViewModel()
let columns = [
GridItem(.adaptive(minimum: 108), spacing: 1),
GridItem(.adaptive(minimum: 108), spacing: 1),
GridItem(.adaptive(minimum: 108), spacing: 1)
]
var body: some View {
NavigationStack {
ZStack {
Color.black.opacity(0.1)
ScrollView {
LazyVGrid(columns: columns, spacing: 1) {
ForEach(viewModel.photos) { photo in
NavigationLink(destination:
PhotoDetailView(photo: photo)) {
PhotoGridItem(photo: photo)
}
}
}
}
if viewModel.isLoading {
ProgressView()
.scaleEffect(1.5)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.1))
}
}
.navigationTitle("Flickr Photos")
.searchable(text: $viewModel.searchText, prompt: "Search photos")
.onChange(of: viewModel.searchText) { oldValue, newValue in
Task {
await viewModel.fetchPhotos()
}
}
.alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
Ключевые изменения:
- Удален вычисляемый параметр
filteredPhotos
изViewModel
, так как фильтрация теперь обрабатывается API - Изменен
ForEach(viewModel.filteredPhotos)
наForEach(viewModel.photos)
вContentView
- Остальные функции оставлены прежними, включая функцию поиска на основе тегов
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 8 работает в точности как и версия 7.
Но нас реально тревожит то, что при вводе “Rose”, “Sunrise”, “Ocean” в поисковой строке searchText
печать каждого символа (буквы) производит выборку с сервера Flickr.com, что приводит к неприятному мельканию на экране и будет сильно нагружать трафик пользователя на iPhone.
С этим надо что-то делать.
Продолжение разработки iOS приложения PhotomaniaCursor с помощью Cursor AI смотри в посте «Cursor AI в iOS разработке. Приложение «Фото с Flickr.com». Часть 2.».
Окончательный вариант iOS приложения PhotomaniaCursor находится на Github.com.