Мы исследуем как работает Cursor AI на примере создания iOS приложения с выборкой данных с ресурса публичных фотографий на Flickr.com.
UI этого iOS приложения представляет собой строку поиска вверху и сетку Grid
под ней для отображения миниатюр этих фотографий. Вы можете кликнуть на любую из фотографий и получить подробную информацию о ней:
Начальное решение этой задачи, то есть создание такого iOS приложения, описано в предыдущем посте «Cursor AI в iOS разработке. Приложение «Фото с Flickr.com». Часть 1.»
Окончательный вариант iOS приложения PhotomaniaCursor находится на Github.com.
Здесь мы продолжим совершенствовать наше приложение PhotomaniaCursor, которое может выбирать публичные фотографии с Flickr.com. И вот наш первый вопрос:
Как часто происходит обращение к Flickr API при наборе текста в строке поиска?
Наша следующая версия 8 приложения PhotomaniaCursor будет работать в точности как и версия 7, но мы хотим четко увидеть, при каких тегах tags
осуществляется обращение к Flickr API при наборе пользователем текста в строке поиска searchText
. Для этого мы будем печатать в FlickrViewModel
теги tags
, для которых этот запрос выполняется, :
if let encodedTags = tags.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
print ("--------------------------- \(encodedTags)")
urlString += "&tags=\(encodedTags)
}
Версия 8
Что нас реально тревожит, так это то, что при вводе “Rose”, “Sunrise”, “Ocean” в поисковой строке searchText
каждого следующего символа (буквы) осуществляется выборка из Flickr.com, что приводит к неприятному мельканию на экране и будет сильно нагружать трафик пользователя на iPhone.
Чтобы сэкономить деньги пользователя, мы должны сделать так. чтобы выборка данных с сервера Flickr.com производилась лишь тогда, когда пользователь закончит вводить данные в поисковой строке.
Это можно сделать с помощью оператора debounce
фреймворка Combine.
Оператор debounce фреймворка Combine
Оператора debounce
полезен, когда вы хотите реагировать на ввод пользователя или изменения данных, но не хотите обрабатывать каждое промежуточное значение. К распространенным случаям использования оператора debounce
относятся строки поиска, когда вы хотите дождаться, пока пользователь прекратит печатать, прежде чем начать поиск.
Давайте попросим Composer Cursor AI использовать его для нас:
Используй для FlickrViewModel Combine
и .debounce(for:.millisecond(500))
Я обновлю FlickrViewModel
для использования Combine с debounce
для изменений текста поиска searchText
:
import Foundation
import SwiftUI
import Combine
@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"
private var cancellables = Set<AnyCancellable>()
init() {
setupSearchSubscription()
}
private func setupSearchSubscription() {
$searchText
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] _ in
Task {
await self?.fetchPhotos()
}
}
.store(in: &cancellables)
}
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
}
}
}
И обновлю ContentView
, чтобы удалить ручной обработчик .onChange
, поскольку теперь мы обрабатываем его в ViewModel
:
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")
.alert("Error", isPresented:
.constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
Ключевые изменения:
- Добавлен
import Combine
и соответствующая функциональность вFlickrViewModel
- Добавлена переменная
var cancellables = Set<AnyCancellable>()
для хранения подписок - Создан метод
setupSearchSubscription()
сdebounce
- Удален обработчик
.onChange
изContentView
- Добавлено правильное управление памятью с
[weak self]
- Добавлен
removeDuplicates()
для предотвращения ненужных вызовов API
Теперь поиск будет запускаться только через 500 мс после того, как пользователь закончит печатать.Эта реализация обеспечит более плавный поиск за счет сокращения вызовов API и предотвращения немедленных запросов к серверу Flickr.com во время набора текста строке поиска searchText .
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 9
Мы видим, что практически нет попыток немедленных запросов к серверу Flickr.com, но малейшая неуверенность в наборе текста в строке поиска приводит к ненужным запросом, хотя и в меньшей степени, чем прежде.
Кэширование Flickr Photos
Давайте продолжим сокращать число ненужных запросов к серверу Flickr.com и организуем кэш для фотографий загруженных по определенному запросу и опять попросим Composer Cursor AI сделать это для нас:
Добавьте кэш для FlickrPhotos
Чтобы добавить кэширование для фотографий Flickr, нам нужно будет изменить FlickrViewModel
, включив механизм кэширования.
Мы будем использовать NSCache
для кэширования в памяти. Вот как мы можем обновить ContentView
и FlickrViewModel
:
Сначала давайте обновим FlickrViewModel:
import Foundation
import SwiftUI
import Combine
@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"
private var cancellables = Set<AnyCancellable>()
private let cache = NSCache<NSString, NSArray>()
init() {
setupSearchSubscription()
}
private func setupSearchSubscription() {
$searchText
.debounce(for: .milliseconds(500),
scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] _ in
Task {
await self?.fetchPhotos()
}
}
.store(in: &cancellables)
}
func fetchPhotos() async {
isLoading = true
errorMessage = nil
do {
let cacheKey = NSString(string: searchText)
if let cachedPhotos = cache.object(forKey: cacheKey)
as? [FlickrPhoto] {
photos = cachedPhotos
isLoading = false
return
}
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
cache.setObject(result.items as NSArray, forKey: cacheKey)
isLoading = false
} catch {
errorMessage = error.localizedDescription
isLoading = false
}
}
}
Теперь давайте обновим ContentView
, чтобы добавить кнопку обновления:
import SwiftUI
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")
.alert("Error", isPresented:
.constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
Task {
await viewModel.fetchPhotos()
}
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
Эти изменения реализуют следующее:
В FlickrViewModel:
- Мы добавили
NSCache
для хранения извлеченных фотографий. - Перед выполнением сетевого запроса мы проверяем, есть ли уже фотографии для текущего поискового запроса в кэше
NSCache
. - Если они найдены в кэше, мы используем кэшированные фотографии вместо выполнения сетевого запроса.
- После успешного получения новых фотографий мы сохраняем их в кэше.
В ContentView:
- Мы добавили кнопку обновления в навигационную панель, которая вызывает
viewModel.fetchPhotos()
при нажатии.
Эта реализация будет кэшировать фотографии в памяти, что улучшит производительность, когда пользователи используют одни и те же поисковые строки searchText
несколько раз или когда они возвращаются к главному View
после просмотра сведений о фотографии. Кнопка обновления позволяет пользователям вручную обновлять фотографии при необходимости, минуя кэш.
Помните, что этот кэш в памяти будет очищен при завершении работы приложения. Для более постоянного кэша вы можете рассмотреть возможность использования Core Data
или сохранения данных на диске.
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 10
После ввода нескольких поисковых строк searchText
формируется кэш с ключами, равными поисковой строке, в том числе и пустой и кнопка обновления ничего не дает, так как все “законсервировалось” наглухо, за исключением ситуации, когда обновляется поисковая строка searchText
и её нет в кэше. Кнопка обновления ничего не дает, так как она доступна только, когда в поисковая строка searchText
пуста, а для пустой строки работает кэш.После ввода нескольких поисковых строк searchText
формируется кэш с ключами, равными поисковой строке, в том числе и пустой и кнопка обновления ничего не дает, так как все “законсервировалось” наглухо, за исключением ситуации, когда обновляется поисковая строка searchText
и её нет в кэше. Кнопка обновления ничего не дает, так как она доступна только, когда в поисковая строка searchText
пуста, а для нее работает кэш.
Обнуление кеша
Кнопка обновления должна не только выбирать фотографии с помощью вызова viewModel.fetchPhotos()
, но и обнулять кэш.
Опять попросим Composer Cursor AI сделать это для нас:
Кнопка обновления должна не только выбирать фотографии путем вызова viewModel.fetchPhotos()
, но обновлять кэш.
Я помогу вам изменить FlickrViewModel
, чтобы добавить функцию обнуления кэша. Вот изменения:
import Foundation
import SwiftUI
import Combine
@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"
private var cancellables = Set<AnyCancellable>()
private let cache = NSCache<NSString, NSArray>()
init() {
setupSearchSubscription()
}
private func setupSearchSubscription() {
$searchText
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] _ in
Task {
await self?.fetchPhotos()
}
}
.store(in: &cancellables)
}
func fetchPhotos() async {
isLoading = true
errorMessage = nil
do {
let cacheKey = NSString(string: searchText)
if let cachedPhotos = cache.object(forKey: cacheKey) as? [FlickrPhoto] {
photos = cachedPhotos
isLoading = false
return
}
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
cache.setObject(result.items as NSArray, forKey: cacheKey)
isLoading = false
} catch {
errorMessage = error.localizedDescription
isLoading = false
}
}
func clearCache() {
cache.removeAllObjects()
}
func refreshPhotos() async {
clearCache()
await fetchPhotos()
}
}
Добавлены два новых метода в FlickrViewModel
:
clearCache()
для удаления всех кэшированных фотографийrefreshPhotos()
, который объединяет обнуление кеша и извлечение новых фотографий
Теперь, кнопка обновления при нажатии очистит кэш и извлечет свежие фотографии из API Flickr.
Если все объяснения нас устраивают, опять кликаем Accept All.
Версия 11
Вначале мы действуем как обычно: выбираем фотографии с тегами “Rose” и “Sunrise” с помощью API Flickr и они записываются в кэш. Если мы вообще очищаем поисковую строку, то перечень фотографий также выбирается из кэша, поскольку пустая строка searchText
также записывается в кэш со своими фотографиями:
let cacheKey = NSString(string: searchText)
Но если мы хотим выбрать свежую информацию, то кликаем на кнопке обновления, которая очищает кэш и извлекает свежие фотографии из API Flickr.
Коррекция Модели
Если мы внимательно посмотрим на подробную информацию о фотографии, то обнаружим, что дата получения фотографии dataUpload
имеет странный формат:
Это происходит потому, что в Модели свойства dataUpload
и published
, которые на самом деле являются датами, представлены как строки String
:
Так как структура FlickrPhoto
является Codable
, то мы могли бы преобразовать эти свойства в даты Date
при декодировании с помощью JSONDecoder()
.
Просим об этом Cursor AI:
В Модели свойства published
и dateTaken
используй как дату Date
и обнови JSONDecoder()
с JSONDecoder.DateDecodingStratege.iso8601
Я помогу вам изменить модель FlickrPhoto
и обновить стратегию декодера.
Вот изменения:
struct FlickrFeedResult: Codable {
let items: [FlickrPhoto]
}
struct FlickrPhoto: Codable, Identifiable {
let title: String
let link: String
let media: Media
let dateTaken: Date
let description: String
let published: Date
let author: String
let tags: String
// Computed property for ID
var id: String { link }
enum CodingKeys: String, CodingKey {
case title, link, media
case dateTaken = "date_taken"
case description, published, author, tags
}
}
struct Media: Codable {
let m: String
var originalURL: String {
m.replacingOccurrences(of: "_m.", with: ".")
}
}
И обновите JSONDecoder
() в FlickrViewModel
:
Вот предложенный код FlickrViewModel
:
// ... existing code ...
func fetchPhotos() async {
isLoading = true
errorMessage = nil
do {
let cacheKey = NSString(string: searchText)
if let cachedPhotos = cache.object(forKey: cacheKey) as? [FlickrPhoto] {
photos = cachedPhotos
isLoading = false
return
}
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 decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let result = try decoder.decode(FlickrFeedResult.self, from: data)
photos = result.items
cache.setObject(result.items as NSArray, forKey: cacheKey)
isLoading = false
} catch {
errorMessage = error.localizedDescription
isLoading = false
}
}
// ... rest of the code ...
Кроме того, нам необходимо обновить PhotoDetailView
, чтобы напрямую использовать новые Date
свойства:
// ... existing code ...
Text("Taken on \(formatDate(photo.dateTaken))")
.font(.subheadline)
.foregroundColor(.secondary)
// ... rest of the code ...
// Update formatDate to take Date instead of optional Date
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
return formatter.string(from: date)
}
Ключевые изменения:
- Сделайте свойства
dateTaken
иpublished
типамиDate
вместоString
- Настройте
JSONDecode
r для автоматической обработки дат ISO8601 - Удалите вычисляемое свойство
dateUpload
, поскольку теперь у нас есть свойствоpublished
как правильная датаDate
- Обновите функцию
formatDate
, чтобы она принимала НЕOptional Date
Это обеспечит надлежащую обработку дат Date
во всем приложении и более понятный код форматирования дат.
Теперь дата получения фотографии выглядит так:
Замена протокола ObservableObject на макрос @Observable
Используй макрос @Observable
для FlickrViewModel
и используй Combine
и var searchSubject = PassthroughSubject<String, Never>()
Я помогу вам обновить FlickrViewModel
для использования нового макроса @Observable
и Combine PassthroughSubject
. Вот измененная версия:
import Foundation
import SwiftUI
import Combine
@Observable
final class FlickrViewModel {
var photos: [FlickrPhoto] = []
var searchText = ""
var isLoading = false
var errorMessage: String?
private let baseURL = "https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1"
private var cancellables = Set<AnyCancellable>()
private let cache = NSCache<NSString, NSArray>()
private let searchSubject = PassthroughSubject<String, Never>()
init() {
setupSearchSubscription()
}
private func setupSearchSubscription() {
searchSubject
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.sink { [weak self] _ in
Task { @MainActor in
await self?.fetchPhotos()
}
}
.store(in: &cancellables)
}
func updateSearchText(_ newText: String) {
searchText = newText
searchSubject.send(newText)
}
@MainActor
func fetchPhotos() async {
isLoading = true
errorMessage = nil
do {
let cacheKey = NSString(string: searchText)
if let cachedPhotos =
cache.object(forKey: cacheKey) as? [FlickrPhoto] {
photos = cachedPhotos
isLoading = false
return
}
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 decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let result =
try decoder.decode(FlickrFeedResult.self, from: data)
photos = result.items
cache.setObject(result.items as NSArray,
forKey: cacheKey)
isLoading = false
} catch {
errorMessage = error.localizedDescription
isLoading = false
}
}
func clearCache() {
cache.removeAllObjects()
}
@MainActor
func refreshPhotos() async {
clearCache()
await fetchPhotos()
}
}
А вот обновленный ContentView
для работы с новой FlickrViewModel
:
struct ContentView: View {
@State 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: .init(
get: { viewModel.searchText },
set: { viewModel.updateSearchText($0) }
),
prompt: "Search photos"
)
.alert("Error", isPresented:
.constant(viewModel.errorMessage != nil)) {
Button("OK") {
viewModel.errorMessage = nil
}
} message: {
Text(viewModel.errorMessage ?? "")
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
Task {
await viewModel.refreshPhotos()
}
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
.task {
await viewModel.fetchPhotos()
}
}
}
Ключевые изменения:
- Заменены
@MainActo
r class и@Published
на новый макрос@Observable
- Добавлен
PassthroughSubject
для обработки текста поиска - Создан новый метод
updateSearchText
для обработки изменений текста поиска - Cделали
FlickrViewModel final class
- Изменен
@StateObject
на@State
вContentView
, поскольку мы используем@Observable
- Обновлён
.searchable
для использования нового методаupdateSearchText
- Добавлены явные аннотации
@MainActor
, где это необходимо - Удалено соответствие протоколу
ObservableObject,
поскольку оно больше не нужно
Эти изменения модернизируют код с использованием более современных функций Swift, сохраняя при этом ту же функциональность. Функциональность поиска теперь использует PassthroughSubject
для лучшего контроля над обновлениями текста поиска searchText
.
Очень крутой код, сочетающий макрос @Observable
с Combine
.
Посмотрим, как это работает. Для контроля того, что нет ненужных “паразитный” запросов к Flickr API при наборе строки поиска searchText
, будем как и раньше печатать tags
, для которых этот запрос выполняется:
if let encodedTags = tags.addingPercentEncoding (
withAllowedCharacters: .urlQueryAllowed) {
print ("--------------------------- \(encodedTags)")
urlString += "&tags=\(encodedTags)"
}
Версия 12
Мы видим, что все работает великолепно — никаких ненужных запросов при наборе в поисковой строке “Rose”, “Sunrise”, “Ocean” нет. Происходит однократная выборка и запись в кэш. Поэтому при повторном наборе “Sunrise” это слово на консоли не появляется, так как выборка фотографий идет из кэша.
Окончательный вариант iOS приложения PhotomaniaCursor находится на Github.com.
Отмечу, что я использовала LLM Claude 3.5 Sonnet:
… которая очень хороша для программирования, но будьте осторожны с Claude 3.5 Sonnet, которая почти всегда предоставляет работающее приложение, иногда с ошибками, которые очень трудно найти. Не поддавайтесь эйфории и не давайте ей завести себя в запутанную ситуацию, проверяйте и анализируйте код. К счастью, в этом проекте таких ситуаций не было.
Как видно, возможно использование и других LLM.
Работа с отдельным фрагментом кода
До сих пор мы использовали всего одну “фишку” Cursor AI — Composer, a их там много: Chat, ⌘K , ⌘L ,Tab, Editor и Terminal.
Например, с помощью ⌘ K можно рефакторить фрагмент кода, a можно запросить объяснения того, что делает этот код, и Cursor AI добавит вам комментарии, разъясняющие этот фрагмент кода — это отличный способ учиться программированию:
ЗАКЛЮЧЕНИЕ.
Используя Composer Cursor AI нам удалось создать с помощью нескольких шагов полноценное iOS приложение выборки и показа публичных фотографий с Flickr.com, не написав практически ни одной строчки кода на SwiftUI. В результате мы получили очень компактный и хорошо читаемый код, Это не значит, что не нужно знать Swift и SwiftUI, наоборот, все наши подсказки (Prompts) были сделаны на основе анализа полученного кода именно с учетом знаний Swift и SwiftUI.
Но код генерился в Cursor AI и запускался в Xcode так легко и быстро, что понадобилось всего несколько минут, чтобы получить такое приложение. Возможно, это объясняется квалифицированными подсказками (Prompts), но время на написание iOS приложения экономит невероятно.
Окончательный вариант iOS приложения PhotomaniaCursor находится на Github.com.
P.S.
Основой для этих постов стало видео ”I Made an iOS App in MINUTES with This AI Tool!” (“Я сделал это iOS приложение в течении нескольких минут с помощью этого ИИ инструмента”).
Я не верю, что без знания Swift и SwiftUI или UIKit можно создать полноценное iOS приложение, но мне было интересно, какие “фишки” использовал автор внутри Cursor AI, их там много: Chat, Composer, ⌘K , Tab, Editor и Terminal.
Оказалось, что автор этого видео использует только Cursor AI Composer, который работает с целым проектом. Это замечательное видео, вы можете его посмотреть или прочитать его русскоязычный конспект здесь. A также увидеть приложение “Приливы” Tide Times на Github, которое я создала, следуя этому видео. Но доступ к информации о “приливах” требует API key и первые 100 запросов вы делаете бесплатно, a затем нужна платная подписка.