Skip to content

Fixed section list resize and review request#152

Merged
amikhaylin merged 3 commits intomasterfrom
fixes
Nov 14, 2025
Merged

Fixed section list resize and review request#152
amikhaylin merged 3 commits intomasterfrom
fixes

Conversation

@amikhaylin
Copy link
Copy Markdown
Owner

No description provided.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code review by ChatGPT


screenshots/desktop/*.html
screenshots/desktop/ru/*.png
screenshots/desktop/en-US/*.png
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Коротко и по делу. Ваш .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 и файлов покрытия/отчётов, если такие генерируются в вашем процессе.

}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только то, что действительно стоит поправить или улучшить.

Критично

  • Накопление высоты при перетаскивании. Вы используете 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 и аккуратности кода.

}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Вот по делу — что можно улучшить и где есть риски/ошибки.

  • Неверный 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ключевые замечания по изменению 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 по платформам.
  • Удаление 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.

Согласованность с профилями и возможные “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)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже только то, что может улучшить надежность/переносимость и предсказуемость поведения.

  • 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 }.
  • Стабильность при смене активного состояния:

    • На некоторых конфигурациях сторонний код может «включить» анимации позже. Подстрахуйте повторной установкой при активации сцены:
      • .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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Коротко по делу:

  • Значение 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Замечания по 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, которой вы реально мигрировали схему.

}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только то, что реально требует исправления или даёт ощутимое улучшение.

  • Логика drag-изменения высоты некорректна: вы складываете translation (который всегда считается от начала жеста) с уже изменённым currentSectionHeight. В результате высота «убегает». Нужна базовая высота, зафиксированная в начале жеста, и вычисление относительно неё.
  • Несоответствие типов: sectionHeight — Double, currentSectionHeight — CGFloat. В onEnded вы делаете sectionHeight = currentSectionHeight без конверсии — это не скомпилируется. Нужен явный Double(currentSectionHeight).
  • Дублирование «магических чисел» и смешение типов в клампинге. Лучше вынести min/max в константы CGFloat, а конвертировать в Double только при записи в AppStorage.
  • Синхронизация с внешними изменениями AppStorage. Сейчас вы синхронизируете только в onAppear. Если значение SectionHeight поменяется извне (другим окном/вью), currentSectionHeight не обновится. Добавьте onChange(of: sectionHeight).

Предлагаемый минимальный патч (ключевые места):

  • Константы и состояния:

    • private let minSectionHeight: CGFloat = 30
    • private let maxSectionHeight: CGFloat = 170
    • @State private var currentSectionHeight: CGFloat = 170
    • @State private var dragStartHeight: CGFloat?
  • Жест:

    • .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
      }
      )
  • Инициализация и синхронизация:

    • .onAppear { currentSectionHeight = CGFloat(sectionHeight) }
    • .onChange(of: sectionHeight) { newValue in
      currentSectionHeight = CGFloat(newValue)
      }

Это устраняет «убегающую» высоту, ошибки типов, снижает риск рассинхронизации и убирает дублирование магических чисел.

<key>com.apple.security.personal-information.calendars</key>
<true/>
</dict>
</plist>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только то, что действительно может быть проблемой или источником недопонимания в этом изменении.

  • Критично: вы удалили 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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только конструктивные замечания по показанному фрагменту 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 режима.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code review by ChatGPT


screenshots/desktop/*.html
screenshots/desktop/ru/*.png
screenshots/desktop/en-US/*.png
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Коротко и по делу. Ваш .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 и файлов покрытия/отчётов, если такие генерируются в вашем процессе.

}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только то, что действительно стоит поправить или улучшить.

Критично

  • Накопление высоты при перетаскивании. Вы используете 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 и аккуратности кода.

}
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Вот по делу — что можно улучшить и где есть риски/ошибки.

  • Неверный 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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ключевые замечания по изменению 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 по платформам.
  • Удаление 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.

Согласованность с профилями и возможные “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)
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже только то, что может улучшить надежность/переносимость и предсказуемость поведения.

  • 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 }.
  • Стабильность при смене активного состояния:

    • На некоторых конфигурациях сторонний код может «включить» анимации позже. Подстрахуйте повторной установкой при активации сцены:
      • .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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Коротко по делу:

  • Значение 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Замечания по 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, которой вы реально мигрировали схему.

}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только то, что реально требует исправления или даёт ощутимое улучшение.

  • Логика drag-изменения высоты некорректна: вы складываете translation (который всегда считается от начала жеста) с уже изменённым currentSectionHeight. В результате высота «убегает». Нужна базовая высота, зафиксированная в начале жеста, и вычисление относительно неё.
  • Несоответствие типов: sectionHeight — Double, currentSectionHeight — CGFloat. В onEnded вы делаете sectionHeight = currentSectionHeight без конверсии — это не скомпилируется. Нужен явный Double(currentSectionHeight).
  • Дублирование «магических чисел» и смешение типов в клампинге. Лучше вынести min/max в константы CGFloat, а конвертировать в Double только при записи в AppStorage.
  • Синхронизация с внешними изменениями AppStorage. Сейчас вы синхронизируете только в onAppear. Если значение SectionHeight поменяется извне (другим окном/вью), currentSectionHeight не обновится. Добавьте onChange(of: sectionHeight).

Предлагаемый минимальный патч (ключевые места):

  • Константы и состояния:

    • private let minSectionHeight: CGFloat = 30
    • private let maxSectionHeight: CGFloat = 170
    • @State private var currentSectionHeight: CGFloat = 170
    • @State private var dragStartHeight: CGFloat?
  • Жест:

    • .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
      }
      )
  • Инициализация и синхронизация:

    • .onAppear { currentSectionHeight = CGFloat(sectionHeight) }
    • .onChange(of: sectionHeight) { newValue in
      currentSectionHeight = CGFloat(newValue)
      }

Это устраняет «убегающую» высоту, ошибки типов, снижает риск рассинхронизации и убирает дублирование магических чисел.

<key>com.apple.security.personal-information.calendars</key>
<true/>
</dict>
</plist>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только то, что действительно может быть проблемой или источником недопонимания в этом изменении.

  • Критично: вы удалили 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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ниже — только конструктивные замечания по показанному фрагменту 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 режима.

@amikhaylin amikhaylin merged commit a03e63d into master Nov 14, 2025
1 check passed
@amikhaylin amikhaylin deleted the fixes branch November 14, 2025 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant