Fixed section list resize and review request#152
Conversation
|
|
||
| screenshots/desktop/*.html | ||
| screenshots/desktop/ru/*.png | ||
| screenshots/desktop/en-US/*.png |
There was a problem hiding this comment.
Коротко и по делу. Ваш .gitignore стал лучше, но есть несколько проблем и мест для улучшения.
Критичные замечания
- Дубли и несогласованность путей:
- /.build/ и .build/ указаны оба. Оставьте один вариант. Рекомендую .build/ (без якоря), чтобы покрыть вложенные Swift Package’и.
- /DerivedData/ и DerivedData/ тоже дублируются. Хватит одного DerivedData/ (без якоря), если хотите игнорировать в любой вложенности.
- .build/ встречается дважды (вверху и ниже рядом с Package.pins). Оставьте один.
- Слишком жёсткая привязка к корню:
- /*.xcresult игнорирует только файлы в корне. В CI/скриптах результаты часто лежат во вложенных папках. Лучше использовать .xcresult (или **/.xcresult).
- /TestResults*/ привязан к корню и к началу имени. Если каталоги с результатами будут не в корне или с другим именем — пропустите. Я бы заменил на TestResults*/ без слеша в начале.
- /screenshots/**/*.… — ок, но обратите внимание, что якорь к корню означает, что screenshots должен быть в корне. Если структура может меняться — уберите ведущий слэш.
- CocoaPods: вы удалили Pods/ из .gitignore, значит собираетесь коммитить Pods. Это увеличит репозиторий, усложнит ревью третьих библиотек и часто избыточно. Если нет осознанной причины держать Pods в гите (офлайн-сборки, строгая воспроизводимость без шагов CI), рекомендую:
- игнорировать Pods/
- коммитить Podfile.lock
Если оставляете Pods в репозитории — зафиксируйте это в contributing/README, чтобы не было «дрейфа» практик в команде.
- Package.resolved: вы перестали его игнорировать. Для приложений это правильно (воспроизводимые сборки). Для библиотек — спорно, часто его игнорируют, чтобы не навязывать пины потребителям. Определите политику и зафиксируйте (для app — коммитить; для library — можно игнорировать).
Рекомендуемые добавления
- SwiftPM служебные файлы:
- .swiftpm/
- Отчеты покрытия/профилировки:
- *.xccovreport
- *.xccovarchive
- *.profdata
- Carthage (если используется):
- Carthage/Build/
- Carthage/Checkouts/ (опционально; многие коммитят Checkouts — определитесь с политикой)
- Fastlane артефакты (если используется):
- fastlane/report.xml
- fastlane/Preview.html
- fastlane/screenshots/**/*
- DocC артефакты:
- *.doccarchive
- IDE прочее (по желанию):
- .idea/
- *.iml
- Mac специфическое (мусорные файлы):
- ._*
Устаревшее/сомнительное
- Package.pins — пережиток старых версий SwiftPM. Можно удалить из .gitignore, если не поддерживаете старые тулчейны.
- Packages/ — тоже из старых версий SwiftPM. Если не поддерживаете старые проекты — удалите; иначе оставьте осознанно.
Предлагаемые правки (фрагменты .gitignore)
- Убрать дубли и ослабить якоря:
- Удалить: /.build/
- Оставить: .build/
- Удалить: /DerivedData/
- Оставить: DerivedData/
- Заменить: /*.xcresult -> *.xcresult
- Заменить: /TestResults*/ -> TestResults*/
- По желанию: /screenshots//*.png|html|mp4 -> screenshots//*.png|html|mp4
- Добавить:
- .swiftpm/
- *.xccovreport
- *.xccovarchive
- *.profdata
- fastlane/report.xml
- fastlane/Preview.html
- fastlane/screenshots/**/*
- *.doccarchive
Итог
- Уберите дубли (.build/, DerivedData/).
- Расслабьте привязку к корню для *.xcresult, TestResults, screenshots, чтобы не пропускать артефакты вне корня.
- Определите политику по Pods и Package.resolved и приведите .gitignore к этой политике.
- Добавьте игнор для .swiftpm и файлов покрытия/отчётов, если такие генерируются в вашем процессе.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Ниже — только то, что действительно стоит поправить или улучшить.
Критично
- Накопление высоты при перетаскивании. Вы используете currentSectionHeight + gesture.translation.height в onChanged, при этом translation — это смещение с начала жеста, а currentSectionHeight вы уже обновляете каждое событие. В итоге происходит “двойной счет” и рост/провал высоты. Правильно фиксировать начальную высоту в начале жеста и считать от неё.
Простой фикс:- Добавьте @State private var dragStartHeight: CGFloat?
- В onChanged, если dragStartHeight == nil — присвойте текущую. Далее height = clamp(dragStartHeight + translation.height). В onEnded — сбросьте dragStartHeight = nil и сохраните в AppStorage.
Типы и приведение
- Явно приводите CGFloat ↔ Double при записи/чтении из @AppStorage, чтобы не полагаться на неявные конверсии:
- onEnded: sectionHeight = Double(currentSectionHeight)
- onAppear: currentSectionHeight = CGFloat(sectionHeight)
Синхронизация состояния
- Если sectionHeight может меняться из другого места (другая сцена/окно), имеет смысл синхронизировать currentSectionHeight через .onChange(of: sectionHeight), игнорируя изменения во время жеста:
- .onChange(of: sectionHeight) { if !isDragging { currentSectionHeight = CGFloat($0) } }
- Для этого храните @State private var isDragging = false, переключая его в onChanged/onEnded.
Магические числа
- Вынесите 60.0 и 300.0 в константы (например, static let minHeight/maxHeight), чтобы не размазывать значения и упростить поддержку.
- Желательно вычислять max динамически относительно доступной высоты (GeometryReader), если макет может меняться.
UX/доступность резайзера
- Увеличьте кликабельную область и добавьте курсор:
- .contentShape(Rectangle()) на “ручке”
- .cursor(.resizeUpDown) на macOS
- Можно оставить визуальную толщину 6 pt, но сделать hit area больше с .padding(.vertical).
- Рассмотрите Divider() с overlay для системной адаптивности цветов.
Сохранение и область
- Подумайте, нужен ли AppStorage (глобально на приложение) или SceneStorage (пер-сцена/окно). Для многооконных приложений логичнее SceneStorage, чтобы у каждого окна была своя высота.
Анимации и отзывчивость
- Обычно при драге анимация не нужна. Если хотите плавности при броске — анимируйте только onEnded (.animation(..., value: currentSectionHeight)).
Небольшие замечания по чистоте
- @query var tasks: [Todo] в этом фрагменте не используется — удалите, чтобы не делать лишний запрос.
- Имена: currentSectionHeight и sectionHeight понятны, но можно сделать ещё явнее: persistedSectionHeight и liveSectionHeight.
Пример корректного жеста (минимальный фрагмент):
-
@State private var dragStartHeight: CGFloat?
-
@State private var isDragging = false
-
let minH: CGFloat = 60
-
let maxH: CGFloat = 300
DragGesture()
.onChanged { value in
if dragStartHeight == nil {
dragStartHeight = currentSectionHeight
isDragging = true
}
let base = dragStartHeight ?? currentSectionHeight
let newHeight = (base + value.translation.height).clamped(to: minH...maxH)
currentSectionHeight = newHeight
}
.onEnded { value in
let base = dragStartHeight ?? currentSectionHeight
let finalHeight = (base + value.translation.height).clamped(to: minH...maxH)
currentSectionHeight = finalHeight
sectionHeight = Double(finalHeight)
dragStartHeight = nil
isDragging = false
}
И утилита для клэмпа:
- extension Comparable {
func clamped(to range: ClosedRange) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}
Итого: главная проблема — неверная логика перерасчёта высоты во время перетаскивания. Остальное — улучшения по устойчивости, UX и аккуратности кода.
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Вот по делу — что можно улучшить и где есть риски/ошибки.
-
Неверный API для запроса оценки. В коде используется AppStore.requestReview(in:), которого нет в StoreKit. Правильно:
- Через SwiftUI-окружение: @Environment(.requestReview) var requestReview; затем requestReview()
- Или напрямую: SKStoreReviewController.requestReview(in: scene)
Рекомендую второй вариант обернуть в безопасный поиск активной сцены и вызывать на главном потоке: - if let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first(where: { $0.activationState == .foregroundActive }) { SKStoreReviewController.requestReview(in: scene) }
-
Вызов из неактивной сцены. Сейчас берется первый connectedScenes.first, что на iPad/мультиоконности может оказаться не тем окном. Фильтруйте по foregroundActive, как выше.
-
Потокобезопасность. checkForReview должен выполняться на главном потоке (и доступ к UIApplication.shared тоже). Аннотируйте функцию как @mainactor или гарантируйте вызов с main. Раньше был Task @mainactor — сейчас это убрано.
-
Оставленные отладочные print. Их стоит убрать или завернуть в #if DEBUG, чтобы не засорять логи в проде.
-
Логика блокировки повторных запросов. Установка firstLaunchDate = .distantFuture — это хрупкий трюк. Лучше хранить:
- дату последнего запроса (lastReviewRequestDate),
- и/или флаг «запросили для версии X» (lastVersionRequestedForReview),
— тогда не придется перегружать смысл firstLaunchDate и будет проще контролировать стратегию (например, 1 запрос на версию, минимум через N дней от последнего).
-
Выбор версии. Вы сравниваете CFBundleShortVersionString. Если вы иногда выкатываете билды с тем же маркетинговым номером, но новым build number, логика «новая версия» не сработает. Если нужно различать билды — включите CFBundleVersion в сравнение (например, "shortVersion (build)").
-
@AppStorage с Date. Хотя UserDefaults поддерживает Date, @AppStorage официально гарантирует работу для примитивов и RawRepresentable. На практике Date обычно работает, но ради надежности можно хранить TimeInterval (Double) — меньше сюрпризов при миграциях. Тем более, вы считаете дни — с Double будет проще.
-
Семантика ключей. savedVersion с ключом "appVersion" — по смыслу это "lastSeenAppVersion". Переименуйте ключ (и переменную) для читаемости и избежания путаницы.
-
UX/Guidelines. Apple рекомендует запрашивать отзыв «в ответ на позитивное действие», а не по таймеру/дате. Привяжите checkForReview к событию (например, успешное завершение N рабочих сессий) и добавьте условие частоты, чтобы не триггерить на каждый запуск на 8-й день.
-
onChange(of:scenePhase) сигнатура. Использование двух параметров (_, newPhase) — это API из iOS 17. Убедитесь, что минимальная версия — iOS 17+. Если нет — верните однопараметровый вариант.
-
Регрессия для UI-тестов. Вы убрали блок, отключающий анимации при UITEST_DISABLE_ANIMATIONS. Если UI-тесты опирались на это — тесты станут flaky. Верните в тестовой сборке/при запуске с флагом.
-
Централизация логики. Вынесите логику отзывов в отдельный ReviewRequestManager (ObservableObject/сервис) и дергайте его из View. Это упростит тестирование, переиспользование и уберет «бизнес-логику» из View.
Минимальная правка запроса оценки с учетом сцены/потока:
- @mainactor
private func requestReviewIfNeeded() {
guard daysSinceFirstLaunch >= daysBeforeRequest else { return }
if let scene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }) {
SKStoreReviewController.requestReview(in: scene)
// пометка, чтобы не повторять
lastVersionRequestedForReview = currentVersion
}
}
И еще: храните и проверяйте lastVersionRequestedForReview, а не «забивать» firstLaunchDate на distantFuture — будет прозрачнее и контролируемее.
| <key>com.apple.security.files.user-selected.read-only</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Ключевые замечания по изменению entitlements:
Критично
-
Удаление com.apple.security.app-sandbox
- macOS: это почти наверняка ошибка. Без App Sandbox:
- Приложение не пройдет Mac App Store ревью.
- application-groups перестанет работать (групповой контейнер вернет nil).
- iCloud KVS (com.apple.developer.ubiquity-kvstore-identifier) не будет доступен.
- Возможны ошибки подписывания/загрузки из‑за несоответствия профилю.
- iOS: ключ не нужен и его наличие ранее намекает на путаницу между таргетами. Разнесите entitlements по платформам.
- macOS: это почти наверняка ошибка. Без App Sandbox:
-
Удаление com.apple.security.files.user-selected.read-only
- macOS (в песочнице): если вы открываете файлы через NSOpenPanel/UIDocumentPicker и ожидаете доступ вне контейнера, вам нужна одна из:
- com.apple.security.files.user-selected.read-only или
- com.apple.security.files.user-selected.read-write (если пишете).
- Если намеренно отключили песочницу (что само по себе плохая идея для macOS/MAS), этот ключ не нужен — но это конфликтует с необходимостью для App Groups и iCloud.
- macOS (в песочнице): если вы открываете файлы через NSOpenPanel/UIDocumentPicker и ожидаете доступ вне контейнера, вам нужна одна из:
Согласованность с профилями и возможные “Invalid Entitlement”
- Если профили подписи сгенерированы с включенной песочницей/файловыми доступами, после удаления ключей получите несоответствие. Проверьте/пересоберите профили для каждого таргета.
iCloud KVS
- com.apple.developer.ubiquity-kvstore-identifier работает только при корректно включенной возможности iCloud и соответствующем профиле. На macOS требует песочницу. На iOS — ок.
- Убедитесь, что это действительно то, что нужно: KVS ограничен по объему и предназначен для мелких настроек; для сложных данных — CloudKit.
Application Groups
- На macOS без песочницы entitlement игнорируется.
- На iOS — ок. Проверьте, что group.com.amikhaylin.PomPadDo создан в Dev Portal и одинаково включен во всех таргетах (приложение/виджеты/расширения).
Рекомендации по исправлению
-
Если таргет iOS:
- Оставьте как есть (без app-sandbox и без user-selected.*).
- Заверьте, что профиль содержит App Groups и iCloud KVS по необходимости.
- Разделите entitlements для iOS и macOS, чтобы избежать повторной путаницы.
-
Если таргет macOS:
- Верните com.apple.security.app-sandbox = true.
- Верните com.apple.security.files.user-selected.* согласно фактическому сценарию (read-only vs read-write). Если нужны постоянные права — используйте security-scoped bookmarks.
- Убедитесь, что App Groups и ubiquity-kvstore работают (containerURL(forSecurityApplicationGroupIdentifier:) не возвращает nil; NSUbiquitousKeyValueStore.default.synchronize() не падает).
- Перегенерируйте профили с этими entitlements.
Небольшие, но полезные замечания
- Поддерживайте отдельные .entitlements файлы per target/platform (и при необходимости per configuration), вместо «универсального» plist.
- Проверьте регистр/стиль идентификаторов групп (лучше придерживаться нижнего регистра и стабильного нейминга).
- Если у вас была попытка «починить доступ к файлам» отключением песочницы — это неправильный путь. Верните песочницу и корректно настройте Powerbox/SSB и соответствующие entitlements.
| UIView.setAnimationsEnabled(false) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Ниже только то, что может улучшить надежность/переносимость и предсказуемость поведения.
-
UIKit-зависимость и кросс-платформенность:
- Если файл собирается не только под iOS/tvOS, оберните вызов в #if canImport(UIKit) (или #if os(iOS) || os(tvOS)) и не забудьте import UIKit. Иначе сборка под macOS/visionOS может упасть.
-
Поток выполнения:
- UIView.setAnimationsEnabled(false) должен вызываться на главном потоке. Без гарантии main-thread это потенциальная гонка/undefined behavior. Оберните в DispatchQueue.main.async { … } или выполните внутри @mainactor.
-
Формат флага из окружения:
- Жёсткая проверка на "YES" хрупкая. Лучше парсить несколько вариантов ("1", "true", "yes", регистр не учитывать). Вынесите в небольшую утилиту/extension, чтобы избежать дублирования и опечаток.
- Рассмотрите поддержку launchArguments (например, -UITestDisableAnimations) — их проще использовать в тестах и они читаются так же рано.
-
SwiftUI-анимации не все отключатся:
- UIView.setAnimationsEnabled(false) не гарантирует отключение всех анимаций SwiftUI (особенно withAnimation/implicit). Если нужна абсолютная предсказуемость в UI-тестах:
- На корневом вью при флаге из окружения добавьте .transaction { $0.animation = nil } — это «глушит» SwiftUI-анимации в поддереве.
- Опционально можно проставить .environment(.accessibilityReduceMotion, true) — многие системные анимации будут уменьшены/отключены (не всё, но помогает).
- Пример применения в WindowGroup при флаге: RootView().transaction { $0.animation = nil }.
- UIView.setAnimationsEnabled(false) не гарантирует отключение всех анимаций SwiftUI (особенно withAnimation/implicit). Если нужна абсолютная предсказуемость в UI-тестах:
-
Стабильность при смене активного состояния:
- На некоторых конфигурациях сторонний код может «включить» анимации позже. Подстрахуйте повторной установкой при активации сцены:
- .onChange(of: scenePhase) { if phase == .active && isUITestFlag { UIView.setAnimationsEnabled(false) } }
- На некоторых конфигурациях сторонний код может «включить» анимации позже. Подстрахуйте повторной установкой при активации сцены:
-
Мелкие улучшения DX:
- Вынесите строковый ключ "UITEST_DISABLE_ANIMATIONS" в константу.
- Локально логируйте факт отключения (только в DEBUG), это экономит время при диагностике, не засоряя релиз.
Иллюстративно (сжато):
init() {
#if canImport(UIKit)
let isUITest = ProcessInfo.processInfo.uiTestDisablesAnimations
guard isUITest else { return }
DispatchQueue.main.async {
UIView.setAnimationsEnabled(false)
}
#endif
}
extension ProcessInfo {
var uiTestDisablesAnimations: Bool {
if arguments.contains("-UITestDisableAnimations") { return true }
guard let v = environment["UITEST_DISABLE_ANIMATIONS"]?.lowercased() else { return false }
return v == "1" || v == "true" || v == "yes"
}
}
И в корневом вью (когда isUITest == true):
RootView()
.transaction { $0.animation = nil }
.environment(.accessibilityReduceMotion, true)
| LastUpgradeVersion = "2610" | ||
| version = "1.7"> | ||
| <BuildAction | ||
| parallelizeBuildables = "YES" |
There was a problem hiding this comment.
Коротко по делу:
- Значение 2610 в LastUpgradeVersion выглядит ошибочным. Для Xcode это поле кодируется как 1600/1610/1620/1630 и т.п. (Xcode 16.0/16.1/16.2/16.3). 2610 — нереальный номер для текущих версий и, скорее всего, опечатка.
- Риск: завышенное значение «обманывает» Xcode — он может не предложить миграции/рекомендованные настройки в будущем, что приведет к скрытым несоответствиям в проектных настройках.
- Рекомендация: не править это поле вручную. Пусть Xcode сам обновит его при «Update to recommended settings». Если нужно задать вручную — поставьте корректное значение для вашей версии Xcode (например, 1630 для 16.3).
- Согласованность: если действительно обновляете, синхронизируйте аналогичные маркеры в проекте — LastUpgradeCheck в project.pbxproj и LastUpgradeVersion во всех .xcscheme, чтобы избежать «шума» и расхождений.
- Процессно: зафиксируйте версию Xcode для команды/CI (документация/xcodes/минимальная версия) и не коммитьте одиночные правки схем без реальной миграции проекта.
| LastUpgradeVersion = "2610" | ||
| wasCreatedForAppExtension = "YES" | ||
| version = "2.0"> | ||
| <BuildAction |
There was a problem hiding this comment.
Замечания по PR:
-
Значение LastUpgradeVersion="2610" выглядит некорректным. Для схем Xcode используется четырехзначный код, соответствующий версии Xcode:
- 15.0 → 1500, 15.1 → 1510, …, 15.4 → 1540
- 16.0 → 1600, 16.1 → 1610, 16.2 → 1620, 16.3 → 1630, 16.4 → 1640
2610 не соответствует реальным версиям Xcode и, вероятно, попадёт обратно в валидное значение при следующем сохранении схемы в Xcode. Это создаст «шум» в истории коммитов.
-
Рекомендации:
- Не редактировать LastUpgradeVersion вручную. Откройте проект в целевой версии Xcode, зайдите в Manage Schemes и просто сохраните схему — Xcode сам проставит корректное значение.
- Приведите LastUpgradeVersion в схемах в соответствие реальной версии Xcode, которой вы пользовались (например, 1640 для Xcode 16.4).
- Проверьте согласованность с Project.pbxproj (ключ LastUpgradeCheck) — не обязаны быть идентичны, но большие расхождения обычно признак случайного/ошибочного правки.
- Добавьте в CI/прехук быстрый линт: отклонять значения LastUpgradeVersion вне допустимого диапазона (например, 1500–1699 на сегодня), чтобы избежать случайных правок.
Итог: предлагаю отклонить данный change и либо оставить прежнее 1640, либо обновить на корректный код версии Xcode, которой вы реально мигрировали схему.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Ниже — только то, что реально требует исправления или даёт ощутимое улучшение.
- Логика drag-изменения высоты некорректна: вы складываете translation (который всегда считается от начала жеста) с уже изменённым currentSectionHeight. В результате высота «убегает». Нужна базовая высота, зафиксированная в начале жеста, и вычисление относительно неё.
- Несоответствие типов: sectionHeight — Double, currentSectionHeight — CGFloat. В onEnded вы делаете sectionHeight = currentSectionHeight без конверсии — это не скомпилируется. Нужен явный Double(currentSectionHeight).
- Дублирование «магических чисел» и смешение типов в клампинге. Лучше вынести min/max в константы CGFloat, а конвертировать в Double только при записи в AppStorage.
- Синхронизация с внешними изменениями AppStorage. Сейчас вы синхронизируете только в onAppear. Если значение SectionHeight поменяется извне (другим окном/вью), currentSectionHeight не обновится. Добавьте onChange(of: sectionHeight).
Предлагаемый минимальный патч (ключевые места):
-
Константы и состояния:
-
Жест:
- .gesture(
DragGesture()
.onChanged { value in
if dragStartHeight == nil { dragStartHeight = currentSectionHeight }
let proposed = (dragStartHeight ?? currentSectionHeight) + value.translation.height
currentSectionHeight = min(max(proposed, minSectionHeight), maxSectionHeight)
}
.onEnded { value in
let final = (dragStartHeight ?? currentSectionHeight) + value.translation.height
let clamped = min(max(final, minSectionHeight), maxSectionHeight)
currentSectionHeight = clamped
sectionHeight = Double(clamped) // явная конверсия
dragStartHeight = nil
}
)
- .gesture(
-
Инициализация и синхронизация:
- .onAppear { currentSectionHeight = CGFloat(sectionHeight) }
- .onChange(of: sectionHeight) { newValue in
currentSectionHeight = CGFloat(newValue)
}
Это устраняет «убегающую» высоту, ошибки типов, снижает риск рассинхронизации и убирает дублирование магических чисел.
| <key>com.apple.security.personal-information.calendars</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Ниже — только то, что действительно может быть проблемой или источником недопонимания в этом изменении.
-
Критично: вы удалили com.apple.security.app-sandbox. На macOS это значит:
- App Store: приложение не пройдёт ревью (Sandbox обязателен).
- iCloud KVS (com.apple.developer.ubiquity-kvstore-identifier) перестанет работать — iCloud на macOS требует Sandbox. Держать этот ключ без Sandbox бессмысленно и может привести к ошибкам при подписи/нотаризации.
- Если цель — Developer ID вне MAS: iCloud (включая KVS/CloudKit) недоступен. Уберите com.apple.developer.ubiquity-kvstore-identifier, иначе возможны «invalid entitlements» при codesign/notarytool.
-
Согласованность с возможностями:
- Если вы реально используете календари (EventKit), то без Sandbox entitlement больше не нужен, но TCC всё равно потребует NSCalendarsUsageDescription в Info.plist. Проверьте наличие этого ключа.
- Network entitlement (com.apple.security.network.client) нужен только в песочнице; вне Sandbox он не нужен. Но если вы когда-либо вернётесь к Sandbox — не забудьте вернуть его, иначе сеть будет заблокирована.
-
Определитесь с целевой платформой и каналом дистрибуции:
- macOS, Mac App Store: верните Sandbox и соответствующие entitlements (network, calendars, files.user-selected.read-write если нужны) — иначе не пройдёте ревью и потеряете iCloud.
- macOS, Developer ID (вне MAS): удалите все iCloud-энтайтлы (включая текущий kvstore), оставьте Hardened Runtime, проведите повторную проверку codesign/notarytool.
- iOS: com.apple.security.* не применимы и не нужны; com.apple.developer.ubiquity-kvstore-identifier остаётся корректным при включённой iCloud Capability.
-
Внимание к значению kvstore-идентификатора:
-
$(TeamIdentifierPrefix)$ (CFBundleIdentifier) обычно корректно разворачивается в TEAMID.com.example.app. Сам по себе ок, но, повторюсь, на macOS без Sandbox и/или вне MAS использовать нельзя.
-
-
Потенциальные структурные проблемы plist:
- В диффе виден закрывающий . Убедитесь, что после удаления ключей XML остаётся валидным (правильные пары …, …). Прогоните plutil -lint.
-
Рекомендация по управлению вариантами:
- Завести отдельные entitlements для разных таргетов/сборок (MAS vs Developer ID), чтобы не миксовать iCloud и отсутствие Sandbox.
- Автоматизировать проверку: scripts, которые на CI запускают codesign -d --entitlements :-, plutil -lint, notarytool submit --dry-run.
Кратко: если вы сознательно уходите от Sandbox на macOS, обязательно уберите iCloud KVS entitlement и проверьте TCC-ключи в Info.plist. Если же iCloud нужен — верните Sandbox и недостающие entitlements, иначе функциональность и дистрибуция сломаются.
| // Switch to board view | ||
| if project.isBoard { | ||
| app/*@START_MENU_TOKEN@*/.radioButtons["rectangle.split.3x1"]/*[[".radioGroups[\"View Mode\"].radioButtons",".radioGroups",".radioButtons[\"Column View\"]",".radioButtons[\"rectangle.split.3x1\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.click() | ||
|
|
There was a problem hiding this comment.
Ниже — только конструктивные замечания по показанному фрагменту UI‑теста.
-
Уберите артефакты UI‑Recorder’а. Ветка с /@START_MENU_TOKEN@/ … /@END_MENU_TOKEN@/ делает селектор хрупким и нечитаемым. Оставьте один понятный запрос.
-
Не используйте SF Symbol/лейбл как ключ. "rectangle.split.3x1" и "View Mode" (а также "Column View") завязаны на локализацию/иконки. Для UI‑тестов нужны стабильные accessibilityIdentifier. Задайте идентификаторы в приложении (например, radio group: "viewMode", radio: "viewMode.board").
-
Избегайте firstMatch без проверки. Это маскирует неоднозначность. Либо настраивайте точный селектор, либо проверяйте, что найден единственный элемент, либо хотя бы делайте wait + exists.
-
Добавьте явное ожидание перед кликом и проверку результата. Перед .click() — waitForExistence/isHittable. После — проверка, что нужный экран действительно активен (например, появление/выбор BoardView).
-
Логика условия может быть двусмысленной. if project.isBoard читается как «проект уже в режиме Board». Тогда клик может быть избыточным. Либо переименуйте флаг (shouldUseBoardView), либо кликайте только если radio не выбран, либо сначала утверждайте текущее состояние.
-
Ограничьте область поиска. Запрашивайте кнопку внутри конкретной radioGroup/toolbar, чтобы исключить конфликты одинаковых иконок в разных местах.
-
Вынесите в «page object»/хелпер. Повторяющиеся действия (выбор режима) — в метод с ожиданиями и проверками.
Предлагаемый фрагмент (пример для macOS UI Tests), с учётом стабильных идентификаторов:
// Switch to board view
if project.shouldUseBoardView {
let viewMode = app.radioGroups["viewMode"]
let board = viewMode.radioButtons["viewMode.board"]
XCTAssertTrue(board.waitForExistence(timeout: 2), "Кнопка board не найдена")
if !(board.value as? String == "1") { // или board.isSelected, если доступно
XCTAssertTrue(board.isHittable, "Кнопка board недоступна для клика")
board.click()
}
// Верификация, что реально переключились на board
XCTAssertTrue(app.collectionViews["boardView"].waitForExistence(timeout: 3), "BoardView не появился")
}
Или обёртка:
func selectViewModeBoard(app: XCUIApplication) {
let viewMode = app.radioGroups["viewMode"]
let board = viewMode.radioButtons["viewMode.board"]
XCTAssertTrue(board.waitForExistence(timeout: 2))
if !(board.value as? String == "1") {
board.click()
}
XCTAssertTrue(app.collectionViews["boardView"].waitForExistence(timeout: 3))
}
Ключевые улучшения: избавиться от recorder‑комментариев, перейти на стабильные accessibilityIdentifier, убрать firstMatch, добавить ожидания и проверку результата, и сделать код декларативнее через хелперы/enum режима.
|
|
||
| screenshots/desktop/*.html | ||
| screenshots/desktop/ru/*.png | ||
| screenshots/desktop/en-US/*.png |
There was a problem hiding this comment.
Коротко и по делу. Ваш .gitignore стал лучше, но есть несколько проблем и мест для улучшения.
Критичные замечания
- Дубли и несогласованность путей:
- /.build/ и .build/ указаны оба. Оставьте один вариант. Рекомендую .build/ (без якоря), чтобы покрыть вложенные Swift Package’и.
- /DerivedData/ и DerivedData/ тоже дублируются. Хватит одного DerivedData/ (без якоря), если хотите игнорировать в любой вложенности.
- .build/ встречается дважды (вверху и ниже рядом с Package.pins). Оставьте один.
- Слишком жёсткая привязка к корню:
- /*.xcresult игнорирует только файлы в корне. В CI/скриптах результаты часто лежат во вложенных папках. Лучше использовать .xcresult (или **/.xcresult).
- /TestResults*/ привязан к корню и к началу имени. Если каталоги с результатами будут не в корне или с другим именем — пропустите. Я бы заменил на TestResults*/ без слеша в начале.
- /screenshots/**/*.… — ок, но обратите внимание, что якорь к корню означает, что screenshots должен быть в корне. Если структура может меняться — уберите ведущий слэш.
- CocoaPods: вы удалили Pods/ из .gitignore, значит собираетесь коммитить Pods. Это увеличит репозиторий, усложнит ревью третьих библиотек и часто избыточно. Если нет осознанной причины держать Pods в гите (офлайн-сборки, строгая воспроизводимость без шагов CI), рекомендую:
- игнорировать Pods/
- коммитить Podfile.lock
Если оставляете Pods в репозитории — зафиксируйте это в contributing/README, чтобы не было «дрейфа» практик в команде.
- Package.resolved: вы перестали его игнорировать. Для приложений это правильно (воспроизводимые сборки). Для библиотек — спорно, часто его игнорируют, чтобы не навязывать пины потребителям. Определите политику и зафиксируйте (для app — коммитить; для library — можно игнорировать).
Рекомендуемые добавления
- SwiftPM служебные файлы:
- .swiftpm/
- Отчеты покрытия/профилировки:
- *.xccovreport
- *.xccovarchive
- *.profdata
- Carthage (если используется):
- Carthage/Build/
- Carthage/Checkouts/ (опционально; многие коммитят Checkouts — определитесь с политикой)
- Fastlane артефакты (если используется):
- fastlane/report.xml
- fastlane/Preview.html
- fastlane/screenshots/**/*
- DocC артефакты:
- *.doccarchive
- IDE прочее (по желанию):
- .idea/
- *.iml
- Mac специфическое (мусорные файлы):
- ._*
Устаревшее/сомнительное
- Package.pins — пережиток старых версий SwiftPM. Можно удалить из .gitignore, если не поддерживаете старые тулчейны.
- Packages/ — тоже из старых версий SwiftPM. Если не поддерживаете старые проекты — удалите; иначе оставьте осознанно.
Предлагаемые правки (фрагменты .gitignore)
- Убрать дубли и ослабить якоря:
- Удалить: /.build/
- Оставить: .build/
- Удалить: /DerivedData/
- Оставить: DerivedData/
- Заменить: /*.xcresult -> *.xcresult
- Заменить: /TestResults*/ -> TestResults*/
- По желанию: /screenshots//*.png|html|mp4 -> screenshots//*.png|html|mp4
- Добавить:
- .swiftpm/
- *.xccovreport
- *.xccovarchive
- *.profdata
- fastlane/report.xml
- fastlane/Preview.html
- fastlane/screenshots/**/*
- *.doccarchive
Итог
- Уберите дубли (.build/, DerivedData/).
- Расслабьте привязку к корню для *.xcresult, TestResults, screenshots, чтобы не пропускать артефакты вне корня.
- Определите политику по Pods и Package.resolved и приведите .gitignore к этой политике.
- Добавьте игнор для .swiftpm и файлов покрытия/отчётов, если такие генерируются в вашем процессе.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Ниже — только то, что действительно стоит поправить или улучшить.
Критично
- Накопление высоты при перетаскивании. Вы используете currentSectionHeight + gesture.translation.height в onChanged, при этом translation — это смещение с начала жеста, а currentSectionHeight вы уже обновляете каждое событие. В итоге происходит “двойной счет” и рост/провал высоты. Правильно фиксировать начальную высоту в начале жеста и считать от неё.
Простой фикс:- Добавьте @State private var dragStartHeight: CGFloat?
- В onChanged, если dragStartHeight == nil — присвойте текущую. Далее height = clamp(dragStartHeight + translation.height). В onEnded — сбросьте dragStartHeight = nil и сохраните в AppStorage.
Типы и приведение
- Явно приводите CGFloat ↔ Double при записи/чтении из @AppStorage, чтобы не полагаться на неявные конверсии:
- onEnded: sectionHeight = Double(currentSectionHeight)
- onAppear: currentSectionHeight = CGFloat(sectionHeight)
Синхронизация состояния
- Если sectionHeight может меняться из другого места (другая сцена/окно), имеет смысл синхронизировать currentSectionHeight через .onChange(of: sectionHeight), игнорируя изменения во время жеста:
- .onChange(of: sectionHeight) { if !isDragging { currentSectionHeight = CGFloat($0) } }
- Для этого храните @State private var isDragging = false, переключая его в onChanged/onEnded.
Магические числа
- Вынесите 60.0 и 300.0 в константы (например, static let minHeight/maxHeight), чтобы не размазывать значения и упростить поддержку.
- Желательно вычислять max динамически относительно доступной высоты (GeometryReader), если макет может меняться.
UX/доступность резайзера
- Увеличьте кликабельную область и добавьте курсор:
- .contentShape(Rectangle()) на “ручке”
- .cursor(.resizeUpDown) на macOS
- Можно оставить визуальную толщину 6 pt, но сделать hit area больше с .padding(.vertical).
- Рассмотрите Divider() с overlay для системной адаптивности цветов.
Сохранение и область
- Подумайте, нужен ли AppStorage (глобально на приложение) или SceneStorage (пер-сцена/окно). Для многооконных приложений логичнее SceneStorage, чтобы у каждого окна была своя высота.
Анимации и отзывчивость
- Обычно при драге анимация не нужна. Если хотите плавности при броске — анимируйте только onEnded (.animation(..., value: currentSectionHeight)).
Небольшие замечания по чистоте
- @query var tasks: [Todo] в этом фрагменте не используется — удалите, чтобы не делать лишний запрос.
- Имена: currentSectionHeight и sectionHeight понятны, но можно сделать ещё явнее: persistedSectionHeight и liveSectionHeight.
Пример корректного жеста (минимальный фрагмент):
-
@State private var dragStartHeight: CGFloat?
-
@State private var isDragging = false
-
let minH: CGFloat = 60
-
let maxH: CGFloat = 300
DragGesture()
.onChanged { value in
if dragStartHeight == nil {
dragStartHeight = currentSectionHeight
isDragging = true
}
let base = dragStartHeight ?? currentSectionHeight
let newHeight = (base + value.translation.height).clamped(to: minH...maxH)
currentSectionHeight = newHeight
}
.onEnded { value in
let base = dragStartHeight ?? currentSectionHeight
let finalHeight = (base + value.translation.height).clamped(to: minH...maxH)
currentSectionHeight = finalHeight
sectionHeight = Double(finalHeight)
dragStartHeight = nil
isDragging = false
}
И утилита для клэмпа:
- extension Comparable {
func clamped(to range: ClosedRange) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}
Итого: главная проблема — неверная логика перерасчёта высоты во время перетаскивания. Остальное — улучшения по устойчивости, UX и аккуратности кода.
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Вот по делу — что можно улучшить и где есть риски/ошибки.
-
Неверный API для запроса оценки. В коде используется AppStore.requestReview(in:), которого нет в StoreKit. Правильно:
- Через SwiftUI-окружение: @Environment(.requestReview) var requestReview; затем requestReview()
- Или напрямую: SKStoreReviewController.requestReview(in: scene)
Рекомендую второй вариант обернуть в безопасный поиск активной сцены и вызывать на главном потоке: - if let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first(where: { $0.activationState == .foregroundActive }) { SKStoreReviewController.requestReview(in: scene) }
-
Вызов из неактивной сцены. Сейчас берется первый connectedScenes.first, что на iPad/мультиоконности может оказаться не тем окном. Фильтруйте по foregroundActive, как выше.
-
Потокобезопасность. checkForReview должен выполняться на главном потоке (и доступ к UIApplication.shared тоже). Аннотируйте функцию как @mainactor или гарантируйте вызов с main. Раньше был Task @mainactor — сейчас это убрано.
-
Оставленные отладочные print. Их стоит убрать или завернуть в #if DEBUG, чтобы не засорять логи в проде.
-
Логика блокировки повторных запросов. Установка firstLaunchDate = .distantFuture — это хрупкий трюк. Лучше хранить:
- дату последнего запроса (lastReviewRequestDate),
- и/или флаг «запросили для версии X» (lastVersionRequestedForReview),
— тогда не придется перегружать смысл firstLaunchDate и будет проще контролировать стратегию (например, 1 запрос на версию, минимум через N дней от последнего).
-
Выбор версии. Вы сравниваете CFBundleShortVersionString. Если вы иногда выкатываете билды с тем же маркетинговым номером, но новым build number, логика «новая версия» не сработает. Если нужно различать билды — включите CFBundleVersion в сравнение (например, "shortVersion (build)").
-
@AppStorage с Date. Хотя UserDefaults поддерживает Date, @AppStorage официально гарантирует работу для примитивов и RawRepresentable. На практике Date обычно работает, но ради надежности можно хранить TimeInterval (Double) — меньше сюрпризов при миграциях. Тем более, вы считаете дни — с Double будет проще.
-
Семантика ключей. savedVersion с ключом "appVersion" — по смыслу это "lastSeenAppVersion". Переименуйте ключ (и переменную) для читаемости и избежания путаницы.
-
UX/Guidelines. Apple рекомендует запрашивать отзыв «в ответ на позитивное действие», а не по таймеру/дате. Привяжите checkForReview к событию (например, успешное завершение N рабочих сессий) и добавьте условие частоты, чтобы не триггерить на каждый запуск на 8-й день.
-
onChange(of:scenePhase) сигнатура. Использование двух параметров (_, newPhase) — это API из iOS 17. Убедитесь, что минимальная версия — iOS 17+. Если нет — верните однопараметровый вариант.
-
Регрессия для UI-тестов. Вы убрали блок, отключающий анимации при UITEST_DISABLE_ANIMATIONS. Если UI-тесты опирались на это — тесты станут flaky. Верните в тестовой сборке/при запуске с флагом.
-
Централизация логики. Вынесите логику отзывов в отдельный ReviewRequestManager (ObservableObject/сервис) и дергайте его из View. Это упростит тестирование, переиспользование и уберет «бизнес-логику» из View.
Минимальная правка запроса оценки с учетом сцены/потока:
- @mainactor
private func requestReviewIfNeeded() {
guard daysSinceFirstLaunch >= daysBeforeRequest else { return }
if let scene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }) {
SKStoreReviewController.requestReview(in: scene)
// пометка, чтобы не повторять
lastVersionRequestedForReview = currentVersion
}
}
И еще: храните и проверяйте lastVersionRequestedForReview, а не «забивать» firstLaunchDate на distantFuture — будет прозрачнее и контролируемее.
| <key>com.apple.security.files.user-selected.read-only</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Ключевые замечания по изменению entitlements:
Критично
-
Удаление com.apple.security.app-sandbox
- macOS: это почти наверняка ошибка. Без App Sandbox:
- Приложение не пройдет Mac App Store ревью.
- application-groups перестанет работать (групповой контейнер вернет nil).
- iCloud KVS (com.apple.developer.ubiquity-kvstore-identifier) не будет доступен.
- Возможны ошибки подписывания/загрузки из‑за несоответствия профилю.
- iOS: ключ не нужен и его наличие ранее намекает на путаницу между таргетами. Разнесите entitlements по платформам.
- macOS: это почти наверняка ошибка. Без App Sandbox:
-
Удаление com.apple.security.files.user-selected.read-only
- macOS (в песочнице): если вы открываете файлы через NSOpenPanel/UIDocumentPicker и ожидаете доступ вне контейнера, вам нужна одна из:
- com.apple.security.files.user-selected.read-only или
- com.apple.security.files.user-selected.read-write (если пишете).
- Если намеренно отключили песочницу (что само по себе плохая идея для macOS/MAS), этот ключ не нужен — но это конфликтует с необходимостью для App Groups и iCloud.
- macOS (в песочнице): если вы открываете файлы через NSOpenPanel/UIDocumentPicker и ожидаете доступ вне контейнера, вам нужна одна из:
Согласованность с профилями и возможные “Invalid Entitlement”
- Если профили подписи сгенерированы с включенной песочницей/файловыми доступами, после удаления ключей получите несоответствие. Проверьте/пересоберите профили для каждого таргета.
iCloud KVS
- com.apple.developer.ubiquity-kvstore-identifier работает только при корректно включенной возможности iCloud и соответствующем профиле. На macOS требует песочницу. На iOS — ок.
- Убедитесь, что это действительно то, что нужно: KVS ограничен по объему и предназначен для мелких настроек; для сложных данных — CloudKit.
Application Groups
- На macOS без песочницы entitlement игнорируется.
- На iOS — ок. Проверьте, что group.com.amikhaylin.PomPadDo создан в Dev Portal и одинаково включен во всех таргетах (приложение/виджеты/расширения).
Рекомендации по исправлению
-
Если таргет iOS:
- Оставьте как есть (без app-sandbox и без user-selected.*).
- Заверьте, что профиль содержит App Groups и iCloud KVS по необходимости.
- Разделите entitlements для iOS и macOS, чтобы избежать повторной путаницы.
-
Если таргет macOS:
- Верните com.apple.security.app-sandbox = true.
- Верните com.apple.security.files.user-selected.* согласно фактическому сценарию (read-only vs read-write). Если нужны постоянные права — используйте security-scoped bookmarks.
- Убедитесь, что App Groups и ubiquity-kvstore работают (containerURL(forSecurityApplicationGroupIdentifier:) не возвращает nil; NSUbiquitousKeyValueStore.default.synchronize() не падает).
- Перегенерируйте профили с этими entitlements.
Небольшие, но полезные замечания
- Поддерживайте отдельные .entitlements файлы per target/platform (и при необходимости per configuration), вместо «универсального» plist.
- Проверьте регистр/стиль идентификаторов групп (лучше придерживаться нижнего регистра и стабильного нейминга).
- Если у вас была попытка «починить доступ к файлам» отключением песочницы — это неправильный путь. Верните песочницу и корректно настройте Powerbox/SSB и соответствующие entitlements.
| UIView.setAnimationsEnabled(false) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Ниже только то, что может улучшить надежность/переносимость и предсказуемость поведения.
-
UIKit-зависимость и кросс-платформенность:
- Если файл собирается не только под iOS/tvOS, оберните вызов в #if canImport(UIKit) (или #if os(iOS) || os(tvOS)) и не забудьте import UIKit. Иначе сборка под macOS/visionOS может упасть.
-
Поток выполнения:
- UIView.setAnimationsEnabled(false) должен вызываться на главном потоке. Без гарантии main-thread это потенциальная гонка/undefined behavior. Оберните в DispatchQueue.main.async { … } или выполните внутри @mainactor.
-
Формат флага из окружения:
- Жёсткая проверка на "YES" хрупкая. Лучше парсить несколько вариантов ("1", "true", "yes", регистр не учитывать). Вынесите в небольшую утилиту/extension, чтобы избежать дублирования и опечаток.
- Рассмотрите поддержку launchArguments (например, -UITestDisableAnimations) — их проще использовать в тестах и они читаются так же рано.
-
SwiftUI-анимации не все отключатся:
- UIView.setAnimationsEnabled(false) не гарантирует отключение всех анимаций SwiftUI (особенно withAnimation/implicit). Если нужна абсолютная предсказуемость в UI-тестах:
- На корневом вью при флаге из окружения добавьте .transaction { $0.animation = nil } — это «глушит» SwiftUI-анимации в поддереве.
- Опционально можно проставить .environment(.accessibilityReduceMotion, true) — многие системные анимации будут уменьшены/отключены (не всё, но помогает).
- Пример применения в WindowGroup при флаге: RootView().transaction { $0.animation = nil }.
- UIView.setAnimationsEnabled(false) не гарантирует отключение всех анимаций SwiftUI (особенно withAnimation/implicit). Если нужна абсолютная предсказуемость в UI-тестах:
-
Стабильность при смене активного состояния:
- На некоторых конфигурациях сторонний код может «включить» анимации позже. Подстрахуйте повторной установкой при активации сцены:
- .onChange(of: scenePhase) { if phase == .active && isUITestFlag { UIView.setAnimationsEnabled(false) } }
- На некоторых конфигурациях сторонний код может «включить» анимации позже. Подстрахуйте повторной установкой при активации сцены:
-
Мелкие улучшения DX:
- Вынесите строковый ключ "UITEST_DISABLE_ANIMATIONS" в константу.
- Локально логируйте факт отключения (только в DEBUG), это экономит время при диагностике, не засоряя релиз.
Иллюстративно (сжато):
init() {
#if canImport(UIKit)
let isUITest = ProcessInfo.processInfo.uiTestDisablesAnimations
guard isUITest else { return }
DispatchQueue.main.async {
UIView.setAnimationsEnabled(false)
}
#endif
}
extension ProcessInfo {
var uiTestDisablesAnimations: Bool {
if arguments.contains("-UITestDisableAnimations") { return true }
guard let v = environment["UITEST_DISABLE_ANIMATIONS"]?.lowercased() else { return false }
return v == "1" || v == "true" || v == "yes"
}
}
И в корневом вью (когда isUITest == true):
RootView()
.transaction { $0.animation = nil }
.environment(.accessibilityReduceMotion, true)
| LastUpgradeVersion = "2610" | ||
| version = "1.7"> | ||
| <BuildAction | ||
| parallelizeBuildables = "YES" |
There was a problem hiding this comment.
Коротко по делу:
- Значение 2610 в LastUpgradeVersion выглядит ошибочным. Для Xcode это поле кодируется как 1600/1610/1620/1630 и т.п. (Xcode 16.0/16.1/16.2/16.3). 2610 — нереальный номер для текущих версий и, скорее всего, опечатка.
- Риск: завышенное значение «обманывает» Xcode — он может не предложить миграции/рекомендованные настройки в будущем, что приведет к скрытым несоответствиям в проектных настройках.
- Рекомендация: не править это поле вручную. Пусть Xcode сам обновит его при «Update to recommended settings». Если нужно задать вручную — поставьте корректное значение для вашей версии Xcode (например, 1630 для 16.3).
- Согласованность: если действительно обновляете, синхронизируйте аналогичные маркеры в проекте — LastUpgradeCheck в project.pbxproj и LastUpgradeVersion во всех .xcscheme, чтобы избежать «шума» и расхождений.
- Процессно: зафиксируйте версию Xcode для команды/CI (документация/xcodes/минимальная версия) и не коммитьте одиночные правки схем без реальной миграции проекта.
| LastUpgradeVersion = "2610" | ||
| wasCreatedForAppExtension = "YES" | ||
| version = "2.0"> | ||
| <BuildAction |
There was a problem hiding this comment.
Замечания по PR:
-
Значение LastUpgradeVersion="2610" выглядит некорректным. Для схем Xcode используется четырехзначный код, соответствующий версии Xcode:
- 15.0 → 1500, 15.1 → 1510, …, 15.4 → 1540
- 16.0 → 1600, 16.1 → 1610, 16.2 → 1620, 16.3 → 1630, 16.4 → 1640
2610 не соответствует реальным версиям Xcode и, вероятно, попадёт обратно в валидное значение при следующем сохранении схемы в Xcode. Это создаст «шум» в истории коммитов.
-
Рекомендации:
- Не редактировать LastUpgradeVersion вручную. Откройте проект в целевой версии Xcode, зайдите в Manage Schemes и просто сохраните схему — Xcode сам проставит корректное значение.
- Приведите LastUpgradeVersion в схемах в соответствие реальной версии Xcode, которой вы пользовались (например, 1640 для Xcode 16.4).
- Проверьте согласованность с Project.pbxproj (ключ LastUpgradeCheck) — не обязаны быть идентичны, но большие расхождения обычно признак случайного/ошибочного правки.
- Добавьте в CI/прехук быстрый линт: отклонять значения LastUpgradeVersion вне допустимого диапазона (например, 1500–1699 на сегодня), чтобы избежать случайных правок.
Итог: предлагаю отклонить данный change и либо оставить прежнее 1640, либо обновить на корректный код версии Xcode, которой вы реально мигрировали схему.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Ниже — только то, что реально требует исправления или даёт ощутимое улучшение.
- Логика drag-изменения высоты некорректна: вы складываете translation (который всегда считается от начала жеста) с уже изменённым currentSectionHeight. В результате высота «убегает». Нужна базовая высота, зафиксированная в начале жеста, и вычисление относительно неё.
- Несоответствие типов: sectionHeight — Double, currentSectionHeight — CGFloat. В onEnded вы делаете sectionHeight = currentSectionHeight без конверсии — это не скомпилируется. Нужен явный Double(currentSectionHeight).
- Дублирование «магических чисел» и смешение типов в клампинге. Лучше вынести min/max в константы CGFloat, а конвертировать в Double только при записи в AppStorage.
- Синхронизация с внешними изменениями AppStorage. Сейчас вы синхронизируете только в onAppear. Если значение SectionHeight поменяется извне (другим окном/вью), currentSectionHeight не обновится. Добавьте onChange(of: sectionHeight).
Предлагаемый минимальный патч (ключевые места):
-
Константы и состояния:
-
Жест:
- .gesture(
DragGesture()
.onChanged { value in
if dragStartHeight == nil { dragStartHeight = currentSectionHeight }
let proposed = (dragStartHeight ?? currentSectionHeight) + value.translation.height
currentSectionHeight = min(max(proposed, minSectionHeight), maxSectionHeight)
}
.onEnded { value in
let final = (dragStartHeight ?? currentSectionHeight) + value.translation.height
let clamped = min(max(final, minSectionHeight), maxSectionHeight)
currentSectionHeight = clamped
sectionHeight = Double(clamped) // явная конверсия
dragStartHeight = nil
}
)
- .gesture(
-
Инициализация и синхронизация:
- .onAppear { currentSectionHeight = CGFloat(sectionHeight) }
- .onChange(of: sectionHeight) { newValue in
currentSectionHeight = CGFloat(newValue)
}
Это устраняет «убегающую» высоту, ошибки типов, снижает риск рассинхронизации и убирает дублирование магических чисел.
| <key>com.apple.security.personal-information.calendars</key> | ||
| <true/> | ||
| </dict> | ||
| </plist> |
There was a problem hiding this comment.
Ниже — только то, что действительно может быть проблемой или источником недопонимания в этом изменении.
-
Критично: вы удалили com.apple.security.app-sandbox. На macOS это значит:
- App Store: приложение не пройдёт ревью (Sandbox обязателен).
- iCloud KVS (com.apple.developer.ubiquity-kvstore-identifier) перестанет работать — iCloud на macOS требует Sandbox. Держать этот ключ без Sandbox бессмысленно и может привести к ошибкам при подписи/нотаризации.
- Если цель — Developer ID вне MAS: iCloud (включая KVS/CloudKit) недоступен. Уберите com.apple.developer.ubiquity-kvstore-identifier, иначе возможны «invalid entitlements» при codesign/notarytool.
-
Согласованность с возможностями:
- Если вы реально используете календари (EventKit), то без Sandbox entitlement больше не нужен, но TCC всё равно потребует NSCalendarsUsageDescription в Info.plist. Проверьте наличие этого ключа.
- Network entitlement (com.apple.security.network.client) нужен только в песочнице; вне Sandbox он не нужен. Но если вы когда-либо вернётесь к Sandbox — не забудьте вернуть его, иначе сеть будет заблокирована.
-
Определитесь с целевой платформой и каналом дистрибуции:
- macOS, Mac App Store: верните Sandbox и соответствующие entitlements (network, calendars, files.user-selected.read-write если нужны) — иначе не пройдёте ревью и потеряете iCloud.
- macOS, Developer ID (вне MAS): удалите все iCloud-энтайтлы (включая текущий kvstore), оставьте Hardened Runtime, проведите повторную проверку codesign/notarytool.
- iOS: com.apple.security.* не применимы и не нужны; com.apple.developer.ubiquity-kvstore-identifier остаётся корректным при включённой iCloud Capability.
-
Внимание к значению kvstore-идентификатора:
-
$(TeamIdentifierPrefix)$ (CFBundleIdentifier) обычно корректно разворачивается в TEAMID.com.example.app. Сам по себе ок, но, повторюсь, на macOS без Sandbox и/или вне MAS использовать нельзя.
-
-
Потенциальные структурные проблемы plist:
- В диффе виден закрывающий . Убедитесь, что после удаления ключей XML остаётся валидным (правильные пары …, …). Прогоните plutil -lint.
-
Рекомендация по управлению вариантами:
- Завести отдельные entitlements для разных таргетов/сборок (MAS vs Developer ID), чтобы не миксовать iCloud и отсутствие Sandbox.
- Автоматизировать проверку: scripts, которые на CI запускают codesign -d --entitlements :-, plutil -lint, notarytool submit --dry-run.
Кратко: если вы сознательно уходите от Sandbox на macOS, обязательно уберите iCloud KVS entitlement и проверьте TCC-ключи в Info.plist. Если же iCloud нужен — верните Sandbox и недостающие entitlements, иначе функциональность и дистрибуция сломаются.
| // Switch to board view | ||
| if project.isBoard { | ||
| app/*@START_MENU_TOKEN@*/.radioButtons["rectangle.split.3x1"]/*[[".radioGroups[\"View Mode\"].radioButtons",".radioGroups",".radioButtons[\"Column View\"]",".radioButtons[\"rectangle.split.3x1\"]"],[[[-1,3],[-1,2],[-1,1,1],[-1,0]],[[-1,3],[-1,2]]],[0]]@END_MENU_TOKEN@*/.firstMatch.click() | ||
|
|
There was a problem hiding this comment.
Ниже — только конструктивные замечания по показанному фрагменту UI‑теста.
-
Уберите артефакты UI‑Recorder’а. Ветка с /@START_MENU_TOKEN@/ … /@END_MENU_TOKEN@/ делает селектор хрупким и нечитаемым. Оставьте один понятный запрос.
-
Не используйте SF Symbol/лейбл как ключ. "rectangle.split.3x1" и "View Mode" (а также "Column View") завязаны на локализацию/иконки. Для UI‑тестов нужны стабильные accessibilityIdentifier. Задайте идентификаторы в приложении (например, radio group: "viewMode", radio: "viewMode.board").
-
Избегайте firstMatch без проверки. Это маскирует неоднозначность. Либо настраивайте точный селектор, либо проверяйте, что найден единственный элемент, либо хотя бы делайте wait + exists.
-
Добавьте явное ожидание перед кликом и проверку результата. Перед .click() — waitForExistence/isHittable. После — проверка, что нужный экран действительно активен (например, появление/выбор BoardView).
-
Логика условия может быть двусмысленной. if project.isBoard читается как «проект уже в режиме Board». Тогда клик может быть избыточным. Либо переименуйте флаг (shouldUseBoardView), либо кликайте только если radio не выбран, либо сначала утверждайте текущее состояние.
-
Ограничьте область поиска. Запрашивайте кнопку внутри конкретной radioGroup/toolbar, чтобы исключить конфликты одинаковых иконок в разных местах.
-
Вынесите в «page object»/хелпер. Повторяющиеся действия (выбор режима) — в метод с ожиданиями и проверками.
Предлагаемый фрагмент (пример для macOS UI Tests), с учётом стабильных идентификаторов:
// Switch to board view
if project.shouldUseBoardView {
let viewMode = app.radioGroups["viewMode"]
let board = viewMode.radioButtons["viewMode.board"]
XCTAssertTrue(board.waitForExistence(timeout: 2), "Кнопка board не найдена")
if !(board.value as? String == "1") { // или board.isSelected, если доступно
XCTAssertTrue(board.isHittable, "Кнопка board недоступна для клика")
board.click()
}
// Верификация, что реально переключились на board
XCTAssertTrue(app.collectionViews["boardView"].waitForExistence(timeout: 3), "BoardView не появился")
}
Или обёртка:
func selectViewModeBoard(app: XCUIApplication) {
let viewMode = app.radioGroups["viewMode"]
let board = viewMode.radioButtons["viewMode.board"]
XCTAssertTrue(board.waitForExistence(timeout: 2))
if !(board.value as? String == "1") {
board.click()
}
XCTAssertTrue(app.collectionViews["boardView"].waitForExistence(timeout: 3))
}
Ключевые улучшения: избавиться от recorder‑комментариев, перейти на стабильные accessibilityIdentifier, убрать firstMatch, добавить ожидания и проверку результата, и сделать код декларативнее через хелперы/enum режима.
No description provided.