├── .gitignore ├── ru ├── roadmaps │ ├── indie.md │ └── junior.md └── tutorials │ ├── meta │ ├── categories.json │ └── authors.json │ ├── custom-swiftui-modifier.md │ ├── how-to-clean-userdefaults-and-realm-on-macos-catalyst.md │ ├── product-page-optimization-alternative-icons.md │ ├── how-to-get-root-view-controller.md │ ├── edge-insets-uibutton.md │ ├── set-launch-screen-via-plist.md │ ├── testing-push-notifications-ios-simulator.md │ ├── sf-symbols-and-render-mode.md │ ├── difference-property-wrappers-in-swiftui.md │ ├── pay-for-apple-developer-account-from-ru.md │ ├── swift-documentation.md │ ├── uisheetpresentationcontroller.md │ ├── uiviewcontroller-lifecycle.md │ ├── cert-and-profile-for-personal-developer-account.md │ ├── privacy-manifest.md │ ├── storekit-external-purchase-link-entitlement-ru.md │ ├── formatters.md │ └── live-activities.md ├── en ├── roadmaps │ └── junior.md └── tutorials │ ├── meta │ ├── categories.json │ ├── authors.json │ └── tutorials.json │ ├── custom-swiftui-modifier.md │ ├── how-to-clean-userdefaults-and-realm-on-macos-catalyst.md │ ├── product-page-optimization-alternative-icons.md │ ├── edge-insets-uibutton.md │ ├── how-to-get-root-view-controller.md │ ├── set-launch-screen-via-plist.md │ ├── testing-push-notifications-ios-simulator.md │ ├── sf-symbols-and-render-mode.md │ ├── uisheetpresentationcontroller.md │ ├── uiviewcontroller-lifecycle.md │ ├── cert-and-profile-for-personal-developer-account.md │ ├── privacy-manifest.md │ └── live-activities.md ├── swift-student-challenge ├── 2023.json └── 2024.json └── developers.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .Trashes 3 | -------------------------------------------------------------------------------- /ru/roadmaps/indie.md: -------------------------------------------------------------------------------- 1 | Here content example. 2 | 3 | Even title 4 | 5 | # Title -------------------------------------------------------------------------------- /en/roadmaps/junior.md: -------------------------------------------------------------------------------- 1 | Here content example. 2 | 3 | Even title 4 | 5 | # Title -------------------------------------------------------------------------------- /ru/roadmaps/junior.md: -------------------------------------------------------------------------------- 1 | Here content example. 2 | 3 | Even title 4 | 5 | # Title -------------------------------------------------------------------------------- /en/tutorials/meta/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "foundation": { 3 | "title": "Foundation" 4 | }, 5 | "swift": { 6 | "title": "Swift" 7 | }, 8 | "uikit": { 9 | "title": "UIKit" 10 | }, 11 | "swiftui": { 12 | "title": "SwiftUI" 13 | }, 14 | "layout": { 15 | "title": "Layout" 16 | }, 17 | "extensions": { 18 | "title": "Extensions" 19 | }, 20 | "development": { 21 | "title": "Development" 22 | }, 23 | "app-store-connect": { 24 | "title": "App Store Connect" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ru/tutorials/meta/categories.json: -------------------------------------------------------------------------------- 1 | { 2 | "foundation": { 3 | "title": "Foundation" 4 | }, 5 | "swift": { 6 | "title": "Swift" 7 | }, 8 | "uikit": { 9 | "title": "UIKit" 10 | }, 11 | "swiftui": { 12 | "title": "SwiftUI" 13 | }, 14 | "layout": { 15 | "title": "Layout" 16 | }, 17 | "extensions": { 18 | "title": "Extensions" 19 | }, 20 | "development": { 21 | "title": "Разработка" 22 | }, 23 | "app-store-connect": { 24 | "title": "App Store Connect" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /en/tutorials/custom-swiftui-modifier.md: -------------------------------------------------------------------------------- 1 | # Create Modifier 2 | 3 | There is a built-in tool for custom modifiers — you need to create a structure and implement the `ViewModifier` protocol. The protocol should be used to implement the `body` method and return a new `View`. 4 | 5 | To give an example, let's make a modifier that combines styles for text: 6 | 7 | ```swift 8 | struct LargeTitleModifier: ViewModifier { 9 | 10 | func body(content: Content) -> some View { 11 | content 12 | .font(.largeTitle) 13 | .foregroundStyle(.primary) 14 | } 15 | } 16 | ``` 17 | 18 | You can use other modifiers and even embed `View`. 19 | 20 | # Apply Modifier 21 | 22 | Call via `.modifier` and pass a custom modifier: 23 | 24 | ```swift 25 | Text("Hello World") 26 | .modifier(LargeTitleModifier()) 27 | ``` 28 | 29 | # Native Style 30 | 31 | In order for the modifier to be called natively, you need to make an extension for `View`: 32 | 33 | ```swift 34 | extension View { 35 | 36 | func largeTitleStyle() -> some View { 37 | modifier(LargeTitleModifier()) 38 | } 39 | } 40 | ``` 41 | 42 | To narrow down the availability of the modifier, you can make the extension only for `Text`. 43 | 44 | The call will now be in native style: 45 | 46 | ```swift 47 | Text("Hello World") 48 | .largeTitleStyle() 49 | ``` -------------------------------------------------------------------------------- /ru/tutorials/custom-swiftui-modifier.md: -------------------------------------------------------------------------------- 1 | # Создаем Модификатор 2 | 3 | Для кастомных модификаторов есть встроенный инструмент - нужно создать структуру и реализовать протокол `ViewModifier`. По протоколу нужно реализовать метод `body` и вернуть новую `View`. 4 | 5 | Для примера сделаем модификатор, который объединяет стили для текста: 6 | 7 | ```swift 8 | struct LargeTitleModifier: ViewModifier { 9 | 10 | func body(content: Content) -> some View { 11 | content 12 | .font(.largeTitle) 13 | .foregroundStyle(.primary) 14 | } 15 | } 16 | ``` 17 | 18 | Вы можете использовать и другие модификаторы и даже встраивать `View`. 19 | 20 | # Применить Модификатор 21 | 22 | Вызываем через `.modifier` и передаем кастомный модификатор: 23 | 24 | ```swift 25 | Text("Hello World") 26 | .modifier(LargeTitleModifier()) 27 | ``` 28 | 29 | # Нативный стиль 30 | 31 | Чтобы модификатор вызывался в нативном стиле, нужно сделать extension для `View`: 32 | 33 | ```swift 34 | extension View { 35 | 36 | func largeTitleStyle() -> some View { 37 | modifier(LargeTitleModifier()) 38 | } 39 | } 40 | ``` 41 | 42 | Чтобы сузить доступность модификатора, вы можете сделать расширение только для `Text`. 43 | 44 | Теперь вызов будет в нативном стиле: 45 | 46 | ```swift 47 | Text("Hello World") 48 | .largeTitleStyle() 49 | ``` -------------------------------------------------------------------------------- /en/tutorials/how-to-clean-userdefaults-and-realm-on-macos-catalyst.md: -------------------------------------------------------------------------------- 1 | To reset a macOS Catalyst application, you need to know these values: 2 | 3 | - User folder `ivanvorobei` 4 | - Application Bundle `io.ivanvorobei.apps.debts` 5 | - AppGroup `group.io.ivanvorobei.apps.debts`. 6 | 7 | Be careful, use the values from your application. 8 | 9 | # Clear UserDefaults 10 | 11 | To remove the default `UserDefaults`, open a terminal and type the command: 12 | 13 | ``` 14 | // Delete `UserDefaults` entirely 15 | defaults delete io.ivanvorobei.apps.debts 16 | 17 | // Remove from `UserDefaults` by key 18 | defaults delete io.ivanvorobei.apps.debts key 19 | ``` 20 | 21 | If you used a custom domain, call the command: 22 | 23 | ``` 24 | // Created like this: 25 | UserDefaults(suiteName: "Custom") 26 | 27 | // Deleted like this: 28 | defaults delete Custom 29 | ``` 30 | 31 | # AppGroup 32 | 33 | If you use an `AppGroup`, delete these folders: 34 | 35 | ``` 36 | /Users/ivanvorobei/Library/Group Containers/group.io.ivanvorobei.apps.debts 37 | /Users/ivanvorobei/Library/Application Scripts/group.io.ivanvorobei.apps.debts 38 | ``` 39 | 40 | If stored in the default path, delete that folder: 41 | 42 | ``` 43 | /Users/ivanvorobei/Library/Containers/io.ivanvorobei.apps.debts 44 | ``` 45 | 46 | # Realm database 47 | 48 | The `Realm` database files are stored as normal files. They are either in the AppGroup or in the default folder. If you follow the steps above, the database is deleted. 49 | -------------------------------------------------------------------------------- /ru/tutorials/how-to-clean-userdefaults-and-realm-on-macos-catalyst.md: -------------------------------------------------------------------------------- 1 | Чтобы ресетнуть приложение для macOS Catalyst, нужно знать эти значения: 2 | 3 | - Папку пользователя `ivanvorobei` 4 | - Bundle приложения `io.ivanvorobei.apps.debts` 5 | - Идентификатор AppGroup `group.io.ivanvorobei.apps.debts`. 6 | 7 | Будьте внимательны, используйте значения от вашего приложения. 8 | 9 | # Очистить UserDefaults 10 | 11 | Чтобы удалить дефолтный `UserDefaults`, откройте терминал и введите команду: 12 | 13 | ``` 14 | // Удаляем `UserDefaults` целиком 15 | defaults delete io.ivanvorobei.apps.debts 16 | 17 | // Удаляем из `UserDefaults` по ключу 18 | defaults delete io.ivanvorobei.apps.debts key 19 | ``` 20 | 21 | Если использовали кастомный домен, вызывайте команду: 22 | 23 | ``` 24 | // Создается так: 25 | UserDefaults(suiteName: "Custom") 26 | 27 | // Удаляется так: 28 | defaults delete Custom 29 | ``` 30 | 31 | # AppGroup 32 | 33 | Если используете `AppGroup`, удалите эти папки: 34 | 35 | ``` 36 | /Users/ivanvorobei/Library/Group Containers/group.io.ivanvorobei.apps.debts 37 | /Users/ivanvorobei/Library/Application Scripts/group.io.ivanvorobei.apps.debts 38 | ``` 39 | 40 | Если хранили в дефолтном пути, удалите эту папку: 41 | 42 | ``` 43 | /Users/ivanvorobei/Library/Containers/io.ivanvorobei.apps.debts 44 | ``` 45 | 46 | # База данных Realm 47 | 48 | Файлы базы данных `Realm` хранятся как обычные файлы. Они находятся либо в AppGroup, либо в дефолтной папке. Если выполните пункты выше, база данных удалится. 49 | -------------------------------------------------------------------------------- /ru/tutorials/product-page-optimization-alternative-icons.md: -------------------------------------------------------------------------------- 1 | С помощью [Product Page Optimization](https://developer.apple.com/app-store/product-page-optimization/) вы можете создавать варианты скриншотов, промо-текстов и иконок. Скриншоты и текст добавляются в App Store Connect, а иконки добавляет разработчик в Xcode-проект. 2 | 3 | В документации написано: «Поместите иконки в Asset Catalog, отправьте бинарный файл в App Store Connect и используйте SDK». Но не сказали как закинуть иконки и что это за SDK. Давайте разбираться. 4 | 5 | # Добавляем иконки в Assets 6 | 7 | Альтернативную иконку делаем в нескольких разрешениях, как и основную. Имя пакета иконок будет видно в App Store Connect. 8 | 9 | ![Добавляем иконки в Assets.](https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-icons-to-assets.png) 10 | 11 | # Настраиваем таргет 12 | 13 | Нам понадобится Xcode 13 и выше. Выберите таргет приложения и перейдите на вкладку `Build Settings`. В поиск вставьте `App Icon` — увидите секцию `Asset Catalog Compiler`. 14 | 15 | ![Параметры в таргете проекта.](https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-settings-to-target.png) 16 | 17 | Нас интересуют 3 параметра: 18 | 19 | - `Alternate App Icons Sets` — перечисление названий иконок, которые добавили в каталог. 20 | - `Include All App Icon Assets` — установите в `true`, чтобы включить альтернативные иконки в сборку. 21 | - `Primary App Icon Set Name` — название иконки по умолчанию. Скорее всего, альтернативную иконку можно сделать основной. Не проверял. 22 | 23 | # Выгружаем 24 | 25 | Остаётся собрать приложение и отправить на проверку. 26 | 27 | > Альтернативные иконки будут доступны после прохождения ревью. 28 | 29 | После ревью можно собирать разные страницы приложения и создавать ссылки для A/B тестов. 30 | -------------------------------------------------------------------------------- /en/tutorials/product-page-optimization-alternative-icons.md: -------------------------------------------------------------------------------- 1 | With [Product Page Optimization](https://developer.apple.com/app-store/product-page-optimization/) you can create variants of screenshots, promo texts, and icons. Screenshots and text are added to App Store Connect, and icons are added by the developer to the Xcode project. 2 | 3 | The documentation says: «Put the icons in Asset Catalog, send the binary to App Store Connect and use the SDK». But they didn't say how to put the icons and what kind of SDK it is. Let's figure it out. 4 | 5 | # Adding icons to Assets 6 | 7 | Make the alternative icon in multiple resolutions, just like the main icon. The name of the icon pack will be visible in App Store Connect. 8 | 9 | ![Adding icons to Assets.](https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-icons-to-assets.png) 10 | 11 | # Setting up targeting 12 | 13 | We need Xcode 13 or higher. Select the application target and go to the `Build Settings` tab. In the search for `App Icon` - you will see the section `Asset Catalog Compiler`. 14 | 15 | ![Parameters in the project target.](https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-settings-to-target.png) 16 | 17 | We are interested in three parameters: 18 | 19 | - `Alternate App Icons Sets` - list the names of the icons you have added to the catalog. 20 | - `Include All App Icon Assets` - set to `true` to include alternative icons in the assembly. 21 | - `Primary App Icon Set Name` - default icon name. Most likely, the alternate icon can be made the primary icon. Did not check. 22 | 23 | # Uploading 24 | 25 | It remains to assemble the application and send it in for review. 26 | 27 | > Alternative icons will be available after the review. 28 | 29 | After the review, you can assemble different pages of the app and create links for A/B tests. 30 | -------------------------------------------------------------------------------- /en/tutorials/meta/authors.json: -------------------------------------------------------------------------------- 1 | { 2 | "ivanvorobei": { 3 | "name": "Ivan Vorobei", 4 | "description": "iOS Developer. Making opensource frameworks & tutorials.", 5 | "avatar": "https://cdn.sparrowcode.io/authors/ivanvorobei.jpg", 6 | "github": "ivanvorobei", 7 | "buttons": [ 8 | { 9 | "title": "GitHub", 10 | "url": "https://github.com/ivanvorobei" 11 | }, 12 | { 13 | "title": "Twitter", 14 | "url": "https://x.com/sparrowcode_ios" 15 | }, 16 | { 17 | "title": "Telegram", 18 | "url": "https://t.me/sparrowcode_en" 19 | } 20 | ] 21 | }, 22 | "svyatoynick": { 23 | "name": "Nikolay Pelevin", 24 | "description": "iOS Developer, candy lover.", 25 | "avatar": "https://cdn.sparrowcode.io/authors/svyatoynick.jpg", 26 | "github": "svyatoynick", 27 | "buttons": [ 28 | { 29 | "title": "GitHub", 30 | "url": "https://github.com/svyatoynick" 31 | }, 32 | { 33 | "title": "App Store", 34 | "url": "https://apps.pelevin.me" 35 | } 36 | ] 37 | }, 38 | "somenkovnikita": { 39 | "name": "Nikita Somenkov", 40 | "description": "iOS developer. I'm developing my own project, and I'm also in favor of native design", 41 | "avatar": "https://cdn.sparrowcode.io/authors/somenkovnikita.jpg", 42 | "github": "somenkovnikita", 43 | "buttons": [ 44 | { 45 | "title": "GitHub", 46 | "url": "https://github.com/somenkovnikita" 47 | }, 48 | { 49 | "title": "Projects", 50 | "url": "https://apps.somenkov.ru" 51 | } 52 | ] 53 | }, 54 | "sparrowcode": { 55 | "name": "Sparrow Code Editorial", 56 | "description": "We do tutorials and opensource for iOS developers.", 57 | "avatar": "https://cdn.sparrowcode.io/authors/sparrowcode.jpg?v=5", 58 | "github": "sparrowcode", 59 | "buttons": [ 60 | { 61 | "title": "GitHub", 62 | "url": "https://github.com/sparrowcode" 63 | }, 64 | { 65 | "title": "X", 66 | "url": "https://x.com/sparrowcode_ios" 67 | }, 68 | { 69 | "title": "App Store", 70 | "url": "https://apps.apple.com/developer/id1617623165" 71 | }, 72 | { 73 | "title": "Telegram", 74 | "url": "https://t.me/sparrowcode_en" 75 | } 76 | ] 77 | }, 78 | "alxrguz": { 79 | "name": "Alexander Guzenko", 80 | "description": "iOS developer. I love native design and bike.", 81 | "avatar": "https://cdn.sparrowcode.io/authors/alxrguz.jpg", 82 | "github": "alxrguz", 83 | "buttons": [ 84 | { 85 | "title": "GitHub", 86 | "url": "https://github.com/alxrguz" 87 | }, 88 | { 89 | "title": "App Store", 90 | "url": "https://apps.apple.com/developer/id1480235724" 91 | } 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ru/tutorials/how-to-get-root-view-controller.md: -------------------------------------------------------------------------------- 1 | Чтобы получить root-контроллер, нужно глянуть на иерархию приложения. 2 | 3 | # Scenes (Сцены) для iOS 13 и выше 4 | 5 | Наглядно UI-архитектура с iOS 13 на картинке: 6 | 7 | ![Схема `UIWindowScene` c iOS 13 и выше.](https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindowscene.jpg) 8 | 9 | На экране может быть несколько сцен, а у сцен несколько окон. Для каждого окна свой root-контроллер, а это значит что у приложения может быть больше одного root-контроллера. 10 | 11 | Допустим вы ищите root-контроллер только для активной сцены, отфильтруем их: 12 | 13 | ```swift 14 | // Window есть только у `UIWindowScene`: 15 | let windowScenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } 16 | 17 | // Получаем активные: 18 | let activeScenes = windowScenes.filter { $0.activationState == .foregroundActive } 19 | ``` 20 | 21 | Теперь у сцены можно получить `keyWindow`: 22 | 23 | ```swift 24 | let firstActiveScene = activeScene.first 25 | let keyWindow = firstActiveScene?.keyWindow?.rootViewController 26 | ``` 27 | 28 | Но на экране может быть два равнозначных окна. Например, две заметки в Split-режиме на iPad. В переборе вам нужно **выбрать главную сцену и контроллер вручную**. Сделать это можно через проверку типа: 29 | 30 | ```swift 31 | // Получаем сцену по классу делегата: 32 | let scene = windowScenes.first(where: { ($0.delegate as? RootSceneDelegate) != nil }) 33 | 34 | // Перебираем окна с нужным root-контроллером: 35 | let controller = scene?.windows.first(where: { $0.rootViewController as? RootSplitController != nil }) 36 | ``` 37 | 38 | > Начиная с iOS 13 главного root-контроллера нет. Вы сами решаете какой из них главный. 39 | 40 | # Windows (Окна) для iOS 12 и ниже 41 | 42 | До iOS 13 были только Window. Root-контроллер можно получить однозначно - с ним запускается приложение: 43 | 44 | ![Схема `UIWindow` для iOS 12 и ниже.](https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindow.jpg) 45 | 46 | Чтобы получить root, нужно получить key-window и обратится к `rootViewController`: 47 | 48 | ```swift 49 | // Главное окно -> главный контроллер 50 | UIApplication.shared.keyWindow?.rootViewController 51 | ``` 52 | 53 | Альтернативный способ обратится к массиву окон и взять первое: 54 | 55 | ```swift 56 | UIApplication.shared.windows.first?.rootViewController 57 | ``` 58 | 59 | Первое окно всегда было root, потому что с ним запускалось приложение. 60 | 61 | > Проперти `UIApplication.shared.keyWindow` и `UIApplication.shared.windows` deprecated. Но если ваше приложение не использует сцены, то варнинга не будет. 62 | 63 | # Для SwiftUI 64 | 65 | Вы можете сохранить root-view и передать её как параметр. Но если вы хотите доступ через UIKit, то вызов `UIApplication` работает и для SwiftUI. 66 | 67 | Если вы хотите развернуть root-контроллер красиво, например, получить `UISplitViewController` из UIKit в коде SwiftUI, попробуйте библиотеку SwiftUI Introspect: 68 | 69 | [SwiftUI Introspect](https://github.com/siteline/swiftui-introspect): Introspect underlying UIKit/AppKit components from SwiftUI. 70 | 71 | Так например для root-вью `NavigationSplitView`: 72 | 73 | ```swift 74 | NavigationSplitView { 75 | Text("Root") 76 | } detail: { 77 | Text("Detail") 78 | } 79 | .introspect(.navigationSplitView, on: .iOS(.v16, .v17)) { 80 | print(type(of: $0)) // Здесь UISplitViewController 81 | } 82 | ``` 83 | 84 | 85 | -------------------------------------------------------------------------------- /en/tutorials/edge-insets-uibutton.md: -------------------------------------------------------------------------------- 1 | You control three indents - `imageEdgeInsets`, `titleEdgeInsets` and `contentEdgeInsets`. Before diving into the process, take a look at [sample project](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/example-project.zip). The project clearly shows how the indentation combinations work. In the video, I put a fill for the elements: 2 | - Red -> background 3 | - Yellow -> icon 4 | - Blue -> title 5 | 6 | [Indent control in `UIButton`.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/edge-insets-uibutton-example-preview.mov) 7 | 8 | # `contentEdgeInsets` 9 | 10 | Adds indents around the header and icon. If you put negative values, the indentation will be reduced. Code: 11 | 12 | ```swift 13 | previewButton.contentEdgeInsets.left = 10 14 | previewButton.contentEdgeInsets.right = 10 15 | previewButton.contentEdgeInsets.top = 5 16 | previewButton.contentEdgeInsets.bottom = 5 17 | ``` 18 | 19 | ![`contentEdgeInsets` indents.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/content-edge-insets.png) 20 | 21 | The indentation around the content affects only the button size. The frame and the clickable area are enlarged accordingly. 22 | 23 | # `imageEdgeInsets` and `titleEdgeInsets` 24 | 25 | They are in the same section, because your task is to add indents on one side and reduce them on the other. Let's add an indent between the picture and the header `10pt`. The first idea is to add an indent through the property `imageEdgeInsets`: 26 | 27 | [Indent `imageEdgeInsets` between the icon and the text.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/image-edge-insets-space-icon-title.mov) 28 | 29 | The indentation is added, but does not affect the size of the button — the icon flies behind the button. TitleEdgeInsets` behaves the same way — it doesn't change button size. If you indent the text positively to the left and the icon negatively indented to the left - then there will be a distance of 10pt between the text and the icon. 30 | 31 | ```swift 32 | previewButton.imageEdgeInsets.left = -10 33 | previewButton.titleEdgeInsets.left = 10 34 | ``` 35 | 36 | This is the symmetry I wrote about above. 37 | 38 | > `contentEdgeInsets` changes the size of the button. 39 | > The `imageEdgeInsets` and `titleEdgeInsets` do not. 40 | 41 | # Icon to the right of the text 42 | 43 | Let's put the icon to the right of the header: 44 | 45 | ```swift 46 | let buttonWidth = previewButton.frame.width 47 | let imageWidth = previewButton.imageView?.frame.width ?? .zero 48 | ``` 49 | 50 | Shift the header to the left edge. The indent on the left was `imageWidth`. If you decrease by this value, you get the left edge. 51 | 52 | ```swift 53 | previewButton.titleEdgeInsets = UIEdgeInsets( 54 | top: 0, 55 | left: -imageWidth, 56 | bottom: 0, 57 | right: imageWidth 58 | ) 59 | ``` 60 | 61 | We move the icon to the right edge. The default indent was `0`, so the new Y point will have the width of the icon. 62 | 63 | ```swift 64 | previewButton.imageEdgeInsets = UIEdgeInsets( 65 | top: 0, 66 | left: buttonWidth - imageWidth, 67 | bottom: 0, 68 | right: 0 69 | ) 70 | ``` 71 | 72 | # Deprecated 73 | 74 | Note, from iOS 15 the indentations are marked as `deprecated`. 75 | 76 | ![Screenshot from Apple Developer website.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/depricated.png) 77 | 78 | Property will work for a few years. Apple recommends using the configuration. 79 | 80 | You can play with the indents in [sample project](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/example-project.zip). 81 | -------------------------------------------------------------------------------- /en/tutorials/how-to-get-root-view-controller.md: -------------------------------------------------------------------------------- 1 | To get root control, you need to look at the application hierarchy. 2 | 3 | # Scenes for iOS 13 and later 4 | 5 | The UI architecture with iOS 13: 6 | 7 | ![`UIWindowScene` c iOS 13 and above.](https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindowscene.jpg) 8 | 9 | There can be several scenes on the screen, and scenes can have several windows. Each window has its own root controller, which means that an application can have more than one root controller. 10 | 11 | Let's say you're only looking for root controllers for the active scene, let's filter them out: 12 | 13 | ```swift 14 | // Window only has `UIWindowScene`.: 15 | let windowScenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } 16 | 17 | // Getting active: 18 | let activeScenes = windowScenes.filter { $0.activationState == .foregroundActive } 19 | ``` 20 | 21 | The scene have a `keyWindow`: 22 | 23 | ```swift 24 | let firstActiveScene = activeScene.first 25 | let keyWindow = firstActiveScene?.keyWindow?.rootViewController 26 | ``` 27 | 28 | Но на экране может быть два равнозначных окна. Например, две заметки в Split-режиме на iPad. В переборе вам нужно **выбрать главную сцену и контроллер вручную**. Сделать это можно через проверку типа: 29 | 30 | But you can have two equivalent windows on the screen. For example, two notes in Split mode on an iPad. You need to **select the main scene and controller manually**. You can do this through type checking: 31 | 32 | ```swift 33 | // Get the scene by delegate class: 34 | let scene = windowScenes.first(where: { ($0.delegate as? RootSceneDelegate) != nil }) 35 | 36 | // Go through the windows with the root controller: 37 | let controller = scene?.windows.first(where: { $0.rootViewController as? RootSplitController != nil }) 38 | ``` 39 | 40 | > As of iOS 13, there is no main root controller. You decide which one is the root. 41 | 42 | # Windows for iOS 12 and below 43 | 44 | Before iOS 13, there were only Window. Root Controller can be obtained unambiguously - the application is launched with it: 45 | 46 | ![`UIWindow` for iOS 12 and below.](https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindow.jpg) 47 | 48 | To get root, you need to get key-window and access `rootViewController`: 49 | 50 | ```swift 51 | // Key window -> root controller 52 | UIApplication.shared.keyWindow?.rootViewController 53 | ``` 54 | 55 | An alternative way is to access the array of windows and grab the first one: 56 | 57 | ```swift 58 | UIApplication.shared.windows.first?.rootViewController 59 | ``` 60 | 61 | The first window was always root, because the application started with it. 62 | 63 | > The `UIApplication.shared.keyWindow` and `UIApplication.shared.windows` properties are deprecated. But if your application does not use scenes, there will be no warning. 64 | 65 | # For SwiftUI 66 | 67 | You can save root-view and pass it as a parameter. But if you want access via UIKit, the `UIApplication.shared` call works for SwiftUI as well. 68 | 69 | If you want to get the root controller nicely, such as getting `UISplitViewController` from UIKit in SwiftUI code, try the SwiftUI Introspect framework: 70 | 71 | [SwiftUI Introspect](https://github.com/siteline/swiftui-introspect): Introspect underlying UIKit/AppKit components from SwiftUI. 72 | 73 | So for example for root view `NavigationSplitView`: 74 | 75 | ```swift 76 | NavigationSplitView { 77 | Text("Root") 78 | } detail: { 79 | Text("Detail") 80 | } 81 | .introspect(.navigationSplitView, on: .iOS(.v16, .v17)) { 82 | print(type(of: $0)) // Здесь UISplitViewController 83 | } 84 | ``` 85 | 86 | 87 | -------------------------------------------------------------------------------- /ru/tutorials/edge-insets-uibutton.md: -------------------------------------------------------------------------------- 1 | ![Про `contentEdgeInsets` в Swift.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/preview.png) 2 | 3 | Вы управляете тремя отступами - `imageEdgeInsets`, `titleEdgeInsets` и `contentEdgeInsets`. Перед погружением в процесс, гляньте [проект-пример](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/example-project.zip). В проекте наглядно показывается как работают комбинации отступов. На видео я поставил заливку для элементов: 4 | - Красный -> фон 5 | - Жёлтая -> иконка 6 | - Синий -> заголовок 7 | 8 | [Управление отступами у `UIButton`.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/edge-insets-uibutton-example-preview.mov) 9 | 10 | # `contentEdgeInsets` 11 | 12 | Добавляет отступы вокруг заголовка и иконки. Если поставить отрицательные значения - отступ будет уменьшаться. Код: 13 | 14 | ```swift 15 | previewButton.contentEdgeInsets.left = 10 16 | previewButton.contentEdgeInsets.right = 10 17 | previewButton.contentEdgeInsets.top = 5 18 | previewButton.contentEdgeInsets.bottom = 5 19 | ``` 20 | 21 | ![`contentEdgeInsets` отступы.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/content-edge-insets.png) 22 | 23 | Отступы вокруг контента влияют только на размер кнопки. Фрейм и кликабельная область увеличиваются соответственно. 24 | 25 | # `imageEdgeInsets` и `titleEdgeInsets` 26 | 27 | Они в одной секции, потому что ваша задача добавить отступы с одной стороны и уменьшить их с другой. Добавим отступ между картинкой и заголовком `10pt`. Первая идея - добавить отступ через проперти `imageEdgeInsets`: 28 | 29 | [Отступ `imageEdgeInsets` между иконкой и текстом.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/image-edge-insets-space-icon-title.mov) 30 | 31 | Отступ добавляется, но не влияет на размер кнопки - иконка вылетает за кнопку. `titleEdgeInsets` ведет себя так же - не меняет размер кнопки. Если для текста поставить положительный отступ слева, а для иконки отрицательный отступ слева - то появится расстояние в 10pt между текстом и иконкой. 32 | 33 | ```swift 34 | previewButton.imageEdgeInsets.left = -10 35 | previewButton.titleEdgeInsets.left = 10 36 | ``` 37 | 38 | Это та симметрия, про которую писал выше. 39 | 40 | > `contentEdgeInsets` меняет размер кнопки. 41 | > `imageEdgeInsets` и `titleEdgeInsets` не меняют размер кнопки. 42 | 43 | # Иконка справа от текста 44 | 45 | Давайте поставим иконку справа от заголовка: 46 | 47 | ```swift 48 | let buttonWidth = previewButton.frame.width 49 | let imageWidth = previewButton.imageView?.frame.width ?? .zero 50 | ``` 51 | 52 | Смещаем заголовок к левому краю. Отступ слева был `imageWidth`. Если уменьшите на это значение, то получите левый край. 53 | 54 | ```swift 55 | previewButton.titleEdgeInsets = UIEdgeInsets( 56 | top: 0, 57 | left: -imageWidth, 58 | bottom: 0, 59 | right: imageWidth 60 | ) 61 | ``` 62 | 63 | Перемещаем иконку к правому краю. Дефолтный отступ был `0`, значит, у новой точки Y шириной станет ширина иконки. 64 | 65 | ```swift 66 | previewButton.imageEdgeInsets = UIEdgeInsets( 67 | top: 0, 68 | left: buttonWidth - imageWidth, 69 | bottom: 0, 70 | right: 0 71 | ) 72 | ``` 73 | 74 | # Deprecated 75 | 76 | Обратите внимание, с iOS 15 отступы помечены как `depriсated`. 77 | 78 | ![Скриншот с сайта Apple Developer.](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/depricated.png) 79 | 80 | Несколько лет проперти будут работать. Apple рекомендуют использовать конфигурацию. 81 | 82 | Поиграть с отступами можно в [проекте-примере](https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/example-project.zip). Задать вопрос в комментариях [к посту](https://t.me/sparrowcode/99). 83 | -------------------------------------------------------------------------------- /ru/tutorials/set-launch-screen-via-plist.md: -------------------------------------------------------------------------------- 1 | # Как удалить LaunchScreen.storyboard 2 | 3 | По умолчанию `LaunchScreen.storyboard`-файл создается только для UIKit-проектов. Сначала удалите его: 4 | 5 | ![Как удалить `LaunchScreen.storyboard`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/delete-launchscreen-storyboard-file.jpg) 6 | 7 | Теперь выберите таргет приложения и перейдите на вкладку `Info`. Здесь нужно удалить ключ «Launch screen interface file base name» или `UILaunchStoryboardName`: 8 | 9 | ![Удалить ключ `UILaunchStoryboardName`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/delete-launch-screen-interface-file-base-name-key.jpg) 10 | 11 | Теперь здесь же добавить словарь `UILaunchScreen`: 12 | 13 | ![Добавить словарь `UILaunchScreen`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-uilaunchscreen-key.jpg) 14 | 15 | Словарь можно оставить пустым, тогда фон будет цвета `.systemBackground`. 16 | 17 | # Настроить Launch Screen через `.plist` 18 | 19 | Доступно для UIKit и SwiftUI начиная с iOS 14. 20 | 21 | Можно добавить плейсхолдеры Tab/Nav/Tool-баров, чтобы переход между Launch Screen и стартовым контроллером был плавный. Ещё можно задать цвет фона и поставить картинку. Для всего этого указываем специальные ключи в plist-файле. 22 | 23 | > Вы можете комбинировать ключи, например установить фон, картинку и Tab-бар вместе. 24 | 25 | Разберем все 6 ключей: 26 | 27 | ## Background color 28 | 29 | В Assets добавьте новый цвет, можно выбрать разные цвета для темной и светлой темы: 30 | 31 | ![Новый цвет в Assets.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-color-to-assets.jpg) 32 | 33 | В словарь «Launch Screen» добавьте ключ `UIColorName` с именем цвета: 34 | 35 | ![Добавляем ключ `UIColorName`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-background-color-launch-screen-key.jpg) 36 | 37 | Теперь Launch Screen будет залит цветом: 38 | 39 | ![Результат с `UIColorName`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uicolorname-result.jpg) 40 | 41 | ## Image name 42 | 43 | Можно установить картинку в центр Launch Screen. Добавляем картинку в Assets, а дальше добавьте ключ `UIImageName` и укажите имя картинки. Результат: 44 | 45 | ![Результат с `UIImageName`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uiimagename-result.jpg) 46 | 47 | > Launch Screen кэшируется, поэтому если изменили картинку — симулятор нужно сбросить через `Device` → `Erase All Content and Settings...`. 48 | 49 | ## Image respects safe area insets 50 | 51 | Ключ `UIImageRespectsSafeAreaInsets` должен влиять на размер картинки и вписывать ее в Safe Area. Я ставил разные картинки, но ключ ни на что не влияет. Проверял на iOS 17.2. Возможно это баг и его оправят в будущем. 52 | 53 | ## Show Tab Bar 54 | 55 | Чтобы показать плейсхолдер Tab-бара, добавьте пустой словарь `UITabBar`: 56 | 57 | ![Добавить словарь `UITabBar`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-uitabbar-key.jpg) 58 | 59 | Снизу появится плейсхолдер Tab-бара: 60 | 61 | ![Результат c `UITabBar`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uitabbar-result.jpg) 62 | 63 | > Высота Tab-бара на Launch Screen выше, чем должна быть. Это баг. Пока рекомендую использовать `Toolbar`, про него ниже. 64 | 65 | ## Show Toolbar 66 | 67 | Аналогично можно показать плейсхолдер Tool-бара, для этого добавьте пустой словарь `UIToolbar`: 68 | 69 | ![Результат c `UIToolbar`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uitoolbar-result.jpg) 70 | 71 | ## Navigation bar 72 | 73 | Чтобы добавить Navigation-бар, добавьте словарь `UINavigationBar`. По дефолту у Navigation-бара с большим заголовком фона нет, поэтому когда установите ключ - ничего не изменится. 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /en/tutorials/set-launch-screen-via-plist.md: -------------------------------------------------------------------------------- 1 | # How to drop LaunchScreen.storyboard 2 | 3 | By default `LaunchScreen.storyboard` file is created only for UIKit projects. Delete it first: 4 | 5 | ![How to drop `LaunchScreen.storyboard`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/delete-launchscreen-storyboard-file.jpg) 6 | 7 | Now select the app target and go to the `Info` tab. Here you need to remove the key "Launch screen interface file base name" or `UILaunchStoryboardName`: 8 | 9 | ![Delete the `UILaunchStoryboardName` key.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/delete-launch-screen-interface-file-base-name-key.jpg) 10 | 11 | Now add the `UILaunchScreen` dictionary here as well: 12 | 13 | ![Add `UILaunchScreen` dictionary.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-uilaunchscreen-key.jpg) 14 | 15 | The dictionary can be left blank, then the background will be the color `.systemBackground`. 16 | 17 | # Set Launch Screen via `.plist` 18 | 19 | Available for UIKit and SwiftUI starting with iOS 14. 20 | 21 | You can add Tab/Nav/Tool-bar placeholders to make the transition between Launch Screen and Root Controller smooth. You can also set the background color and put an image. For all this we specify special keys in plist-file. 22 | 23 | > You can combine keys, for example, set background, image and Tab bar. 24 | 25 | Let's check all six keys: 26 | 27 | ## Background color 28 | 29 | In Assets add a new color, you can choose different colors for dark and light theme: 30 | 31 | ![New color in Assets.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-color-to-assets.jpg) 32 | 33 | In the 'Launch Screen dictionary', add the `UIColorName` key with the name of the color: 34 | 35 | ![Add the `UIColorName` key.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-background-color-launch-screen-key.jpg) 36 | 37 | The Launch Screen will now be filled with color: 38 | 39 | ![Result with `UIColorName`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uicolorname-result.jpg) 40 | 41 | ## Image name 42 | 43 | You can set the image to the center of the Launch Screen. Add the picture to Assets, and then add the `UIImageName` key and specify the name of the picture. Result: 44 | 45 | ![Result with `UIImageName`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uiimagename-result.jpg) 46 | 47 | > Launch Screen is cached, so if you changed the image - the simulator should be reset via `Device` → `Erase All Content and Settings...`. 48 | 49 | ## Image respects safe area insets 50 | 51 | The `UIImageRespectsSafeAreaInsets` key should affect the size of the picture and fit it into the Safe Area. I've put different images, but the key doesn't affect anything. I checked on iOS 17.2. Maybe it's a bug and will be fixed in the future. 52 | 53 | ## Show Tab Bar 54 | 55 | To show the Tab bar placeholder, add an empty `UITabBar` dictionary: 56 | 57 | ![Add `UITabBar` dictionary.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-uitabbar-key.jpg) 58 | 59 | The Tab bar placeholder will appear at the bottom: 60 | 61 | ![Result with `UITabBar`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uitabbar-result.jpg) 62 | 63 | > The height of Tab-bar on Launch Screen is higher than it should be. This is a bug. For now, I recommend to use `Toolbar`, about it below. 64 | 65 | ## Show Toolbar 66 | 67 | Similarly, you can show the Tool-bar placeholder by adding an empty `UIToolbar` dictionary: 68 | 69 | ![Result with `UIToolbar`.](https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uitoolbar-result.jpg) 70 | 71 | ## Navigation bar 72 | 73 | To add a Navigation-bar, add the `UINavigationBar` dictionary. By default, Navigation-bar with a large header has no background, so when you set the key, nothing will change. 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /ru/tutorials/testing-push-notifications-ios-simulator.md: -------------------------------------------------------------------------------- 1 | Перед тем как тестировать push-уведомления на симуляторе, нужно получить разрешение от пользователя. Как запросить разрешение написано в конце туториала. На симуляторе можно тестировать как обычные, так и Rich-уведомления, это которые с картинками, звуками и кнопками-действиями. 2 | 3 | > Apple Push Notification Service-сервер присылает устройствам файл c контентом уведомления. Чтобы тестировать пуш-уведомления, можно сэмулировать этот запрос 4 | 5 | Можно это сделать через json-файл с данными, или через терминал. 6 | 7 | # Перетащить json-файла 8 | 9 | Создаем файл с данными для пуша. Здесь я добавлю текст, звук и число в бейдже иконки приложения: 10 | 11 | ```JSON 12 | { 13 | "aps" : { 14 | "alert" : { 15 | "title" : "Game Request", 16 | "body" : "Bob wants to play poker" 17 | }, 18 | "badge" : 9, 19 | "sound" : "bingbong.aiff" 20 | } 21 | } 22 | ``` 23 | 24 | Вы можете указать больше контента, например, картинку или действия. Все доступные ключи для push-уведомлений [по ссылке](https://developer.apple.com/documentation/usernotifications/unnotificationcontent). 25 | 26 | Теперь в файл нужно добавить `Simulator Target Bundle`, чтобы симулятор понимал какому таргету прилетает пуш: 27 | 28 | ```JSON 29 | { 30 | "aps" : { 31 | "alert" : { 32 | "title" : "Game Request", 33 | "body" : "Bob wants to play poker" 34 | } 35 | }, 36 | "Simulator Target Bundle": "com.bundle.example" 37 | } 38 | ``` 39 | 40 | Если бандл не указали, то получите такую ошибку: 41 | 42 | ![Ошибка, потому что не указали Target Bundle](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/invalid-notification.png?v=2) 43 | 44 | Если все в порядке, то на симуляторе появится пуш: 45 | 46 | ![Пуш уведомление](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/push.png?v=2) 47 | 48 | # Через Terminal 49 | 50 | В этом способе вы так же используете APNS-файл, но передаете его через терминал. Проверьте в настройках Xcode что `Command Line Tools` установлен, иначе **simctl** будет выдавать ошибку. Если внизу не видно путь, то выберите еще раз версию Xcode: 51 | 52 | ![Включаем Command Line Tools](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/command-line-tools.png?v=2) 53 | 54 | Для отправки пуша используется команда: 55 | 56 | ```console 57 | xcrun simctl push 58 | ``` 59 | 60 | `Bundle id` - это бандл вашего приложения. А чтобы узнать `id simulator` используется команда: 61 | 62 | ```console 63 | xcrun simctl list 64 | ``` 65 | 66 | Она покажет список всех симуляторов и их id. Обратите внимание, у запущенного симулятора будет указанно *Booted*: 67 | 68 | ![Список всех доступных симуляторов](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/id-simulator-list.png?v=2) 69 | 70 | 71 | Собираем команду с `id симулятора` и вызываем: 72 | 73 | ```console 74 | xcrun simctl push 4D1C144E-7C68-484D-894D-CF17928D3D3A com.bundle.example payload.apns 75 | ``` 76 | 77 | Если у вас запущен симулятор, то вместо ключа можно указать *Booted*, так пуш автоматически улетит на запущенный симулятор. 78 | 79 | Если все сделано правильно получите такое сообщение: 80 | 81 | ![Сообщение об отправке push-уведомления](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/notification-sent.png?v=2) 82 | 83 | # Разрешения 84 | 85 | Чтобы push-уведомления показывались на симуляторе и устройстве, нужно запросить разрешение. Можно это сделать вручную или через нашу библиотеку. 86 | 87 | ## Запрос разрешения 88 | 89 | Импортируем `UserNotifications` и вызываем системный запрос: 90 | 91 | ```swift 92 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {(granted, error) in 93 | print("Permission Granted: \(granted)") 94 | } 95 | ``` 96 | 97 | Запрашивать нужно в любом месте до отправки уведомлений. Примерно то же самое делает наша библиотека [PermissionsKit](https://github.com/sparrowcode/PermissionsKit): 98 | 99 | ```swift 100 | import PermissionsKit 101 | 102 | Permission.notification.request {} 103 | ``` 104 | 105 | ## Сброс разрешения 106 | 107 | Если нужно сбросить разрешение на push-уведомления, достаточно удалить приложение 108 | 109 | > Иногда разрешение может остаться даже после переустановки, тогда после удаления подождите минуту и установите снова. -------------------------------------------------------------------------------- /ru/tutorials/meta/authors.json: -------------------------------------------------------------------------------- 1 | { 2 | "sparrowcode": { 3 | "name": "Редакция Код Воробья", 4 | "description": "Делаем полезности для iOS разработчиков", 5 | "avatar": "https://cdn.sparrowcode.io/authors/sparrowcode.jpg?v=5", 6 | "github": "sparrowcode", 7 | "social_url": "https://t.me/sparrowcode", 8 | "buttons": [ 9 | { 10 | "title": "GitHub", 11 | "url": "https://github.com/sparrowcode" 12 | }, { 13 | "title": "Телеграм-канал", 14 | "url": "https://t.me/sparrowcode" 15 | }, { 16 | "title": "Телеграм-чат", 17 | "url": "https://t.me/sparrowcodechat" 18 | }, { 19 | "title": "Youtube", 20 | "url": "https://youtube.com/@sparrowcode" 21 | }, { 22 | "title": "App Store", 23 | "url": "https://apps.apple.com/developer/id1617623165" 24 | }, { 25 | "title": "X", 26 | "url": "https://twitter.com/sparrowcode_ios" 27 | } 28 | ] 29 | }, 30 | "ivanvorobei": { 31 | "name": "Иван Воробей", 32 | "description": "iOS разработчик. Пишу библиотеки, веду телеграм-канал Код Воробья", 33 | "avatar": "https://cdn.sparrowcode.io/authors/ivanvorobei.jpg", 34 | "github": "ivanvorobei", 35 | "buttons": [ 36 | { 37 | "title": "GitHub", 38 | "url": "https://github.com/ivanvorobei" 39 | }, { 40 | "title": "Телеграм-канал", 41 | "url": "https://t.me/sparrowcode" 42 | }, { 43 | "title": "Youtube", 44 | "url": "https://youtube.com/@sparrowcode" 45 | } 46 | ] 47 | }, 48 | "alxrguz": { 49 | "name": "Александр Гузенко", 50 | "description": "iOS разработчик. Люблю нативный дизайн и велик.", 51 | "avatar": "https://cdn.sparrowcode.io/authors/alxrguz.jpg", 52 | "github": "alxrguz", 53 | "buttons": [ 54 | { 55 | "title": "GitHub", 56 | "url": "https://github.com/alxrguz" 57 | }, { 58 | "title": "App Store", 59 | "url": "https://apps.apple.com/developer/id1480235724" 60 | } 61 | ] 62 | }, 63 | "somenkovnikita": { 64 | "name": "Никита Соменков", 65 | "description": "iOS разработчик. Развиваю свой проект, и тоже за нативный дизайн", 66 | "avatar": "https://cdn.sparrowcode.io/authors/somenkovnikita.jpg", 67 | "github": "somenkovnikita", 68 | "buttons": [ 69 | { 70 | "title": "GitHub", 71 | "url": "https://github.com/somenkovnikita" 72 | }, { 73 | "title": "Projects", 74 | "url": "https://apps.somenkov.ru" 75 | } 76 | ] 77 | }, 78 | "svyatoynick": { 79 | "name": "Николай Пелевин", 80 | "description": "iOS Разработчик, люблю конфеты.", 81 | "avatar": "https://cdn.sparrowcode.io/authors/svyatoynick.jpg", 82 | "github": "svyatoynick", 83 | "buttons": [ 84 | { 85 | "title": "GitHub", 86 | "url": "https://github.com/svyatoynick" 87 | }, { 88 | "title": "App Store", 89 | "url": "https://apps.pelevin.me" 90 | } 91 | ] 92 | }, 93 | "liubowolkova": { 94 | "name": "Любовь Волкова", 95 | "description": "Люблю матан, swift и 🐺", 96 | "avatar": "https://cdn.sparrowcode.io/authors/liubowolkova.jpg", 97 | "github": "liubowolkova", 98 | "buttons": [ 99 | { 100 | "title": "GitHub", 101 | "url": "https://github.com/liubowolkova" 102 | } 103 | ] 104 | }, 105 | "rentel": { 106 | "name": "Команда Rentel", 107 | "description": "Мобильная касса для iOS-устройств на SwiftUI", 108 | "avatar": "https://cdn.sparrowcode.io/authors/rentel.jpg", 109 | "github": "iOSRentel", 110 | "buttons": [ 111 | { 112 | "title": "Сайт", 113 | "url": "https://rentel.app/" 114 | }, { 115 | "title": "Телеграм-канал", 116 | "url": "https://t.me/rentelbusiness" 117 | }, { 118 | "title": "Приложение", 119 | "url": "https://apps.apple.com/ru/developer/rentel-ooo/id1632637158" 120 | }, { 121 | "title": "GitHub", 122 | "url": "https://github.com/iOSRentel" 123 | } 124 | ] 125 | } 126 | } -------------------------------------------------------------------------------- /en/tutorials/testing-push-notifications-ios-simulator.md: -------------------------------------------------------------------------------- 1 | Before testing push-notifications on the simulator, you need to get permission from the user. How to request permission is described at the end of the tutorial. You can test both regular and Rich-notifications, which are notifications with pictures, sounds and action-buttons. 2 | 3 | > Apple Push Notification Service-server sends a notification content file to devices. To test push notifications, you can simulate this request 4 | 5 | You can do this through a json-file with data, or through a terminal. 6 | 7 | # Drag and drop json-file 8 | 9 | Create a file with the data for the push. Here I will add text, sound and number to the application icon badge: 10 | 11 | ```JSON 12 | { 13 | "aps" : { 14 | "alert" : { 15 | "title" : "Game Request", 16 | "body" : "Bob wants to play poker" 17 | }, 18 | "badge" : 9, 19 | "sound" : "bingbong.aiff" 20 | } 21 | } 22 | ``` 23 | 24 | You can specify more content, such as a picture or actions. All available keys for push notifications at the [link](https://developer.apple.com/documentation/usernotifications/unnotificationcontent). 25 | 26 | Now you need to add `Simulator Target Bundle` to the file, so that the simulator understands which target is getting a push: 27 | 28 | ```JSON 29 | { 30 | "aps" : { 31 | "alert" : { 32 | "title" : "Game Request", 33 | "body" : "Bob wants to play poker" 34 | } 35 | }, 36 | "Simulator Target Bundle": "com.bundle.example" 37 | } 38 | ``` 39 | 40 | If the bundle is not specified, you will get this error: 41 | 42 | ![Error because you did not specify a Target Bundle](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/invalid-notification.png?v=2) 43 | 44 | If all is well, a push will appear on the simulator: 45 | 46 | ![Push notification](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/push.png?v=2) 47 | 48 | # Through Terminal 49 | 50 | In this method you also use the APNS-file, but you pass it through the terminal. Check in the Xcode settings that `Command Line Tools` is set, otherwise **simctl** will give an error. If you can't see the path at the bottom, select the Xcode version again: 51 | 52 | ![Turn on Command Line Tools](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/command-line-tools.png?v=2) 53 | 54 | The command is used to send a push: 55 | 56 | ```console 57 | xcrun simctl push 58 | ``` 59 | 60 | The `Bundle id` is the bundle of your application. And to find out the `id simulator` the command is used: 61 | 62 | ```console 63 | xcrun simctl list 64 | ``` 65 | 66 | It will show a list of all simulators and their id. Note that a running simulator will have *Booted*: 67 | 68 | ![List of all available simulators](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/id-simulator-list.png?v=2) 69 | 70 | 71 | Collect the command with the `id simulator` and call it: 72 | 73 | ```console 74 | xcrun simctl push 4D1C144E-7C68-484D-894D-CF17928D3D3A com.bundle.example payload.apns 75 | ``` 76 | 77 | If you have a simulator running, you can specify *Booted* instead of the key, so the push will automatically fly to the running simulator. 78 | 79 | If everything is done correctly, you will get this message: 80 | 81 | ![Message about sending a push-notification](https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/notification-sent.png?v=2) 82 | 83 | # Permission 84 | 85 | In order for push-notifications to be shown on the simulator and the device, you need to request permission. You can do this manually or via our library. 86 | 87 | ## Permission request 88 | 89 | Import `UserNotifications` and invoke the system query: 90 | 91 | ```swift 92 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) {(granted, error) in 93 | print("Permission Granted: \(granted)") 94 | } 95 | ``` 96 | 97 | Requests need to be made anywhere before notices are sent. This is roughly what our library does [PermissionsKit](https://github.com/sparrowcode/PermissionsKit) : 98 | 99 | ```swift 100 | import PermissionsKit 101 | 102 | Permission.notification.request {} 103 | ``` 104 | 105 | ## Permission reset 106 | 107 | If you need to reset the permission for push-notifications, all you need to do is uninstall the app 108 | 109 | > Sometimes the permission may remain even after reinstalling, then after uninstalling wait a minute and install again. -------------------------------------------------------------------------------- /en/tutorials/sf-symbols-and-render-mode.md: -------------------------------------------------------------------------------- 1 | Keep an eye on the compatibility of the symbols - not all symbols are available for iOS 14 and earlier. You can see which version of the symbol is available [in the app](https://developer.apple.com/sf-symbols/). The code examples will be for `SwiftUI` and `UIKit`. 2 | 3 | Render Modes is to render an icon in a color scheme. Monochrome, Hierarchical, Palette and Multicolor are available. 4 | 5 | ![SFSymbols Render Modes.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/render-modes-preview.jpg) 6 | 7 | The symbol may not support all renderings. If no rendering is available, the symbol will be rendered in monochrome. You can compare renders in the official [SF Symbols](https://developer.apple.com/sf-symbols/) application. 8 | 9 | # Monochrome Render 10 | 11 | The icon is filled with color. Control the color through `tintColor`. 12 | 13 | ```swift 14 | // UIKit 15 | let image = UIImage(systemName: "doc") 16 | let imageView = UIImageView(image: image) 17 | imageView.tintColor = .systemRed 18 | 19 | // SwiftUI 20 | Image(systemName: "doc") 21 | .foregroundColor(.red) 22 | ``` 23 | 24 | The method works not only for SF Symbols, but for any image. 25 | 26 | # Hierarchical Render 27 | 28 | Draws the icon in one color, but creates depth with transparency for the elements of the symbol. 29 | 30 | ![SFSymbols Hierarchical Render.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/hierarchical-render.jpg) 31 | 32 | ```swift 33 | // UIKit 34 | let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemIndigo) 35 | let image = UIImage(systemName: "square.stack.3d.down.right.fill", withConfiguration: config) 36 | 37 | // SwiftUI 38 | Image(systemName: "square.stack.3d.down.right.fill") 39 | .symbolRenderingMode(.hierarchical) 40 | .foregroundColor(.indigo) 41 | ``` 42 | 43 | Note that sometimes the hierarchical render looks the same as the `Monochrome Render`. 44 | 45 | # Palette Render 46 | 47 | Draws the icon in custom colors. Each symbol needs a specific number of colors. 48 | 49 | ![SFSymbols Palette Render.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/palette-render.jpg) 50 | 51 | ```swift 52 | // UIKit 53 | let config = UIImage.SymbolConfiguration(paletteColors: [.systemRed, .systemGreen, .systemBlue]) 54 | let image = UIImage(systemName: "person.3.sequence.fill", withConfiguration: config) 55 | 56 | // SwiftUI 57 | Image(systemName: "person.3.sequence.fill") 58 | .symbolRenderingMode(.palette) 59 | .foregroundStyle(.red, .green, .blue) 60 | ``` 61 | 62 | To preserve the universal API, you can pass any number of colors. Here are the rules by which this works: 63 | 64 | - If a symbol has one segment for a color, it will use the first color specified. 65 | - If the symbol has two segments, but one color is specified, it will be used for both segments. 66 | - If you specify two colors, they will be applied accordingly. 67 | - If you specify three colors for a symbol with two segments, the third is ignored. 68 | 69 | # Multicolor Render 70 | 71 | Important elements will be painted in a fixed color, while the filler color can be customized. In the preview, the filler color is `.systemCyan`: 72 | 73 | ![Multicolor Render в SFSymbols.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/multicolor-render.jpg) 74 | 75 | ```swift 76 | // UIKit 77 | let config = UIImage.SymbolConfiguration.configurationPreferringMulticolor() 78 | let image = UIImage(systemName: "externaldrive.badge.plus", withConfiguration: config) 79 | 80 | // SwiftUI 81 | Image(systemName: "externaldrive.badge.plus") 82 | .symbolRenderingMode(.multicolor) 83 | ``` 84 | 85 | Images that do not have a multicolor version will automatically be displayed in `Monochrome Render`. 86 | 87 | # Symbol Variant 88 | 89 | Some symbols have shape support, for example the bell `bell` can be inscribed in a square or a circle. In `UIKit` you have to call them by name - for example `bell.square`, but in SwiftUI there is a modifier `.symbolVariant()`: 90 | 91 | ```swift 92 | // The bell is crossed out 93 | Image(systemName: "bell") 94 | .symbolVariant(.slash) 95 | 96 | // Inscribes in the square 97 | Image(systemName: "bell") 98 | .symbolVariant(.square) 99 | 100 | // You can combine 101 | Image(systemName: "bell") 102 | .symbolVariant(.fill.slash) 103 | ``` 104 | 105 | Note, in the last example you can combine character variants. 106 | 107 | # Adaptation 108 | 109 | SwiftUI knows how to display characters according to context. For iOS, Apple uses filled icons, but in macOS, icons without a fill - just lines. If you use SF Symbols for the Side Bar, you don't need to specify this specifically - the symbol adapts. 110 | 111 | ```swift 112 | Label("Home", systemImage: "person") 113 | .symbolVariant(.none) 114 | ``` -------------------------------------------------------------------------------- /ru/tutorials/sf-symbols-and-render-mode.md: -------------------------------------------------------------------------------- 1 | Следите за совместимостью символов - не все доступны для 14-ой и предыдущих iOS. Глянуть с какой версии доступен символ можно [в приложении](https://developer.apple.com/sf-symbols/). Примеры кода будут для `SwiftUI` и `UIKit`. 2 | 3 | ![Про Render Modes в SF Symbols.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/preview.png) 4 | 5 | Render Modes - это отрисовка иконки в цветовой схеме. Доступны монохром, иерархический, палетка и мульти-цвет. 6 | 7 | ![Render Modes в SFSymbols.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/render-modes-preview.jpg) 8 | 9 | Символ может поддерживать не все рендеры. Если рендер не доступен, то символ будет отрисован в монохроме. Сравнить рендеры можно в официальном приложении [SF Symbols](https://developer.apple.com/sf-symbols/). 10 | 11 | # Monochrome Render 12 | 13 | Иконка заливается цветом. Управлять цветом через `tintColor`. 14 | 15 | ```swift 16 | // UIKit 17 | let image = UIImage(systemName: "doc") 18 | let imageView = UIImageView(image: image) 19 | imageView.tintColor = .systemRed 20 | 21 | // SwiftUI 22 | Image(systemName: "doc") 23 | .foregroundColor(.red) 24 | ``` 25 | 26 | Способ работает не только для SF Symbols, а для любых изображений. 27 | 28 | # Hierarchical Render 29 | 30 | Рисует иконку в одном цвете, но создает глубину с помощью прозрачности для элементов символа. 31 | 32 | ![Hierarchical Render в SFSymbols.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/hierarchical-render.jpg) 33 | 34 | ```swift 35 | // UIKit 36 | let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemIndigo) 37 | let image = UIImage(systemName: "square.stack.3d.down.right.fill", withConfiguration: config) 38 | 39 | // SwiftUI 40 | Image(systemName: "square.stack.3d.down.right.fill") 41 | .symbolRenderingMode(.hierarchical) 42 | .foregroundColor(.indigo) 43 | ``` 44 | 45 | Обратите внимание, иногда иерархический рендер выглядит так же, как `Monochrome Render`. 46 | 47 | # Palette Render 48 | 49 | Рисует иконку в кастомных цветах. Каждому символу нужно конкретное количество цветов. 50 | 51 | ![Palette Render в SFSymbols.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/palette-render.jpg) 52 | 53 | ```swift 54 | // UIKit 55 | let config = UIImage.SymbolConfiguration(paletteColors: [.systemRed, .systemGreen, .systemBlue]) 56 | let image = UIImage(systemName: "person.3.sequence.fill", withConfiguration: config) 57 | 58 | // SwiftUI 59 | Image(systemName: "person.3.sequence.fill") 60 | .symbolRenderingMode(.palette) 61 | .foregroundStyle(.red, .green, .blue) 62 | ``` 63 | 64 | Чтобы сохранить универсальный API, можно передать любое количество цветов. Вот правила, по которым это работает: 65 | 66 | - Если у символа 1 сегмент для цвета, он будет использовать первый указанный цвет. 67 | - Если у символа 2 сегмента, но будет указан 1 цвет, он будет использоваться для обоих сегментов. 68 | - Если укажете 2 цвета — они применятся соответственно. 69 | - Если указать 3 цвета для символа с 2-мя сегментами, третий игнорируется. 70 | 71 | # Multicolor Render 72 | 73 | Важные элементы будут покрашены в фиксированный цвет, а для заполняющего цвет можно настроить. На превью заполняющий цвет `.systemCyan`: 74 | 75 | ![Multicolor Render в SFSymbols.](https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/multicolor-render.jpg) 76 | 77 | ```swift 78 | // UIKit 79 | let config = UIImage.SymbolConfiguration.configurationPreferringMulticolor() 80 | let image = UIImage(systemName: "externaldrive.badge.plus", withConfiguration: config) 81 | 82 | // SwiftUI 83 | Image(systemName: "externaldrive.badge.plus") 84 | .symbolRenderingMode(.multicolor) 85 | ``` 86 | 87 | Изображения, у которых нет многоцветного варианта, будут автоматически отображаться в `Monochrome Render`. 88 | 89 | # Symbol Variant 90 | 91 | Некоторые символы имеют поддержку форм, например колокольчик `bell` можно вписать в квадрат или круг. В `UIKit` нужно вызывать их по имени - например, `bell.square`, но в SwiftUI есть модификатор `.symbolVariant()`: 92 | 93 | ```swift 94 | // Колокольчик перечеркнут 95 | Image(systemName: "bell") 96 | .symbolVariant(.slash) 97 | 98 | // Вписывает в квадрат 99 | Image(systemName: "bell") 100 | .symbolVariant(.square) 101 | 102 | // Можно комбинировать 103 | Image(systemName: "bell") 104 | .symbolVariant(.fill.slash) 105 | ``` 106 | 107 | Обратите внимание, в последнем примере можно комбинировать варианты символов. 108 | 109 | # Адаптация 110 | 111 | SwiftUI умеет отображать символы соответственно контексту. Для iOS Apple использует залитые иконки, но в macOS иконки без заливки - только линии. Если вы используете SF Symbols для Side Bar, то это не нужно указывать специально - символ адаптируется. 112 | 113 | ```swift 114 | Label("Home", systemImage: "person") 115 | .symbolVariant(.none) 116 | ``` -------------------------------------------------------------------------------- /ru/tutorials/difference-property-wrappers-in-swiftui.md: -------------------------------------------------------------------------------- 1 | `@State` используйте только внутри вью. Изменения стейта перерисовывает вью. 2 | 3 | `@StateObject` будет доступен во всех вью куда вы его передадите. 4 | 5 | `@Binding` создает ссылку на `@State`, для использования в другом вью. 6 | 7 | `@ObservedObject` тоже самое что и `@StateObject`, но при перерисовке уничтожается. 8 | 9 | `@EnvironmentObject` похож на `@ObservedObject`, но передается как модификатор. 10 | 11 | `@Environment` позволяет прочитать значения, встроенные в окружение SwiftUI. 12 | 13 | # @State 14 | 15 | Не храните данные в `@State`, это только для состояний. Когда он меняется вью перерисовывается. 16 | 17 | В примере кнопка, у которой переключаем состояние с Play на Pause: 18 | 19 | ```swift 20 | struct PlayButton: View { 21 | @State private var isPlaying: Bool = false // Create the state. 22 | 23 | var body: some View { 24 | Button(isPlaying ? "Pause" : "Play") { 25 | isPlaying.toggle() // Write the state. 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | > `@State` должен меняться только внутри вью. Поэтому делайте его приватным 32 | 33 | Если у вас большие данные используется **@StateObject**. 34 | 35 | # @StateObject 36 | 37 | Будет доступен во всех вью куда его передадите. Он управляет экземплярами, соответствующих протоколу **ObservableObject**. 38 | 39 | `@Published` помечает свойство за которым нужно наблюдать. `@StateObject` используется во вью, которые должны реагировать на изменения. 40 | 41 | ```swift 42 | class DataProvider: ObservableObject { 43 | @Published var currentValue = "a value" 44 | } 45 | 46 | struct DataOwnerView: View { 47 | @StateObject private var provider = DataProvider() 48 | 49 | var body: some View { 50 | Text("provider value: \(provider.currentValue)") 51 | } 52 | } 53 | ``` 54 | 55 | `@StateObject` остается уникальным и не будет пересоздан если вью перерисуется. 56 | 57 | # @Binding 58 | 59 | Предоставляет доступ по ссылке к стейту другого вью. 60 | 61 | Используется символ `$` для передачи привязываемой ссылки, без него Swift передаст копию значения вместо ссылки. 62 | 63 | ```swift 64 | struct StateView: View { 65 | 66 | @State private var intValue = 0 67 | 68 | var body: some View { 69 | VStack { 70 | Text("intValue equals \(intValue)") 71 | BindingView(intValue: $intValue) // binding reference 72 | } 73 | } 74 | } 75 | ``` 76 | 77 | Меняем значение стейта в новом вью: 78 | 79 | ```swift 80 | struct BindingView: View { 81 | @Binding var intValue: Int 82 | 83 | var body: some View { 84 | Button("Increment") { 85 | intValue += 1 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | # @ObservedObject 92 | 93 | `@ObservedObject` это как `@StateObject`, но наблюдаемые объекты уничтожаются и создаются повторно при перерисовке вью. 94 | 95 | ```swift 96 | class DataProvider: ObservableObject { 97 | @Published var currentValue = "a value" 98 | } 99 | 100 | struct DataOwnerView: View { 101 | @ObservedObject var provider: DataProvider 102 | 103 | var body: some View { 104 | Text("provider value: \(provider.currentValue)") 105 | } 106 | } 107 | ``` 108 | 109 | > Будет плохая производительность, когда часто будет перерисовывать тяжелый объект 110 | 111 | # @EnvironmentObject 112 | 113 | `@EnvironmentObject` то же самое что `@ObservedObject`. Передается через модификатор, а не инициализатор. Хорошо подходит для пользовательских настроек, тем или состояний приложения. 114 | 115 | 116 | ```swift 117 | class DataProvider: ObservableObject { 118 | @Published var currentValue = "value" 119 | } 120 | 121 | struct EnvironmentUsingView: View { 122 | @EnvironmentObject var dependency: DataProvider 123 | 124 | var body: some View { 125 | Text(dependency.currentValue) 126 | } 127 | } 128 | ``` 129 | 130 | ```swift 131 | struct MyApp: App { 132 | @StateObject var dataProvider = DataProvider() 133 | 134 | var body: some Scene { 135 | WindowGroup { 136 | EnvironmentUsingView() 137 | .environmentObject(dataProvider) 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | > `@EnvironmentObject` может вызвать ненужные обновления вью. Часто несколько вью с разных уровней наблюдают за одним и тем же экземпляром и реагируют на него. 144 | 145 | # @Environment 146 | 147 | Окружение - это встроенные значения вью в SwiftUI. 148 | 149 | @Environment позволяет получить значения из окружения — ориентацию, цветовую схему и тд. Все доступные значения можно посмотреть [тут](https://developer.apple.com/documentation/swiftui/environmentvalues). 150 | 151 | ![Значения по умолчанию](https://cdn.sparrowcode.io/tutorials/difference-property-wrappers-in-swiftui/environment-default.png) 152 | 153 | В примере получаем значение цветовой схемы `colorScheme` и обновляем вью при ее изменении: 154 | 155 | ```swift 156 | struct MyView: App { 157 | @Environment(\.colorScheme) var colorScheme: ColorScheme 158 | 159 | var body: some View { 160 | Text("The color scheme is \(colorScheme == .dark ? "dark" : "light")") 161 | } 162 | } 163 | ``` 164 | 165 | Здесь изменяем `Environment` для всей иерархии, добавив модификатор к корневому вью: 166 | 167 | ```swift 168 | @main 169 | struct Property_Wrappers: App { 170 | var body: some Scene { 171 | WindowGroup { 172 | ContentView() 173 | .environment(\.multilineTextAlignment, .center) 174 | .environment(\.lineLimit, nil) 175 | .environment(\.lineSpacing, 8) 176 | } 177 | } 178 | } 179 | ``` 180 | 181 | Каждое вью внутри SwiftUI по умолчанию наследует среду от родительского вью. Можно переопределить любые значения для дочерних вью, присоединив модификатор **.environment**. 182 | -------------------------------------------------------------------------------- /swift-student-challenge/2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "developers": [ 3 | { 4 | "name": "Alperen \u00d6rence", 5 | "source": "https://github.com/alperenorence/HandSignal", 6 | "video": "", 7 | "frameworks": [ 8 | "SwiftUI", 9 | "CoreML" 10 | ], 11 | "status": "accepted", 12 | "github_username": "alperenorence", 13 | "twitter_username": null 14 | }, 15 | { 16 | "name": "Amelia While", 17 | "source": "https://github.com/elihwyma/WWDC2023-Semaphores", 18 | "video": "", 19 | "frameworks": [ 20 | "UIKit", 21 | "AVFoundation", 22 | "Vision" 23 | ], 24 | "status": "submitted", 25 | "github_username": "elihwyma", 26 | "twitter_username": null 27 | }, 28 | { 29 | "name": "Chongin Jeong", 30 | "source": "https://github.com/chongin12/Sometimes", 31 | "video": "https://www.youtube.com/watch?v=qT3PcCvPN44", 32 | "frameworks": [ 33 | "SwiftUI", 34 | "AVFoundation", 35 | "SpriteKit" 36 | ], 37 | "status": "submitted", 38 | "github_username": "chongin12", 39 | "twitter_username": null 40 | }, 41 | { 42 | "name": "Daniel Riege", 43 | "source": "https://github.com/danielriege/WWDC23-Submission", 44 | "video": "", 45 | "frameworks": [ 46 | "simd", 47 | "SceneKit", 48 | "SwiftUI" 49 | ], 50 | "status": "accepted", 51 | "github_username": "danielriege", 52 | "twitter_username": null 53 | }, 54 | { 55 | "name": "David Mazzeo", 56 | "source": "https://github.com/TheIntelCorei9/Swift-Student-Challenge-23", 57 | "video": "https://www.youtube.com/watch?v=ViGDWfh0ViA", 58 | "frameworks": [ 59 | "UIKit", 60 | "SpriteKit", 61 | "Core Motion" 62 | ], 63 | "status": "submitted", 64 | "github_username": "TheIntelCorei9", 65 | "twitter_username": null 66 | }, 67 | { 68 | "name": "Henri Bredt", 69 | "source": "https://github.com/henribredt", 70 | "video": "https://www.youtube.com/watch?v=0ZGPRZ1uUi0", 71 | "frameworks": [ 72 | "SwiftUI" 73 | ], 74 | "status": "submitted", 75 | "github_username": "henribredt", 76 | "twitter_username": null 77 | }, 78 | { 79 | "name": "John Seong", 80 | "source": "https://github.com/wonmor/Atomizer-Swift-Challenge", 81 | "video": "https://www.youtube.com/watch?v=kHcdvyaqslU", 82 | "frameworks": [ 83 | "SwiftUI", 84 | "SceneKit", 85 | "ARKit", 86 | "Vision" 87 | ], 88 | "status": "submitted", 89 | "github_username": "wonmor", 90 | "twitter_username": null 91 | }, 92 | { 93 | "name": "Jose Adolfo Talactac", 94 | "source": "https://github.com/devjoseadolfo/LogicBoard", 95 | "video": "https://youtu.be/Pg_R5nvF2Tw", 96 | "frameworks": [ 97 | "SwiftUI", 98 | "SpriteKit", 99 | "UIKit" 100 | ], 101 | "status": "accepted", 102 | "github_username": "devjoseadolfo", 103 | "twitter_username": null 104 | }, 105 | { 106 | "name": "Myung Geun Choi", 107 | "source": "https://github.com/mgdgc/earth-debugger", 108 | "video": "https://youtu.be/prc4jeNdFfA", 109 | "frameworks": [ 110 | "SwiftUI" 111 | ], 112 | "status": "accepted", 113 | "github_username": "mgdgc", 114 | "twitter_username": null 115 | }, 116 | { 117 | "name": "Riccardo Persello", 118 | "source": "https://github.com/persello/ssc23", 119 | "video": "", 120 | "frameworks": [ 121 | "Accelerate", 122 | "AVFoundation", 123 | "SwiftUI", 124 | "Vision" 125 | ], 126 | "status": "submitted", 127 | "github_username": "persello", 128 | "twitter_username": null 129 | }, 130 | { 131 | "name": "Rithul Kamesh", 132 | "source": "https://github.com/rithulkamesh/fitness", 133 | "video": "", 134 | "frameworks": [ 135 | "SwiftUI" 136 | ], 137 | "status": "submitted", 138 | "github_username": "rithulkamesh", 139 | "twitter_username": null 140 | }, 141 | { 142 | "name": "Yanan Li", 143 | "source": "", 144 | "video": "https://youtu.be/2CStbcJK0qM", 145 | "frameworks": [ 146 | "SwiftUI", 147 | "Swift Charts" 148 | ], 149 | "status": "submitted", 150 | "github_username": null, 151 | "twitter_username": null 152 | }, 153 | { 154 | "name": "Yi Cao", 155 | "source": "https://github.com/xiaoyu2006/IFS", 156 | "video": "", 157 | "frameworks": [ 158 | "SwiftUI", 159 | "UIKit" 160 | ], 161 | "status": "rejected", 162 | "github_username": "xiaoyu2006", 163 | "twitter_username": null 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /ru/tutorials/pay-for-apple-developer-account-from-ru.md: -------------------------------------------------------------------------------- 1 | В статье разберем как оплатить аккаунт, если вы в РФ. Перед тем как оплачивать, важное: 2 | 3 | > Если Apple ID зарегистрирован в РФ-регионе, то платное соглашение недоступно 4 | 5 | Это значит вы не сможете создавать покупки-подписки и продавать цифровые товары. Если аккаунт зарегистрирован до санкций, то в нем будет платное соглашение. 6 | 7 | Для РФ-региона доступна оплата по внешней ссылке. Мы написали туториал как получить это разрешение и платить комиссию: 8 | 9 | [Механизм внешних покупок по ссылке в StoreKit](https://sparrowcode.io/ru/tutorials/storekit-external-purchase-link-entitlement-ru): Инструкция как добавить StoreKit External Purchase Link Entitlement в приложение в России. 10 | 11 | Если вам нужно платное соглашение, то нужно сделать учетку для другого региона. В статье разберем и такие варианты. 12 | 13 | # Оплатить через мобильного оператора 14 | 15 | Тратите баланс тарифного плана. Этот способ работает только через приложение [Developer](https://apps.apple.com/us/app/apple-developer/id640199958). В будущем продлевать аккаунт придется тоже отсюда: 16 | 17 | ![Покупка Apple Developer Program в приложении Developer](https://cdn.sparrowcode.io/tutorials/pay-for-apple-developer-account-from-ru/payment.png?v=2) 18 | 19 | ## Российский Билайн и МТС 20 | 21 | Привяжите в способах оплаты App Store сим-карту. Работают Билайн и МТС. Оставьте мобильный баланс как единственный способ оплаты, остальные способы — удалить. 22 | 23 | > Проверьте лимиты на оплату у оператора. Для этого обратитесь в службу поддержки 24 | 25 | Если оплата не пройдет по лимиту, кнопка Enroll может погаснуть и придется решать через службу поддержки Apple. 26 | 27 | Если оплата прошла, обычно аккаунт активируется сразу. Но может занять 3 дня. Если аккаунт не активировали, напишите на почту *eurodev@apple.com*, прикрепите скрин оплаты и дату когда платили. 28 | 29 | У вас могут попросить подтвердить регион — запросить прописку или квитанцию за коммунальные. Происходит не часто, но бывает. 30 | 31 | ## Казахстанский Билайн 32 | 33 | Получить Казахстанскую сим-карту не проблема, продаются на Авито. Пополнить сим-карту можно через Сбербанк или посредников. 34 | 35 | Регистрируете Apple ID в регионе Казазхстан и оплачиваете через приложение [Developer](https://apps.apple.com/us/app/apple-developer/id640199958). Так как регион не под санкциями, в аккаунте будут платные соглашения. 36 | 37 | > Если регистрируете учетку в Казахстане, то оплата [по внешней ссылке](https://sparrowcode.io/ru/tutorials/storekit-external-purchase-link-entitlement-ru) недоступна 38 | 39 | Получать выплаты от Apple можно на любой банк, у которого работают Swift-переводы. Например в РФ это Райфайзен, Юникредит, Москоммерц. Подойдут и банки не в РФ — Apple не валидирует владельца счета, можно указать реквизиты друга. 40 | 41 | ## Кнопка Enroll недоступна 42 | 43 | Если в приложении кнопка Enroll недоступна, свяжитесь с поддержкой через *eurodev@apple.com*: 44 | 45 | ![Кнопка Enroll недоступна в приложении Developer](https://cdn.sparrowcode.io/tutorials/pay-for-apple-developer-account-from-ru/enroll-disabled.png?v=2) 46 | 47 | Непонятно почему у некоторых кнопка недоступна. Поддержка что-то переключает и кнопка появляется. Отвечают за 2-3 дня. 48 | 49 | Вам активируют кнопку и отправят ссылку со способами оплаты, в том числе и для России. Могут отказать в регистрации в Apple Developer Program, тогда можно сделать новый аккаунт. 50 | 51 | # Оплатить картой 52 | 53 | Apple Developer Program можно оплатить картой Visa и Mastercard даже не на ваше имя. Если вы оплатите картой друга, Apple может попросить Ваш (не друга) паспорт. 54 | 55 | > Не оплачивайте несколько аккаунтов одной и той же картой. Apple блокирует учетки, и уже со второго аккаунта отказывает в регистрации 56 | 57 | Даже если вы оплатите картой учетку в РФ-регионе, платное соглашение всё равно не появится. Чтобы появилось платное соглашение, нужен именно не РФ-регион. 58 | 59 | # Открыть компанию 60 | 61 | Мы открываем под ключ компании в Великобритании. Компания на ваше имя, а в аккаунте разработчика будет доступно платное соглашение. Подробнее здесь: 62 | 63 | [Открыть компанию в UK](https://sparrowcode.io/ru/business/company_registration): Вы сможете добавлять других разработчиков, указывать имя в App Store и публиковать VPN 64 | 65 | С паспортом РФ есть проблемы с получением счета для компании, решается в каждом случае индивидуально. 66 | 67 | > Если регистрируете учетку для Великобритании, то оплату [по внешней ссылке](https://sparrowcode.io/ru/tutorials/storekit-external-purchase-link-entitlement-ru) добавить нельзя 68 | 69 | > Мы консультируем регистрации аккаунтов, по реджектам и покупкам в приложении. Записаться на консультацию [по ссылке](https://sparrowcode.io/ru/business/consultation) 70 | 71 | # Куда принимать выплату 72 | 73 | Вы можете принимать выплаты в любой банк на любое имя. Apple не проверяет имя в аккаунте разработчика и имя счета. Если вы хотите оформить счет на себя в РФ, то вот список банков, которые подойдут: 74 | 75 | - **Спецстройбанк**: 0% за зачисление USD. Хранение 1,6% годовых ежемесячно. Открывают при личном присутствии, разовая комиссия 1500 руб. 76 | - ️**Энерготрансбанк**: 1% за зачисление. Хранение 0,1% в день на сумму свыше 5000 USD. Удаленно можно открыть по биометрии или через платформу Финуслуги 77 | - ️**Челябинвестбанк**: 3% за зачисление, минимум $30. Открывают по биометрии удаленно 78 | - **Интеза**: 0% за зачисление. Открывают при личном присутствии 79 | - **Юникредитбанк**: 0% за зачисление. Если открыть счет в офисе, ставят в очередь из-за тех.проблем и приглашают позже, но открывают быстро по биометрии 80 | - **Москоммерцбанк**: 5% за зачисление USD. Хранение 1%, минимум $100 в сутки. Открывают при личном присутствии 81 | - **Райффайзенбанк RUB**: без комиссии. Не забывайте поставить выплату в рублях в App Store Connect 82 | - **Райффайзенбанк USD**: 50% за зачисление, 50% минимум $1000, макс. $10000. Хранение 0.5%, минимум $10 на остаток $10.000-100.000. Открывают при личном присутствии 83 | 84 | Условия быстро меняются, если у вас информация — [напишите мне](https://t.me/ivanvorobei) 85 | -------------------------------------------------------------------------------- /en/tutorials/uisheetpresentationcontroller.md: -------------------------------------------------------------------------------- 1 | When I was young, I made [package](https://github.com/ivanvorobei/SPStorkController) with similar behavior on snapshots. In iOS 13 Apple introduced updated modal controllers, and with iOS 15 you can control their height: 2 | 3 | [Sheet controller with detents in the middle and at the top.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/header.mov) 4 | 5 | # Quick Start 6 | 7 | To show the default sheet-controller, use the code: 8 | 9 | ```swift 10 | let controller = UIViewController() 11 | if let sheetController = controller.sheetPresentationController { 12 | sheetController.detents = [.medium(), .large()] 13 | } 14 | present(controller, animated: true) 15 | ``` 16 | 17 | This is a regular modal controller that has been added complex behavior. You can wrap the sheet-controller into a navigation controller, add a header and bar buttons. If the project supports previous versions of iOS, wrap the code with `sheetController` in `if #available(iOS 15.0, *) {}`. 18 | 19 | # Detents 20 | 21 | Detent - the height to which the controller aspires. Similar to situations with scroll paging or when the electron is not at its energy level. 22 | 23 | There are two detents available: 24 | - `.medium()` half the size of the screen 25 | - `.large()` repeats the large modal controller. 26 | 27 | If you leave only `.medium()`, the controller will open at half of the screen and will not rise higher. You can't set your own height in pixels, you choose only from the available detents. By default, the controller is shown with the `.large()` detent. 28 | 29 | The available detents are indicated as follows: 30 | 31 | ```swift 32 | sheetController.detents = [.medium(), .large()] 33 | ``` 34 | 35 | If you specify only one detent, you cannot switch between them with a gesture. 36 | 37 | ## Switching between detents by code 38 | 39 | To go from one detent to another, use the code: 40 | 41 | ```swift 42 | sheetController.animateChanges { 43 | sheetController.selectedDetentIdentifier = .medium 44 | } 45 | ``` 46 | 47 | It is possible to call without animation block. It is also possible to switch the detent without being able to change it, to do this, change the available detents: 48 | 49 | ```swift 50 | sheetController.animateChanges { 51 | sheetController.detents = [.large()] 52 | } 53 | ``` 54 | 55 | The controller will switch to `.large()`-detent and will no longer allow the gesture to switch to `.medium()`. 56 | 57 | # Lock Dismiss 58 | 59 | If you want to lock a controller in one detent without being able to close it, set `isModalInPresentation` to `true` for the parent. In the example, the parent is the navigation controller: 60 | 61 | ```swift 62 | navigationController.isModalInPresentation = true 63 | if let sheetController = nav.sheetPresentationController { 64 | sheetController.detents = [.medium()] 65 | sheetController.largestUndimmedDetentIdentifier = .medium 66 | } 67 | ``` 68 | 69 | [Sheet controller with a prohibition to close.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/prevent-dismiss.mov) 70 | 71 | # Content Scrolling 72 | 73 | If `.medium()`-detent is active and the controller content is scrolling, the modal controller will go to `.large()`-detent when scrolling up and the content will stay in place. 74 | 75 | [Standard scroll on the sheet controller.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/scrolling-expands-true.mov) 76 | 77 | To scroll content without changing the detent, specify these parameters: 78 | 79 | ```swift 80 | sheetController.prefersScrollingExpandsWhenScrolledToEdge = false 81 | ``` 82 | 83 | [Scroll on a sheet controller with `prefersScrollingExpandsWhenScrolledToEdge = false`.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/scrolling-expands-false.mov) 84 | 85 | Scrolling up will now work for content scrolling. 86 | 87 | > To go to the big detent, pull the navigation bar. 88 | 89 | # Album orientation 90 | 91 | By default, the sheet-controller in landscape orientation looks like a normal controller. The point is that `.medium()`-detent is not available, and `.large()` is the default mode of the modal controller. But you can add edge indentation. 92 | 93 | ```swift 94 | sheetController.prefersEdgeAttachedInCompactHeight = true 95 | ``` 96 | 97 | This is what it looks like: 98 | 99 | ![Sheet-controller in landscape orientation with edge indentation.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/edge-attached.png) 100 | 101 | To make the controller take the prefered size, set `widthFollowsPreferredContentSizeWhenEdgeAttached` to `true`. 102 | 103 | # Dimmed background 104 | 105 | If the background is dimmed, the buttons behind the modal controller will not be clickable. To allow interaction with the background, you must remove the dimming. Specify the largest detent that doesn't need to be dimmed. Here's the code: 106 | 107 | ```swift 108 | sheetController.largestUndimmedDetentIdentifier = .medium 109 | ``` 110 | 111 | [Sheet controller with disabled dimming for the `.medium` stopper.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/undimmed-detent.mov) 112 | 113 | It is specified that the `.medium` will not dim, but anything larger will. It is possible to remove the dimming for the largest detent as well. 114 | 115 | # Indicator 116 | 117 | To add an indicator on top of the controller, set `.prefersGrabberVisible` to `true`. By default, the indicator is hidden. The indicator has no effect on safe area and layout margins. 118 | 119 | ![Grabber indicator on the sheet-controller.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/grabber.png) 120 | 121 | # Corner Radius 122 | 123 | You can control the edge rounding of the controller. Set a value for `.preferredCornerRadius`. The rounding changes not only for the presented controller, but also for the parent. 124 | 125 | ![Corner radius at the sheet-controller.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/corner-radius.png) 126 | 127 | In the screenshot I set the corner radius to `22`. The radius remains the same for the `.medium` detent. 128 | -------------------------------------------------------------------------------- /ru/tutorials/swift-documentation.md: -------------------------------------------------------------------------------- 1 | Хорошая документация помогает понять как работает код. Какие функции он выполняет и как его использовать. Это важно для больших проектов и библиотек, которые могут использовать другие разработчики. 2 | 3 | Для создания однострочной документации используется три косые черты. Для многострочной используем - /** ... */ 4 | 5 | Для описания используется синтаксис **Markdown**: 6 | 7 | - Абзацы разделяются пустыми строками. 8 | 9 | - Неупорядоченные списки отмечаются символами маркеров -, +, * или • 10 | 11 | - В упорядоченных списках используются цифры, за которыми следует точка. 12 | 13 | - Заголовкам обозначаются # 14 | 15 | - Ссылки обозначаются `[text](https://developer.apple.com/)` 16 | 17 | Первый абзац это всегда поле `summary`, краткое описание. 18 | 19 | ```swift 20 | /// This is your User documentation. 21 | struct User { 22 | let firstName: String 23 | let lastName: String 24 | } 25 | 26 | /// This is your User documentation. 27 | /// A very long one. 28 | struct Person { 29 | let firstName: String 30 | let lastName: String 31 | } 32 | ``` 33 | 34 | ![Summary документация](https://cdn.sparrowcode.io/tutorials/swift-documentation/summary.png) 35 | 36 | Чтобы добавить раздел `overview`, добавляем еще один абзац. Второй абзац, будет относиться к разделу `overview`. 37 | 38 | ```swift 39 | /// This is your User documentation (This is summary). 40 | /// 41 | /// A very long one (This will be shown in the discussion section). 42 | struct Person { 43 | let firstName: String 44 | let lastName: String 45 | } 46 | ``` 47 | 48 | ![Overview документация](https://cdn.sparrowcode.io/tutorials/swift-documentation/overview.png) 49 | 50 | Пример простой документации. Первый абзац это `summary`. Второй абзац попадает в `overview`. Остальное сгруппированною в общий раздел. Обратите внимание на заголовки, списки и добавление ссылки. 51 | 52 | ```swift 53 | /// This is your User documentation. 54 | /// A very long one. 55 | /// 56 | /// # Text 57 | /// It's very easy to make some words **bold** and other words *italic* with Markdown. You can even [link to Apple](https://developer.apple.com/) 58 | /// 59 | /// # Lists 60 | /// Sometimes you want numbered lists: 61 | /// 62 | /// 1. One 63 | /// 2. Two 64 | /// 65 | /// - Dashes work just as well 66 | /// - And if you have sub points, put two spaces before the dash or star: 67 | /// - Like this 68 | /// 69 | /// # Code 70 | ```swift 71 | if (isAwesome){ 72 | return true 73 | } 74 | struct User { 75 | let firstName: String 76 | let lastName: String 77 | } 78 | ``` 79 | 80 | ![Пример документации](https://cdn.sparrowcode.io/tutorials/swift-documentation/example.png) 81 | 82 | Для функции с параметрами добавляем раздел `параметров`. Есть два вида написания параметров. Раздел параметров и отдельные поля параметров. 83 | 84 | ```swift 85 | /// - Parameter firstName: This is first name. 86 | /// - Parameter lastName: This is last name. 87 | struct User { 88 | let firstName: String 89 | let lastName: String 90 | } 91 | 92 | 93 | /// - Parameters: 94 | /// - firstName: This is first name. 95 | /// - lastName: This is last name. 96 | struct User { 97 | let firstName: String 98 | let lastName: String 99 | } 100 | ``` 101 | 102 | ![Parameters документация](https://cdn.sparrowcode.io/tutorials/swift-documentation/parameters.png) 103 | 104 | Для функции с возвращаемым значением добавляем раздел `Returns`, как с параметрами. 105 | 106 | ```swift 107 | /// - Returns: A greeting of the current User. 108 | func greeting(person: User) -> String { 109 | return "Hello \(person.firstName)" 110 | } 111 | ``` 112 | 113 | ![Returns документация](https://cdn.sparrowcode.io/tutorials/swift-documentation/returns.png) 114 | 115 | В поле `Throws` указываем какие ошибки будут выброшена и в каких ситуациях. 116 | 117 | ```swift 118 | /// - Throws: MyError.invalidPerson `if `person` is not known by the caller. 119 | func greeting(person: User) throws -> String { 120 | return "Hello \(person.firstName)" 121 | } 122 | ``` 123 | 124 | ![Throws документация](https://cdn.sparrowcode.io/tutorials/swift-documentation/throws.png) 125 | 126 | Так же можно ссылаться на другие сущности в проекте, используя двойные обратные кавычки 127 | 128 | ```swift 129 | /// A greeting of the current ``User`` 130 | func greeting(person: User) String { 131 | return "Hello \(person.firstName)" 132 | } 133 | ``` 134 | 135 | ![Ссылка на другие сущности](https://cdn.sparrowcode.io/tutorials/swift-documentation/ref-entity.png) 136 | 137 | Чтобы добавить изображение используем `![image](link)` 138 | 139 | ```swift 140 | /// An example of using *images* to display a web image 141 | /// 142 | /// ![image](https://cdn.sparrowcode.io/authors/sparrowcode.jpg) 143 | ``` 144 | 145 | ![Добавляем изображения](https://cdn.sparrowcode.io/tutorials/swift-documentation/image.png) 146 | 147 | Есть еще много полей, которые можно добавить в документацию. Вот [список](https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_markup_formatting_ref/Attention.html#//apple_ref/doc/uid/TP40016497-CH29-SW1): 148 | 149 | ![Дополнительные поля](https://cdn.sparrowcode.io/tutorials/swift-documentation/other-fields.png) 150 | 151 | # Создание документации 152 | 153 | DocC мощный инструмент для создания качественной документации из кода. Он позволяет структурировать информацию, добавлять примеры кода, изображения и диаграммы. Это упрощает понимание и использование проекта или фреймворка: 154 | 155 | - **Автоматическая генерация документации:** DocC автоматически создает документацию на основе комментариев в коде и специальных аннотаций. 156 | 157 | - **Поддержка разных типов контента:** Документация может включать текст, примеры кода, изображения и диаграммы. 158 | 159 | - **Навигация по документации:** Документация имеет удобную структуру, включающую оглавление, навигационные ссылки и поисковую систему. 160 | 161 | Нажмите **⌃** + **⇧** + **⌘** + **D** или **Editor** > **Structure** > **Add documentation**. Xcode сбилдит документацию. 162 | 163 | ![Генерация документации](https://cdn.sparrowcode.io/tutorials/swift-documentation/docc.png) 164 | 165 | Когда добавляете что-то новое, нужно заново сбилдить документацию. После этого информация обновиться в браузере документации. -------------------------------------------------------------------------------- /swift-student-challenge/2024.json: -------------------------------------------------------------------------------- 1 | { 2 | "developers": [ 3 | { 4 | "name": "Kyoya Yamaguchi", 5 | "github_username" : "kyoya1123", 6 | "twitter_username": null, 7 | "source": null, 8 | "video": null, 9 | "frameworks": [], 10 | "status": "accepted" 11 | }, { 12 | "name": "Carlos Mbendera", 13 | "github_username" : "carlosmbe", 14 | "twitter_username": null, 15 | "source": "https://github.com/carlosmbe/Better-Talk", 16 | "video": null, 17 | "frameworks": [], 18 | "status": "accepted" 19 | }, { 20 | "name": "Mercen-Lee", 21 | "github_username" : "Mercen-Lee", 22 | "twitter_username": null, 23 | "source": "https://github.com/Mercen-Lee/App-Pilot", 24 | "video": null, 25 | "frameworks": [], 26 | "status": "accepted" 27 | }, { 28 | "name": "Henri Bredt", 29 | "github_username" : "henribredt", 30 | "twitter_username": null, 31 | "source": "https://github.com/henribredt/Sampler-WWDC24", 32 | "video": null, 33 | "frameworks": [], 34 | "status": "accepted" 35 | }, { 36 | "name": "Vedant", 37 | "github_username" : "vedantapps", 38 | "twitter_username": null, 39 | "source": "https://github.com/vedantapps/MagiCode", 40 | "video": null, 41 | "frameworks": [], 42 | "status": "accepted" 43 | }, { 44 | "name": "Jose Adolfo Talactac", 45 | "github_username" : "devjoseadolfo", 46 | "twitter_username": null, 47 | "source": "https://github.com/devjoseadolfo/PowerGrid", 48 | "video": null, 49 | "frameworks": [], 50 | "status": "accepted" 51 | }, { 52 | "name": "Raissa Parente", 53 | "github_username" : "raissaparente", 54 | "twitter_username": null, 55 | "source": "https://github.com/raissaparente/Grannys-Recipebook-WWDC", 56 | "video": null, 57 | "frameworks": [], 58 | "status": "accepted" 59 | }, { 60 | "name": "Rivian Pratama", 61 | "github_username" : "rivianpratama", 62 | "twitter_username": null, 63 | "source": "https://github.com/rivianpratama/WWDC24_MyopiaSim", 64 | "video": "https://www.youtube.com/watch?v=sHBY8pKAU_g&feature=youtu.be", 65 | "frameworks": [], 66 | "status": "accepted" 67 | }, { 68 | "name": "Syuan-Yu Chen", 69 | "github_username" : "dongdong867", 70 | "twitter_username": null, 71 | "source": "https://github.com/dongdong867/SimPOS", 72 | "video": null, 73 | "frameworks": [], 74 | "status": "accepted" 75 | }, { 76 | "name": "Raphael Kitahara", 77 | "github_username" : "raphaelfk", 78 | "twitter_username": "raphadevelops", 79 | "source": "https://github.com/raphaelfk/wwdc24", 80 | "video": null, 81 | "frameworks": [], 82 | "status": "accepted" 83 | }, { 84 | "name": "Masakaz Ozaki", 85 | "github_username" : "masakazozaki", 86 | "twitter_username": "masakazozaki", 87 | "source": "https://github.com/masakazozaki/LookThatWay", 88 | "video": null, 89 | "frameworks": [], 90 | "status": "accepted" 91 | }, { 92 | "name": "Timo", 93 | "github_username" : "omit2c", 94 | "twitter_username": "timo_e002", 95 | "source": "https://github.com/omit2c/GrowHub-SSC-24", 96 | "video": null, 97 | "frameworks": [], 98 | "status": "accepted" 99 | }, { 100 | "name": "Shaurya Gupta", 101 | "github_username" : "Shaurya50211", 102 | "twitter_username": "shaurya50211", 103 | "source": "https://github.com/Shaurya50211/Fizzix", 104 | "video": "https://youtu.be/xjSNIMTSfcA?si=qVB0xrdnexIC31UR", 105 | "frameworks": [], 106 | "status": "accepted" 107 | }, { 108 | "name": "Keitaro Kawahara", 109 | "github_username" : "Keitaro0226", 110 | "twitter_username": "harii_226", 111 | "source": null, 112 | "video": null, 113 | "frameworks": [], 114 | "status": "accepted" 115 | }, { 116 | "name": "Kaijun Zhu", 117 | "github_username" : "Heyya-x", 118 | "twitter_username": "kaijunzhu_", 119 | "source": null, 120 | "video": null, 121 | "frameworks": [], 122 | "status": "accepted" 123 | }, { 124 | "name": "Matteo Zappia", 125 | "github_username" : "matteozappia", 126 | "twitter_username": "aboutzeph", 127 | "source": null, 128 | "video": null, 129 | "frameworks": [], 130 | "status": "accepted" 131 | }, { 132 | "name": "Pranav Karthik", 133 | "github_username" : "pranavkarthik10", 134 | "twitter_username": "pranavkarthik__", 135 | "source": null, 136 | "video": null, 137 | "frameworks": [], 138 | "status": "accepted" 139 | }, { 140 | "name": "Roscoe Rubin-Rottenberg", 141 | "github_username" : "knotbin", 142 | "twitter_username": "knotbin", 143 | "source": null, 144 | "video": null, 145 | "frameworks": [], 146 | "status": "accepted" 147 | }, { 148 | "name": "Till Brügmann", 149 | "github_username" : "stoobit", 150 | "twitter_username": "stoobitofficial", 151 | "source": "https://github.com/stoobit/Vitality-Pro/tree/main", 152 | "video": null, 153 | "frameworks": ["SwiftUI", "CoreML", "AVFoundation", "Vision"], 154 | "status": "accepted" 155 | }, { 156 | "name": "Nadya Tyandra", 157 | "github_username" : "nadyatyandra", 158 | "twitter_username": null, 159 | "source": "https://github.com/nadyatyandra/CircuitCraze", 160 | "video": "https://youtu.be/6zm5z_AhVS4?si=NEyDWfCvwy_arx2O", 161 | "frameworks": ["SwiftUI", "AVFoundation"], 162 | "status": "accepted" 163 | } 164 | ] 165 | } 166 | -------------------------------------------------------------------------------- /ru/tutorials/uisheetpresentationcontroller.md: -------------------------------------------------------------------------------- 1 | ![Сравнение кастового контроллера с `UISheetPresentationController`.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/preview.png) 2 | 3 | Когда я был молодым, то сделал [либу](https://github.com/ivanvorobei/SPStorkController) с похожим поведением на снепшотах. В iOS 13 Apple представила обновленные модальные контроллеры, а с iOS 15 можно управлять их высотой: 4 | 5 | [Sheet-контроллер со стопорами посередине и сверху.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/header.mov) 6 | 7 | # Быстрый старт 8 | 9 | Чтобы показать дефолтный sheet-controller, используйте код: 10 | 11 | ```swift 12 | let controller = UIViewController() 13 | if let sheetController = controller.sheetPresentationController { 14 | sheetController.detents = [.medium(), .large()] 15 | } 16 | present(controller, animated: true) 17 | ``` 18 | 19 | Это обычный модальный контроллер, которому добавили сложное поведение. Sheet-контроллер можно оборачивать в навигационный контроллер, добавлять заголовок и бар-кнопки. Если проект поддерживает предыдущие версии iOS, оберните код с `sheetController` в `if #available(iOS 15.0, *) {}`. 20 | 21 | # Cтопоры (Detents) 22 | 23 | Стопор — высота, к которой стремится контроллер. Похоже на ситуации с пейджингом скролла или когда электрон не на своём энергетическом уровне. 24 | 25 | Доступно два стопора: 26 | - `.medium()` с размером на половину экрана 27 | - `.large()` повторяет большой модальный контроллер. 28 | 29 | Если оставить только `.medium()`, то контроллер откроется на половину экрана и подниматься выше не будет. Установить свою высоту в пикселях нельзя, выбираем только из доступных стопоров. По умолчанию контроллер показывается со стопором `.large()`. 30 | 31 | Доступные стопоры указываются так: 32 | 33 | ```swift 34 | sheetController.detents = [.medium(), .large()] 35 | ``` 36 | 37 | Если укажите только один стопор, то переключиться между ними жестом не получится. 38 | 39 | ## Переключение между стопорами кодом 40 | 41 | Чтобы перейти из одного стопора в другой, используйте код: 42 | 43 | ```swift 44 | sheetController.animateChanges { 45 | sheetController.selectedDetentIdentifier = .medium 46 | } 47 | ``` 48 | 49 | Можно вызывать без блока анимации. Ещё можно переключать стопор без возможности изменять его, для этого меняем доступные стопоры: 50 | 51 | ```swift 52 | sheetController.animateChanges { 53 | sheetController.detents = [.large()] 54 | } 55 | ``` 56 | 57 | Контроллер переключиться в `.large()`-стопор и больше не даст переключиться жестом в `.medium()`. 58 | 59 | # Заблокировать Dismiss 60 | 61 | Если вы хотите зафиксировать контроллер в одном стопоре без возможности закрыть его, установите `isModalInPresentation` в `true` родителю. В примере родитель это навигационный контроллер: 62 | 63 | ```swift 64 | navigationController.isModalInPresentation = true 65 | if let sheetController = nav.sheetPresentationController { 66 | sheetController.detents = [.medium()] 67 | sheetController.largestUndimmedDetentIdentifier = .medium 68 | } 69 | ``` 70 | 71 | [Sheet-контроллер с запретом на закрытие.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/prevent-dismiss.mov) 72 | 73 | # Скроллинг контента 74 | 75 | Если активен `.medium()`-стопор и контент контроллера скролится, то при скролле вверх модальный контроллер перейдёт в `.large()`-стопор, а контент останется на месте. 76 | 77 | [Стандартный скролл на sheet-контроллере.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/scrolling-expands-true.mov) 78 | 79 | Чтобы скролить контент без изменения стопора, укажите такие параметры: 80 | 81 | ```swift 82 | sheetController.prefersScrollingExpandsWhenScrolledToEdge = false 83 | ``` 84 | 85 | [Скролл на sheet-контроллере с `prefersScrollingExpandsWhenScrolledToEdge = false`.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/scrolling-expands-false.mov) 86 | 87 | Теперь при скролле вверх будет отрабатываться скролл контента. 88 | 89 | > Чтобы перейти в большой стопор, потяните за navigation-бар. 90 | 91 | # Альбомная ориентация 92 | 93 | По умолчанию sheet-контроллер в альбомной ориентации выглядит как обычный контроллер. Дело в том, что `.medium()`-стопор недоступен, а `.large()` — дефолтный режим модального контроллера. Но можно добавить отступы по краям. 94 | 95 | ```swift 96 | sheetController.prefersEdgeAttachedInCompactHeight = true 97 | ``` 98 | 99 | Вот как это выглядит: 100 | 101 | ![Sheet-контроллер в альбомной ориентации с отступами по краям.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/edge-attached.png) 102 | 103 | Чтобы контроллер учитывал prefered-размер, установите `widthFollowsPreferredContentSizeWhenEdgeAttached` в `true`. 104 | 105 | # Затемнить фон 106 | 107 | Если фон затемнён, кнопки за модальным контроллером будут не кликабельные. Чтобы разрешить взаимодействие с фоном, нужно убрать затемнение. Укажите самый большой стопор, который не нужно затемнять. Вот код: 108 | 109 | ```swift 110 | sheetController.largestUndimmedDetentIdentifier = .medium 111 | ``` 112 | 113 | [Sheet-контроллер с отключенным затемнением для `.medium` стопора.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/undimmed-detent.mov) 114 | 115 | Указано, что `.medium` затемняться не будет, а всё, что больше - будет. Можно убрать затемнение и для самого большого стопора. 116 | 117 | # Индикатор 118 | 119 | Чтобы добавить индикатор вверху контроллера, установите `.prefersGrabberVisible` в `true`. По умолчанию индикатор спрятан. Индикатор не влияет на safe area и layout margins. 120 | 121 | ![Grabber-индикатора на sheet-контроллере.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/grabber.png) 122 | 123 | # Corner Radius 124 | 125 | Можно управлять закруглением краёв у контроллера. Установите значение для `.preferredCornerRadius`. Закругление меняется не только у презентуемого контроллера, но и у родителя. 126 | 127 | ![Corner-радиус у sheet-контроллера.](https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/corner-radius.png) 128 | 129 | На скриншоте я установил corner-радиус в `22`. Радиус сохраняется и для `.medium`-стопора. 130 | 131 | На этом всё. Напишите в [комментариях к посту](https://t.me/sparrowcode/71), будете ли использовать в своих проектах sheet-контроллеры. 132 | -------------------------------------------------------------------------------- /ru/tutorials/uiviewcontroller-lifecycle.md: -------------------------------------------------------------------------------- 1 | Класс контроллера содержит `view`. Вы добавляете свои вью именно на эту корневую вью контроллера. Чтобы понять жизненный цикл, нужно знать, что: 2 | 3 | > `View` не создается с инициализацией контроллера. 4 | 5 | Контроллеру нужна причина, чтобы создать объект `view`. Концепция жизненного цикла строится вокруг этой особенности. Просто держите в уме, что `view` контроллера создаётся не сразу, а по необходимости. 6 | 7 | ![Про жизненный цикл `UIViewController`](https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/hello.jpg) 8 | 9 | # Инициализируем UIViewController 10 | 11 | Рассмотрим `UIViewController`. Доступно два инициализатора: 12 | 13 | ```swift 14 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 15 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | } 21 | ``` 22 | 23 | Ещё есть инициализатор без параметров `init()`, но это обёртка над первым инициализатором. 24 | 25 | На этом этапе контроллер инициализирует проперти и отрабатывает тело инициализатора. View не загружается, аутлеты не активны. В инициализаторе с nib сохраняется только имя файла, а сам файл не подгружается. 26 | 27 | # Загружаем View 28 | 29 | Когда разработчик презентует контроллер, для системы это причина загрузить view. В контроллере есть методы жизненного цикла, с помощью которых мы следим за процессом и добавляем свою логику. 30 | 31 | ```swift 32 | override func loadView() {} 33 | ``` 34 | 35 | Метод `loadView()` вызывается системой. Его не нужно вызывать вручную. Но можно переопределить, чтобы подменить корневую view. Если нужно загрузить view вручную (и вы уверены, что это нужно), то держите красную кнопку `loadViewIfNeeded()`. Флаг `isViewLoaded` показывает загружена view или нет. 36 | 37 | Второй метод легендарен, как Стив Джобс. Он вызывается, когда view закончила загрузку. 38 | 39 | ```swift 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | } 43 | ``` 44 | 45 | Разработчики не просто так настраивают контроллер и view-хи в методе `viewDidLoad()`. До вызова этого метода корневой view не существует, а после - контроллер готов появиться на экране. Во `viewDidLoad()` память под view выделена, view загружена и готова к настройкам. 46 | 47 | > View нельзя настраивать в инициализаторе: если вызывать `controller.view` - она загрузится. Но контроллер сейчас не виден, а может быть вообще никогда не покажется. Зря потратите память и займете главный поток. 48 | 49 | Проект от такого не развалится, но элементы интерфейса расходуют память — не нужно тратить её раньше, чем нужно. Делайте это по необходимости. 50 | 51 | Раньше я делал проперти-вьюхи контроллера так: 52 | 53 | ```swift 54 | class ViewController: UIViewController { 55 | 56 | let redView = UIView() 57 | } 58 | ``` 59 | 60 | Но когда подготавливал статью, понял ошибку. Проперти инициализируется вместе с контроллером, а значит, память для view выделится сразу. Правильно отложить это до требования, пометьте проперти как `lazy`. 61 | 62 | В методе `viewDidLoad()` размеры view-х неверные - привязываться к высоте и ширине нельзя. Делайте настройку, которая не зависит от размеров. 63 | 64 | Есть метод `viewDidUnload()`. Корневая view может выгружаться из памяти, а это означает кое-что невероятное! 65 | 66 | > Метод `viewDidLoad()` может вызываться несколько раз. 67 | 68 | Если модальный контроллер закрыть, view выгрузится из памяти, но контроллер будет жив. Аутлеты здесь активны, но уже не имеют смысла — их можно ресетить. Если показать контроллер ещё раз, view загрузится снова. Если система выгрузила view, значит, у неё была причина. Не нужно обращаться к корневой view в этом методе — это загрузит view. 69 | 70 | В вашем проекте ничего не сломается, `viewDidLoad()` несколько раз вызывается редко. Разделите настройку данных и view-х в следующем проекте. 71 | 72 | # Показываем и прячем View 73 | 74 | Появление контроллера начинается с метода `viewWillAppear`: 75 | 76 | ```swift 77 | override func viewWillAppear(_ animated: Bool) { 78 | super.viewWillAppear(animated) 79 | } 80 | 81 | override func viewDidAppear(_ animated: Bool) { 82 | super.viewDidAppear(animated) 83 | } 84 | ``` 85 | 86 | Появление контроллера в модальном окне или переход в `UINavigationController`-e вызовут `viewWillAppear` до анимации, а `viewDidAppear` — после. При вызове `viewWillAppear` view уже находится в иерархии. 87 | 88 | Оба метода в связке. Тут делать настройку не нужно, но можно спрятать или показать view-хи, или добавить несложное поведение. В методе `viewDidAppear()` начинайте сетевой запрос или крутите индикатор загрузки. Оба метода могут вызываться несколько раз. 89 | 90 | Есть методы, которые сообщают, что view пропадает с экрана. Вот схема: 91 | 92 | ![Схема жизненного цикла `ViewController`.](https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/header.jpg) 93 | 94 | Обратите внимание на пару антагонистов `viewWillDisappear()` и `viewDidDisappear()`. Они вызываются, когда view удаляется из иерархии представлений. Если вы показываете другой контроллер поверх, то методы не вызываются. 95 | 96 | # Layout 97 | 98 | Методы лейаута привязаны к жизненному циклу view. Доступно 3 метода: 99 | 100 | ```swift 101 | override func viewWillLayoutSubviews() { 102 | super.viewWillLayoutSubviews() 103 | } 104 | 105 | override func viewDidLayoutSubviews() { 106 | super.viewDidLayoutSubviews() 107 | } 108 | ``` 109 | 110 | Первый метод вызывается до `layoutSubviews()` корневой view, второй - после. Во втором методе размеры корректные, а view размещены правильно — можно подвязываться к размерам корневой view. 111 | 112 | Есть отдельный метод про изменение размеров view. Он вызывается и для поворота устройства: 113 | 114 | ```swift 115 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 116 | super.viewWillTransition(to: size, with: coordinator) 117 | } 118 | ``` 119 | 120 | После него вызываются методы `viewWillLayoutSubviews()` и `viewDidLayoutSubviews()`. 121 | 122 | # Кончилась память 123 | 124 | Если вы не очистите объекты, из-за которых это происходит, iOS принудительно крашнет приложение. Этот метод - предупреждение, у вас есть шанс освободить немного памяти. 125 | 126 | ```swift 127 | override func didReceiveMemoryWarning() { 128 | super.didReceiveMemoryWarning() 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /en/tutorials/uiviewcontroller-lifecycle.md: -------------------------------------------------------------------------------- 1 | The controller class contains a `view`. You add your views exactly to this controller root view. To understand the lifecycle, you need to know that: 2 | 3 | > `View` is not created with controller initialization. 4 | 5 | The controller needs a reason to create the `view` object. The lifecycle concept is built around this feature. Keep in mind that the controller's `view` is not created immediately, but as needed. 6 | 7 | ![About lifecycle of `UIViewController`](https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/hello.jpg) 8 | 9 | # Initializing the UIViewController 10 | 11 | Consider the `UIViewController`. Two initializers are available: 12 | 13 | ```swift 14 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 15 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | } 21 | ``` 22 | 23 | There is also an initializer without parameters `init()`, but this is a wrapper over the first initializer. 24 | 25 | At this point, the controller initializes the property and fills the initializer body. View is not loaded, outlets are not active. Only file name is saved in initializer with nib, but file itself is not loaded. 26 | 27 | # Loading View 28 | 29 | When a developer presents a controller, it is a reason for the system to load a view. The controller has lifecycle methods with which we monitor the process and add our logic. 30 | 31 | ```swift 32 | override func loadView() {} 33 | ``` 34 | 35 | The `loadView()` method is called by the system. It doesn't need to be called manually. But you can override it to override the root view. If you need to load the view manually (and you're sure you need to), hold the red `loadViewIfNeeded()` button. The `isViewLoaded` flag shows whether the view is loaded or not. 36 | 37 | The second method is called when the view has finished loading. 38 | 39 | ```swift 40 | override viewDidLoad() { 41 | super.viewDidLoad() 42 | } 43 | ``` 44 | 45 | Developers don't just set up the controller and view-his in the `viewDidLoad()` method. Before this method is called, the root view doesn't exist, and after, the controller is ready to appear on the screen. In `viewDidLoad()` the memory for the view is allocated, the view is loaded and ready to be configured. 46 | 47 | > View cannot be configured in the initializer: if you call `controller.view` - it will load. But the controller is not visible now, and maybe it will never show up at all. You will waste memory and occupy the main thread. 48 | 49 | This will not destroy the project, but the interface elements consume memory - you don't want to waste them before they are needed. Do it as needed. 50 | 51 | I used to make the controller's proprietary views this way: 52 | 53 | ```swift 54 | class ViewController: UIViewController { 55 | 56 | let redView = UIView() 57 | } 58 | ``` 59 | 60 | But when I was preparing the article I realized my mistake. The property is initialized together with the controller, which means the memory for the view will be allocated immediately. The right thing to do is to defer this to the requirement, mark the property as `lazy`. 61 | 62 | In the `viewDidLoad()` method, the view dimensions are wrong - you can't bind to height and width. Make a setting that doesn't depend on dimensions. 63 | 64 | There is a method `viewDidUnload()`. The root view can unload from memory, which means something incredible! 65 | 66 | > The `viewDidLoad()` method can be called several times. 67 | 68 | If the modal controller is closed, the view is unloaded from memory, but the controller is alive. Outlets are active here, but no longer meaningful - they can be reset. If you show the controller again, the view will load again. If the system unloaded the view, then it must have had a reason. You don't need to refer to the root view in this method - it will load the view. 69 | 70 | Nothing will break in your project, `viewDidLoad()` is rarely called multiple times. Separate the data and view setup in the next project. 71 | 72 | # Show and Hide View 73 | 74 | The appearance of the controller starts with the `viewWillAppear` method: 75 | 76 | ```swift 77 | override func viewWillAppear(_ animated: Bool) { 78 | super.viewWillAppear(animated) 79 | } 80 | 81 | override func viewDidAppear(_ animated: Bool) { 82 | super.viewDidAppear(animated) 83 | } 84 | ``` 85 | 86 | The appearance of the controller in the modal window or the transition in `UINavigationController`-e will call `viewWillAppear` before the animation and `viewDidAppear` after it. When `viewWillAppear` is called, the view is already in the hierarchy. 87 | 88 | Both methods are bundled. You don't need to do any customization here, but you can hide or show view-highs, or add uncomplicated behavior. In the `viewDidAppear()` method, start a network request or spin the load indicator. Both methods can be called multiple times. 89 | 90 | There are methods that report that the view disappears from the screen. Here's a schematic: 91 | 92 | ![Lifecycle scheme of the `ViewController`.](https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/header-en.jpg) 93 | 94 | Note the pair of antagonists `viewWillDisappear()` and `viewDidDisappear()`. They are called when the view is removed from the view hierarchy. If you show another controller on top, the methods are not called. 95 | 96 | # Layout 97 | 98 | Layout methods are tied to the view lifecycle. Three methods are available: 99 | 100 | ```swift 101 | override func viewWillLayoutSubviews() { 102 | super.viewWillLayoutSubviews() 103 | } 104 | 105 | override func viewDidLayoutSubviews() { 106 | super.viewDidLayoutSubviews() 107 | } 108 | ``` 109 | 110 | The first method is called before `layoutSubviews()` of the root view, the second method is called after. In the second method, the dimensions are correct and the view is placed correctly - you can link to the dimensions of the root view. 111 | 112 | There is a separate method for resizing the view. It is also called to rotate the device: 113 | 114 | ```swift 115 | override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { 116 | super.viewWillTransition(to: size, with: coordinator) 117 | } 118 | ``` 119 | 120 | The `viewWillLayoutSubviews()` and `viewDidLayoutSubviews()` methods are called after it. 121 | 122 | # Memory is out 123 | 124 | If you don't clear the objects that cause it to happen, iOS will forcibly crash the app. This method is a warning, you have a chance to free up some memory. 125 | 126 | ```swift 127 | override func didReceiveMemoryWarning() { 128 | super.didReceiveMemoryWarning() 129 | } 130 | ``` 131 | -------------------------------------------------------------------------------- /ru/tutorials/cert-and-profile-for-personal-developer-account.md: -------------------------------------------------------------------------------- 1 | Вы хотите добавить разработчика в аккаунт, чтобы он мог выгружать приложения. Если у вас аккаунт компании (юр. лицо), то всё работает из коробки. 2 | 3 | Но если у вас индивидуальный аккаунт (физ. лицо), то сторонний разработчик сможет выгружать приложения только со специальным профайлом. 4 | 5 | > Передавать логин-пароль от вашего Apple ID небезопасно, не делайте так 6 | 7 | Сертификаты можно сделать вручную или через API. В этой статье разберем ручной способ. 8 | 9 | По шагам, что будем делать: 10 | - Сначала запрос на подпись для сертификата 11 | - Создадим сам сертификат 12 | - Объединим этот сертификат с ключом 13 | - Регистрируем приложение (возможно, оно у вас уже зарегано) 14 | - Делаем профайл на основе сертификата — именно он нужен, чтобы выгружать приложения 15 | 16 | # Запрос сертификата 17 | 18 | Делаем специальный запрос на сертификат — это файл с расширением `.certSigningRequest`. 19 | 20 | Открываем *Keychain Access* и создаём файл `CertificateSigningRequest.certSigningRequest`: 21 | 22 | ![Запрос в центре сертификации](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/keychain-request.png) 23 | 24 | Вводим почту, имя и выбираем *Saved to disk*. В следующем окне просто сохраните файл: 25 | 26 | ![Сохраняем запрос на сертификат](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/keychain-sert-info.png?v=2) 27 | 28 | У вас появится файл, он ещё пригодится: 29 | 30 | ![Готовый файл `.certSigningRequest`](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/keychain-sert-created.png?v=2) 31 | 32 | > Если у владельца акаунта нет macOS, то запрос-файл делает разработчик и отправляет владельцу аккаунта 33 | 34 | # Делаем сертификат 35 | 36 | Сертификат подтверждает, что приложение именно ваше. Расширение у файла-сертификата — `.cer`. 37 | 38 | Откройте в *Developer Account* вкладку сертификаты: 39 | 40 | ![Вкладка с сертификатами](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/main-sert.png) 41 | 42 | Чтобы сделать новый сертификат, жмите плюс: 43 | 44 | ![Добавляем сертификат](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/add-sert.png) 45 | 46 | Выбираем *Apple Distribution* и жмем *Continue*: 47 | 48 | ![Apple Distribution](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/new-sert.png) 49 | 50 | На этой странице попросит файл-запрос на сертификат `.certSigningRequest`, который мы сделали выше. Выбирайте файл: 51 | 52 | ![Добавляем `.certSigningRequest`](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/select-new-sert.png) 53 | 54 | Сертификат готов — скачайте его, он ещё пригодится: 55 | 56 | ![Скачиваем сертификат](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/download-sert.png) 57 | 58 | # Объединяем сертификат и ключ 59 | 60 | Дальше нужен файл с расширением `.p12`. Он хранит связку сертификат-ключ. 61 | 62 | Кликните два раза по файлу `distribution.cer`, и он откроется *Keychain Access*. 63 | 64 | > Если ничего не происходит, просто найдите последний загруженный сертификат *Apple Distribution* по дате. Дата истечения будет через год 65 | 66 | ![Apple Distribution сертификат](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/distribution-sert.png) 67 | 68 | Разверните выпадайку (слева от сертификата), выделите сертификат и приватный ключ. Дальше нажмите правую кнопку и выберите `Export 2 items...`. 69 | 70 | ![Экспортируем сертификат с ключом](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/export-distribution-sert.png) 71 | 72 | Сохраняем файл: 73 | 74 | ![Имя для сертификата](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/create-sert-p12.png) 75 | 76 | Ставим пароль сертификату, можно оставить пустым: 77 | 78 | ![Пароль для сертификата](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/sert-p12-non-pass.png) 79 | 80 | Тут попросит пароль от вашего мака — введите и нажмите *Always Allow*: 81 | 82 | ![Вводим пароль от вашего мака](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/sert-p12-system-pass.png) 83 | 84 | Получим файл `Certificates.p12`: 85 | 86 | ![Сертификат `.p12`](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/save-sert-p12.png) 87 | 88 | # Регистрируем приложение 89 | 90 | > Если у вас уже есть приложение, этот шаг пропускаем 91 | 92 | `App ID` это уникальный идентификатор приложения. Он связывает приложения с сервисами Apple, такими как Push Notifications, iCloud, Game Center и др. 93 | 94 | Идем в *Developer Account* во вкладку *Identifiers* и жмем плюс: 95 | 96 | ![Вкладка Identifiers](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/identifiers.png) 97 | 98 | Выбираем *App IDs*, далее *App*: 99 | 100 | ![App IDs и App](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/register-identifier-app-id.png) 101 | 102 | Здесь в *Description* введите название приложения, а в *Bundle ID* бандл. `Explicit` — используется для подписи только одного приложения. `Wildcard` — используется для подписи нескольких приложений. 103 | 104 | > Подробнее про Explicit и Wildcard [по ссылке](https://developer.apple.com/library/archive/qa/qa1713/_index.html): 105 | 106 | ![Регистрация App ID](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/register-app-id.png) 107 | 108 | Когда заполнили поля, жмём *Register*: 109 | 110 | > Если получили ошибку проверьте поле Bundle ID 111 | 112 | ![Регистрируем App ID](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/end-register-app-id.png) 113 | 114 | На странице *Identifiers* появится идентификатор нового приложения: 115 | 116 | ![Идентификатор приложения](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/identifiers-list.png) 117 | 118 | # Provisioning Profile 119 | 120 | `Provisioning Profile` связывает всё вместе: Apple Developer Account, App ID, сертификаты и устройства. 121 | 122 | Это файл с расширением `.mobileprovision`. 123 | 124 | Идем во вкладку *Profiles*, жмем кнопку *Generate a profile*: 125 | 126 | ![Вкладка Profiles](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/profiles.png) 127 | 128 | Выбираем *App Store Connect*: 129 | 130 | ![App Store Connect](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/new-profile.png) 131 | 132 | В `App ID` выбираем нужный `Bundle ID` из списка: 133 | 134 | ![Выбираем App ID](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/generate-profile-app-id.png) 135 | 136 | Выбираем недавно созданный сертификат (проверь дату, когда истекает): 137 | 138 | ![Добавляем сертификат](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/generate-profile-select-sert.png) 139 | 140 | Заполните имя *Provisioning Profile Name* и нажмите *Generate*: 141 | 142 | ![Название для Provisioning Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/generate-profile-name.png) 143 | 144 | Осталось скачать файл: 145 | 146 | ![Скачиваем Provisioning Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/download-profile.png) 147 | 148 | Получаем файл с вашим именем и расширением `.mobileprovision`: 149 | 150 | ![Provision Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/created-profile.png) 151 | 152 | # Передаем файлы разработчику 153 | 154 | Передаем разработчику файл `.p12` и `Provision Profile`. Дальше разработчику нужно дважды щелкнуть на файл `.p12` или импортировать в *Keychain Access*: 155 | 156 | ![Импортируем `.p12`](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/add-p12.png) 157 | 158 | Теперь разработчик идет в Xcode-проект — Project Settings и выбирает таргет. На вкладке *Signing & Capabilities* отключаем `Automatically manage signing`, выбираем Team ID и импортируем Provisioning Profile: 159 | 160 | ![Импортируем Provision Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/add-profile-xcode.png) 161 | 162 | Готово! Разработчик сможет выгружать приложения на индивидуальный аккаунт. 163 | 164 | > Инструкцию повторять только если меняется Profile. Для каждого приложения повторять не нужно -------------------------------------------------------------------------------- /en/tutorials/cert-and-profile-for-personal-developer-account.md: -------------------------------------------------------------------------------- 1 | You want to add a developer to the account so that they can upload apps. If you have a company account, everything works out of the box. 2 | 3 | But if you have an individual account, a third-party developer will be able to upload applications only with a special profile. 4 | 5 | > It's not safe to pass your Apple ID username-password, don't do that 6 | 7 | Сертификаты можно сделать вручную или через API. В этой статье разберем ручной способ. 8 | 9 | Step by step what we are going to do: 10 | - First, request a signature for the certificate 11 | - Create the certificate 12 | - Combine this certificate with the key 13 | - Register the app (you may already have it registered). 14 | - Create a profile based on the certificate — it is the one we need to upload app 15 | 16 | # Certificate Request 17 | 18 | We make a special request for a certificate — this is a file with the extension `.certSigningRequest`. 19 | 20 | Open *Keychain Access* and create the file `CertificateSigningRequest.certSigningRequest`: 21 | 22 | ![Inquiry at the certification center](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/keychain-request.png) 23 | 24 | Enter your email, name and select *Saved to disk*. In the next window, just save the file: 25 | 26 | ![Save the certificate request](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/keychain-sert-info.png?v=2) 27 | 28 | You'll have a file, it'll still come in handy: 29 | 30 | ![Ready `.certSigningRequest` file](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/keychain-sert-created.png?v=2) 31 | 32 | > If the account holder doesn't have macOS, the request-file is made by the developer and sent to the account holder 33 | 34 | # Making a Certificate 35 | 36 | The certificate confirms that the app is yours. The extension of the certificate file is `.cer`. 37 | 38 | Open the Certificates tab in *Developer Account*: 39 | 40 | ![Certificate tab](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/main-sert.png) 41 | 42 | To make a new certificate, click the plus sign: 43 | 44 | ![Adding a Certificate](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/add-sert.png) 45 | 46 | Select *Apple Distribution* and click *Continue*: 47 | 48 | ![Apple Distribution](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/new-sert.png) 49 | 50 | This page will ask for the `.certSigningRequest` certificate request file we made above. Select the file: 51 | 52 | ![Add `.certSigningRequest`.](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/select-new-sert.png) 53 | 54 | The certificate is ready — download it, it will still come in handy: 55 | 56 | ![Download the certificate](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/download-sert.png) 57 | 58 | # Merge certificate and key 59 | 60 | Next we need a file with the extension `.p12`. It stores the certificate-key mapping. 61 | 62 | Double-click on the `distribution.cer` file and it will open *Keychain Access*. 63 | 64 | > If nothing happens, just search for the last downloaded *Apple Distribution* certificate by date. The expiration date will be one year from now 65 | 66 | ![Apple Distribution Certificate](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/distribution-sert.png) 67 | 68 | Expand the drop-down box (to the left of the certificate), highlight the certificate and private key. Next, right-click and select `Export 2 items...`. 69 | 70 | ![Export Certificate with key](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/export-distribution-sert.png) 71 | 72 | Save the file: 73 | 74 | ![Name for the Certificate](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/create-sert-p12.png) 75 | 76 | Set a password for the certificate, you can leave it blank: 77 | 78 | ![Password for Certificate](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/sert-p12-non-pass.png) 79 | 80 | It will ask for your mac password - enter it and click *Always Allow*: 81 | 82 | ![Enter your mac's password](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/sert-p12-system-pass.png) 83 | 84 | Get the file `Certificates.p12`: 85 | 86 | ![Certificate `.p12'.](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/save-sert-p12.png) 87 | 88 | # Register the App 89 | 90 | > If you already have an application, skip this step 91 | 92 | The `App ID` is a unique identifier for an app. It links apps to Apple services such as Push Notifications, iCloud, Game Center, etc. 93 | 94 | Go to *Developer Account* under the *Identifiers* tab and click the plus sign: 95 | 96 | ![Identifiers Tab](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/identifiers.png) 97 | 98 | Select *App IDs*, then *App*: 99 | 100 | ![App IDs & App](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/register-identifier-app-id.png) 101 | 102 | Here in *Description* enter the name of the app, and in *Bundle ID* enter the bundle. `Explicit` - used to sign only one application. `Wildcard` - used to sign multiple apps. 103 | 104 | > Learn more about Explicit and Wildcard [at link](https://developer.apple.com/library/archive/qa/qa1713/_index.html): 105 | 106 | ![App ID registration](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/register-app-id.png) 107 | 108 | When you have filled in the fields, click *Register*: 109 | 110 | > If you get an error, check the Bundle ID field 111 | 112 | ![Registering an App ID](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/end-register-app-id.png) 113 | 114 | The *Identifiers* page will display the ID of the new app: 115 | 116 | ![Application Identifier](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/identifiers-list.png) 117 | 118 | # Provisioning Profile 119 | 120 | The `Provisioning Profile' ties everything together: Apple Developer Account, App ID, certificates, and devices. 121 | 122 | This is a file with the extension `.mobileprovision`. 123 | 124 | Go to the *Profiles* tab, click the *Generate a profile* button: 125 | 126 | ![Profiles Tab](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/profiles.png) 127 | 128 | Select *App Store Connect*: 129 | 130 | ![App Store Connect](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/new-profile.png) 131 | 132 | In `App ID` select the desired `Bundle ID` from the list: 133 | 134 | ![Select App ID](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/generate-profile-app-id.png) 135 | 136 | Select the newly created certificate (check the date when it expires): 137 | 138 | ![Adding a certificate](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/generate-profile-select-sert.png) 139 | 140 | Fill in the *Provisioning Profile Name* and click *Generate*: 141 | 142 | ![Name for Provisioning Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/generate-profile-name.png) 143 | 144 | All that's left is to download the file: 145 | 146 | ![Downloading Provisioning Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/download-profile.png) 147 | 148 | We get a file with your name and extension `.mobileprovision`: 149 | 150 | ![Provision Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/created-profile.png) 151 | 152 | # Transfer files to the developer 153 | 154 | Pass the `.p12` file and `Provision Profile` to the developer. Next, the developer needs to double-click the `.p12` file or import it into *Keychain Access*: 155 | 156 | ![Import `.p12`](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/add-p12.png) 157 | 158 | Now the developer goes to Xcode-project - Project Settings and selects the target. On the *Signing & Capabilities* tab disable `Automatically manage signing`, select Team ID and import Provisioning Profile: 159 | 160 | ![Importing a Provision Profile](https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/add-profile-xcode.png) 161 | 162 | Done! The developer will be able to upload apps to an individual account. 163 | 164 | > Repeat the steps only if the Profile is changed. It does not need to be repeated for each app -------------------------------------------------------------------------------- /ru/tutorials/privacy-manifest.md: -------------------------------------------------------------------------------- 1 | Если вы используете User Defaults или собираете данные о пользователе, то вам нужно заполнить манифест. Всё что вы укажите появиться на странице приложения. 2 | 3 | > Авторы библиотеки тоже добавляют манифест. Но если они этого не сделали, то внутри проекта добавляет сам разработчик. 4 | 5 | Если у библиотеки есть манифест, то не нужно дублировать в ваш манифест. Когда архивируете проект, все манифесты объединяются в один. 6 | 7 | # Добавляем Манифест 8 | 9 | Нажмите `⌘+N` и выберите `App Privacy`-файл. 10 | 11 | ![Создаем `App Privacy`-файл](https://cdn.sparrowcode.io/tutorials/privacy-manifest/app-privacy.png?v=2) 12 | 13 | У каждого таргета свой манифест, поэтому внимательно ставьте чекмарк нужному таргету. Если манифест одинаковый для всех таргетов, то можно сразу указать несколько таргетов. 14 | 15 | ![Указываем таргет для манифеста](https://cdn.sparrowcode.io/tutorials/privacy-manifest/enable-target.png?v=2) 16 | 17 | # Структура Манифеста 18 | 19 | Манифест это plist-файл с расширением `.xcprivacy`. 20 | 21 | ![Пример заполненного Privacy Манифеста](https://cdn.sparrowcode.io/tutorials/privacy-manifest/base-app-manifest.png?v=2) 22 | 23 | Манифест состоит из трех полей. Первое про трекинг — его заполняете когда собираете почту или имя. Второе отвечает за системные API, например, User Defaults. Третье отвечает за `IDFA`. 24 | 25 | Разберем каждое поле подробнее. 26 | 27 | ## Трекинг пользователя 28 | 29 | Поле `Privacy Nutrition Label Types` описывает какие данные собираем о пользователе. Все что укажите в манифесте, будет видно в поле App Privacy на странице приложения: 30 | 31 | ![Информация какие данные собираем на странице App Store](https://cdn.sparrowcode.io/tutorials/privacy-manifest/nutrition-label-app-store.png?v=2) 32 | 33 | **Collected Data Type** — это тип данных, которые собираете о пользователе. Например, контакты или информация о платежах. Все типы на [официальном сайте](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4250555), свои добавлять нельзя. В plist-файл добавлять строку из `Data type`. 34 | 35 | ![Типы данных про контакты для Манифеста](https://cdn.sparrowcode.io/tutorials/privacy-manifest/collected-data-type.png?v=2) 36 | 37 | Для каждого типа данных создаете новый Item. Поля ниже нужно указывать для каждого типа данных: 38 | 39 | **Linked to User** — если собираете данные, связанные с личностью пользователя, ставьте `YES`. 40 | 41 | **Used for Tracking** — если ли данные используются для трекинга, ставим `YES`. 42 | 43 | **Collection Purposes** — здесь указываем причины почему собираем данные. Например, аналитика, реклама или аутентификация. Выбирать из доступного [списка причин](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4250556), свои указывать нельзя. 44 | 45 | ![Причины в Манифесте почему собираем данные](https://cdn.sparrowcode.io/tutorials/privacy-manifest/collection-purposes.png?v=2) 46 | 47 | ## Системное API 48 | 49 | Для API отдельное поле `Privacy Accessed API Types`. Как раз по нему прилетает письмо с ошибками от Apple. В этом поле указываем какое API используем и почему. 50 | 51 | ![Тип API и причина его использования](https://cdn.sparrowcode.io/tutorials/privacy-manifest/privacy-accessed-api-reasons.png?v=2) 52 | 53 | Это системные API, которые нужно указывать в манифесте: 54 | 55 | [File Timestamp](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393): Получаете время когда создан или изменен файл 56 | [System Boot Time](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278394): Информация о запуске приложения и времени работы OS 57 | [Disk Space](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278397): Доступное пространство в хранилище устройства 58 | [Active Keyboard](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278400): Доступ к списку активных клавиатур 59 | [User Defaults](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401): Если используете User Defaults 60 | 61 | Для каждого API по ссылке будет и список доступных причин. Свои причины указывать нельзя. 62 | 63 | > Если подходит несколько причин, нужно указывать все 64 | 65 | ## IDFA 66 | 67 | Если используете IDFA, добавьте поле **Privacy Tracking Enabled** и установите `YES`. Сразу добавляйте поле **Privacy Tracking Domains**, здесь нужно указать все домены, которые работают в IDFA. 68 | 69 | ![Поля для IDFA в Манифесте](https://cdn.sparrowcode.io/tutorials/privacy-manifest/tracking-enabled-tracking-domains.png?v=2) 70 | 71 | > Если установили `Privacy Tracking Enabled`, то обязательно указать хотя бы один домен. 72 | 73 | Чтобы получить какие домены используются для IDFA, откройте профайлер `Product` → `Profile`. Теперь в окне выберите Network: 74 | 75 | ![Окно профайлера](https://cdn.sparrowcode.io/tutorials/privacy-manifest/profile-network.png?v=2) 76 | 77 | В левом верхнем углу жмем кнопку Start Recording. Выбираете вкладку **Points of Interest**, здесь будет список всех доменов. В колонке **Start Message** видно домен и указано что его не добавили в манифест. 78 | 79 | ![Как собрать домены IDFA](https://cdn.sparrowcode.io/tutorials/privacy-manifest/points-of-interest.png?v=2) 80 | 81 | Профайл иногда сбоит, если в **Points of Interest** ничего не показывает или вообще пропадает, вот второй способ. Выбираете вкладку вашего приложения, а в сессиях видны все домены. 82 | 83 | ![Все домены в сессиях приложения](https://cdn.sparrowcode.io/tutorials/privacy-manifest/app-sessions.png?v=2) 84 | 85 | Теперь придется проверить каждый или участвует он в IDFA. Сделать придется вам лично. 86 | 87 | # Манифест в библиотеках 88 | 89 | > Авторы библиотеки тоже добавляют манифест. Но если они этого не сделали, то внутри проекта добавляет сам разработчик 90 | 91 | Если автор библиотеки не добавил манифест, то разработчик должен заполнить манифест сам. 92 | 93 | Если в библиотеке есть манифест и он заполнен, то не нужно дублировать информацию в главный манифест. Все манифесты объединяются в один, когда собираем архив. 94 | 95 | Если в манифесте есть ошибки, то разработчику придется самому дополнить манифест внутри проекта. Например, Firebase Сrashlytics использует домен **firebase-settings.crashlytics.com**. В своем манифесте они это не указали: 96 | 97 | ![Ошибка манифесте Firebase](https://cdn.sparrowcode.io/tutorials/privacy-manifest/firebase-manifest.png?v=2) 98 | 99 | Мы это нашли с помощью [профайлера](https://sparrowcode.io/ru/tutorials/privacy-manifest#idfa). В такой ситуации добавляем домен в свой манифест, это перекроет проблемное поле в манифесте от Firebase. 100 | 101 | В манифестах библиотек допускают ошибки — обязательно перепроверяйте. 102 | 103 | # Если ошибка в Манифесте 104 | 105 | > Ошибки придут на почту, только когда отправите приложение на проверку. Если просто выгрузить проект, то ошибок не будет 106 | 107 | На почту придут ошибки только про системное API: 108 | 109 | ![Письмо с ошибками в манифесте](https://cdn.sparrowcode.io/tutorials/privacy-manifest/privacy-manifest-email.png?v=2) 110 | 111 | Чтобы быстро найти ключи, введите в поиске `NS`. Именно их не хватает в вашем Манифесте. Даже если вы не используете это API, его могут использовать библиотеки, которые вы добавили в проект. 112 | 113 | Вот NS ключи, и ссылки на ключ и причину на сайте Apple: 114 | 115 | - [NSPrivacyAccessedAPICategoryFileTimestamp](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393) 116 | - [NSPrivacyAccessedAPICategorySystemBootTime](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278394) 117 | - [NSPrivacyAccessedAPICategoryDiskSpace](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278397) 118 | - [NSPrivacyAccessedAPICategoryActiveKeyboards](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278400) 119 | - [NSPrivacyAccessedAPICategoryUserDefaults](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401) 120 | 121 | # Финальный Манифест 122 | 123 | Собираем архив Product -> Archive. Правой кнопкой по архиву, выбираем Generate Privacy Report. 124 | 125 | ![Экспорт финального манифеста](https://cdn.sparrowcode.io/tutorials/privacy-manifest/generate-privacy-report.png?v=2) 126 | 127 | В экспорте PDF-файл. Все манифесты объединились в итоговый: 128 | 129 | ![PDF отчет со всеми манифестами](https://cdn.sparrowcode.io/tutorials/privacy-manifest/pdf-report.png?v=2) 130 | 131 | Все поля что с расширением `.app`, это из вашего манифеста. Остальные поля это сторонние библиотеки в вашем проекте. -------------------------------------------------------------------------------- /en/tutorials/privacy-manifest.md: -------------------------------------------------------------------------------- 1 | If you use User Defaults or collect user data, you need to fill out a manifest. Everything you specify will appear on the application page. 2 | 3 | > The frameowkr's developers also add a Manifest. But if they didn’t do this, then the developer himself adds it inside the project 4 | 5 | If the framework has a manifest, it doesn't need to be duplicated into your manifest. When you archive a project, all manifests are merged into one. 6 | 7 | # Adding Manifest 8 | 9 | Press `⌘+N` and select `App Privacy`-file. 10 | 11 | ![Create an `App Privacy`-file](https://cdn.sparrowcode.io/tutorials/privacy-manifest/app-privacy.png?v=2) 12 | 13 | Each target has its own Manifest, so be careful to checkmark the right target. If the Manifest is the same for all targets, you can specify several targets at once. 14 | 15 | ![Specifying the target for the Manifest](https://cdn.sparrowcode.io/tutorials/privacy-manifest/enable-target.png?v=2) 16 | 17 | # Structure of Manifest 18 | 19 | The Manifest is a plist-file with the `.xcprivacy` extension. 20 | 21 | ![Example of a completed Privacy Manifest](https://cdn.sparrowcode.io/tutorials/privacy-manifest/base-app-manifest.png?v=2) 22 | 23 | The manifest contains three fields. The first is about tracking — you fill it out when you collect mail or name. The second one for system API, for example, User Defaults. The third one for `IDFA`. 24 | 25 | Let's break down each field in more detail. 26 | 27 | ## User Tracking 28 | 29 | The `Privacy Nutrition Label Types` field describes what data collect about the user. Anything specify in the manifest will be visible in the App Privacy field on the application page: 30 | 31 | ![Information about what data we collect on the App Store page](https://cdn.sparrowcode.io/tutorials/privacy-manifest/nutrition-label-app-store.png?v=2) 32 | 33 | **Collected Data Type** — is the type of data collect about the user. For example, contacts or payment information. All types are on the [official website](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4250555), you cannot add your own. Add a line from `Data type` to the plist-file. 34 | 35 | ![Contact data types for Manifest](https://cdn.sparrowcode.io/tutorials/privacy-manifest/collected-data-type.png?v=2) 36 | 37 | For each data type, create a new Item. The fields below must be specified for each data type: 38 | 39 | **Linked to User** — if you collect data related to the user's identity, put `YES`. 40 | 41 | **Used for Tracking** — if the data is used for tracking, put `YES`. 42 | 43 | **Collection Purposes** — here specify the reasons why are collecting the data. For example, analytics, advertising or authentication. Choose from the available [list of reasons](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4250556), you can't list your own. 44 | 45 | ![Reasons in Manifest why we collect data](https://cdn.sparrowcode.io/tutorials/privacy-manifest/collection-purposes.png?v=2) 46 | 47 | ## System API 48 | 49 | There is `Privacy Accessed API Types` field for APIs. You recive email with error descriptions exactly about this field. Here we specify which API we are using and reason for it. 50 | 51 | ![The type of API and the reason for its use](https://cdn.sparrowcode.io/tutorials/privacy-manifest/privacy-accessed-api-reasons-en.png?v=2) 52 | 53 | These are the system APIs that need to be specified in the Manifest: 54 | 55 | [File Timestamp](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393): Get the time when the file was created or modified 56 | [System Boot Time](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278394): Information about application startup and OS runtime 57 | [Disk Space](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278397): Available storage space on the device 58 | [Active Keyboard](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278400): Access to the list of active keypads 59 | [User Defaults](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401): If used User Defaults 60 | 61 | For each API by link you get a list of availalbe reasons. You can't specify your own reasons. 62 | 63 | > If more than one reason is correct, fill all of them 64 | 65 | ## IDFA 66 | 67 | If you are using IDFA, add the **Privacy Tracking Enabled** field and set `YES`. Add the **Privacy Tracking Domains** field as well, here you need to specify all domains that work in IDFA. 68 | 69 | ![Fields for IDFA in Manifest](https://cdn.sparrowcode.io/tutorials/privacy-manifest/tracking-enabled-tracking-domains.png?v=2) 70 | 71 | > If you set `Privacy Tracking Enabled`, be sure to specify at least one domain. 72 | 73 | To get which domains are used for IDFA, open the `Product` → `Profile` profiler. Now select Network in the window: 74 | 75 | ![Profiler window](https://cdn.sparrowcode.io/tutorials/privacy-manifest/profile-network.png?v=2) 76 | 77 | In the upper left corner, click Start Recording. Select the **Points of Interest** tab, this will list all the domains. The **Start Message** column shows the domain and indicates that it has not been added to the Manifest. 78 | 79 | ![How to collect IDFA domains](https://cdn.sparrowcode.io/tutorials/privacy-manifest/points-of-interest.png?v=2) 80 | 81 | The profile sometimes fails. If **Points of Interest** doesn't show anything or disappears altogether, here's the second way. Select your application tab, and can see all domains in the sessions. 82 | 83 | ![All domains in application sessions](https://cdn.sparrowcode.io/tutorials/privacy-manifest/app-sessions.png?v=2) 84 | 85 | Now you will have to check each domain to see if it participates in IDFA. You will have to do it yourself. 86 | 87 | # Manifest in Frameworks 88 | 89 | > Framework developer adds the manifest too. But if they haven't done so, the developer adds it internally 90 | 91 | If the framework developer has not added a Manifest, you must fill in the Manifest themselves. 92 | 93 | If there is a Manifest in the framework, and it is complete, there is no need to duplicate to your manifest. All Manifests are merged into one when we collect the archive. 94 | 95 | If there are errors in the Manifest, the developer will have to complete the Manifest himself within the project. For example, Firebase Crashlytics uses the domain **firebase-settings.crashlytics.com**. They didn't specify this in their manifest: 96 | 97 | ![Firebase manifest error](https://cdn.sparrowcode.io/tutorials/privacy-manifest/firebase-manifest.png?v=2) 98 | 99 | We found it with the help of a [profiler](https://sparrowcode.io/ru/tutorials/privacy-manifest#idfa). In this case add the domain to your Manifest, this will override the problem field in the Firebase Manifest. 100 | 101 | Framework Manifests make mistakes — be sure to double-check. 102 | 103 | # If the error in Manifest 104 | 105 | > Errors will come to mail only when send the application for review. If you just upload the project, there will be no errors 106 | 107 | Only errors about the system API will come to the mail: 108 | 109 | ![A email with errors in the Manifest](https://cdn.sparrowcode.io/tutorials/privacy-manifest/privacy-manifest-email.png?v=2) 110 | 111 | To quickly find the keys, type `NS` in the search. These are the ones that are missing from your Manifest. Even if you don't use this API, it can be used by frameworks that you have added to your project. 112 | 113 | Here are the NS keys, and links to the key and the reason on Apple's website: 114 | 115 | - [NSPrivacyAccessedAPICategoryFileTimestamp](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278393) 116 | - [NSPrivacyAccessedAPICategorySystemBootTime](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278394) 117 | - [NSPrivacyAccessedAPICategoryDiskSpace](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278397) 118 | - [NSPrivacyAccessedAPICategoryActiveKeyboards](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278400) 119 | - [NSPrivacyAccessedAPICategoryUserDefaults](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api#4278401) 120 | 121 | # Final Manifest 122 | 123 | Collect the archive Product -> Archive. Right-click on the archive, select Generate Privacy Report. 124 | 125 | ![Exporting the final manifest](https://cdn.sparrowcode.io/tutorials/privacy-manifest/generate-privacy-report.png?v=2) 126 | 127 | In the export PDF-file. All manifests merged into the final one: 128 | 129 | ![PDF report with all manifests](https://cdn.sparrowcode.io/tutorials/privacy-manifest/pdf-report.png?v=2) 130 | 131 | All fields with `.app` extension are from your Manifest. Other fields are third-party frameworks in your project. -------------------------------------------------------------------------------- /ru/tutorials/storekit-external-purchase-link-entitlement-ru.md: -------------------------------------------------------------------------------- 1 | Apple [разрешила](https://t.me/sparrowcode/450) направлять пользователей из РФ на оплату цифровых покупок в приложении на **внешнем сайте**, минуя App Store Payments. Но чтобы вы могли это делать, нужно подать заявку, получить разрешение и обновить приложения. 2 | 3 | > Внутри одного региона вы **не можете** одновременно принимать и внешние платежи, и классические платежи через App Store 4 | 5 | Но можно использовать внешние платежи для РФ, а для других регионов - классические. 6 | 7 | В статье рассматривается только `External Purchase Link`. Но есть еще и `External Purchases` (без Link), где внешняя покупка осуществляется в интерфейсе приложения. Например, карту предлагается ввести на одном из экранов. 8 | 9 | # Заявка 10 | 11 | Заявка это что-то вроде анкеты, практически все поля заполняются автоматически. Подавать заявку [по этой ссылке](https://developer.apple.com/contact/request/storekit-external-entitlement-ru). У вас должен быть аккаунт компании, аккаунт физичего лица не подойдет. Обязательно без Small Business Program. 12 | 13 | > Ссылка на заявку работает только для регионов, где разрешили внешние покупки. 14 | 15 | ![Подаёте заявку](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/request-welcome.jpg?v=1) 16 | 17 | В заявке Apple просит подписанное платное соглашение, но для новых аккаунтов в РФ его не подписать. Если у вас оно подписано, просто следуйте инструкции. 18 | 19 | Если платного соглашения нет, то попробуйте подать заявку без него — Apple проверять не будет. Если же заявку не открывается, вам нужно связаться с Apple и сказать что вы хотите активировать `External Purchase Link`. С недавнего времени Apple вручуню отрабатывает заявки для таких аккаунтов, писал про это [в канале](https://t.me/sparrowcode/530). 20 | 21 | > Чтобы принимать оплаты через App Store Payments, можно [открыть компанию в Великобритании](https://sparrowcode.io/ru/business/company_registration). Работает с РФ-паспортом 22 | 23 | Дальше введите название приложения (локализация не важна), Bundle ID и описание. Здесь мы написали коротко: приложение доступно только для iOS, внутри есть бесплатный и платный функционал. 24 | 25 | ![Заполняете инфо о приложении](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/request-app-info.jpg?v=1) 26 | 27 | Следующий шаг — указать ваш эквайринг: 28 | 29 | ![Указываете ваш эквайринг](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/reqeust-payment-processing.jpg?v=1) 30 | 31 | > Проверяйте чтобы эквайринг был не под санкциями. Нашу первую заявку отклонили, потому что мы указали ЮКассу. 32 | 33 | Сейчас работают эти эквайринги: 34 | 35 | - Солид Банк 36 | - Москоммерцбанк 37 | - Фора Банк 38 | - Дело Банк 39 | 40 | Точно не работают эти: 41 | 42 | - Интеза (убрали эквайринг) 43 | - Райффайзенбанк (не подключают новых клиентов) 44 | - ЮниКредит (не подключают новых клиентов) 45 | - ЮКасса (под санкциями) 46 | - Яндекс Банк (под санкциями) 47 | - Ситибанк 48 | - ОТП Банк 49 | - Ренессанс Банк 50 | - Азиатско-Тихоокеанский банк 51 | - Кредит Европа Банк 52 | - ББР Банк 53 | - БКС Банк 54 | - Робокасса и все другие агрегаторы 55 | 56 | Если у вас есть дополнительная информация про банки и эквайринги, [напишите мне](https://t.me/ivanvorobei), я обновлю список. 57 | 58 | Теперь заполняем информация о веб-сайте, здесь нужно указать страницу оплаты (куда будете направлять пользователей) и страницу поддержки по вопросам платежей. 59 | 60 | > Ссылка может содержать только путь — не должно быть параметров и меток. URL в заявке должен совпадать с реальной ссылкой 61 | 62 | Эти URL вы будете добавлять в `Info.plist`. Фактически сайт открывать будете не вы, а системное окно. Без параметров тяжелее определить какой именно пользователь оплатил. Вам нужно будет или его авторизировать перед оплатой, или после оплаты попросить ввести почту. 63 | 64 | ![Заполняете инфо о сайте](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/request-website-info.jpg?v=1) 65 | 66 | ## Требования к сайту 67 | 68 | На сайте нужно указать: 69 | 70 | - Оплачивать безопасно: используется шифрование платежных систем VISA International, MasterCard Worldwide, МИР 71 | - Контакты технической поддержки по обработке платежей 72 | - Приложение `yourapp` поддерживает оплату по внешней ссылке 73 | - Ваша компания несёт ответственность за возврат платежей 74 | 75 | В качестве примера, можете взглянуть на [нашу страницу](https://rentel.app/rentel-support?v=1). 76 | 77 | Последний шаг — заполнить информацию о компании, заполняется руководителем: 78 | 79 | ![Заполняете инфо о компании](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/request-company-info.jpg) 80 | 81 | # Проверка заявки 82 | 83 | Нашу заявку отклонили через 7 дней, потому что эквайринг ЮКасса в этот момент находился под санкциями. Мы сменили эквайринг на Райффайзен, заполнили новую заявку. Но не смогли её отправить — наша старая заявка висела в статусе рассмотрения. 84 | 85 | В течение месяца мы писали на eurodev@apple.com, чтобы аннулировать первую заявку. Она блокировала подачу новой заявки. Так мы потеряли время. 86 | 87 | Я отправил вторую заявку. Через 7 дней увидел в Apple Developer что мне доступен `Additional Capabilities` для бандла приложения. 88 | 89 | ![Новый Capability](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/additional-capabilities.jpg?v=1) 90 | 91 | Уведомлений на почту не приходило, так что регулярно проверяйте Developer, Certifies, Identifiers & Profiles. 92 | 93 | Внутри `Additional Capabilities` выбираете `ExternalPurchaseLink` и применяете изменения. Теперь нужно интегрировать эту capabilty в приложение. 94 | 95 | # Настраиваем приложение 96 | 97 | В списке Capability появится новая `StoreKit External Purchase Link Entitlement (RU)`. Добавляете к приложению: 98 | 99 | ![Добавляете `StoreKit External Purchase Link Entitlement (RU)` Capability](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/capability.jpg?v=1) 100 | 101 | В `Info.plist` добавляем словарь. В словаре указываем ссылку на оплату на сайте, для каждой страны своя ссылка: 102 | 103 | ``` 104 | SKExternalPurchaseLink 105 | 106 | ru 107 | https://yourapp.com/price 108 | 109 | ``` 110 | 111 | Аббревиатура страны по стандарту ISO. 112 | 113 | Сайт внешних покупок нужно открывать не как ссылку, а вызывать `try await ExternalPurchaseLink.open()` из StoreKit. Пользователю покажут системный дисклеймер, что “полномочия Apple всё”, и если что-то пойдёт не так, разбираться с разработчиком придётся самостоятельно. 114 | 115 | ![Системный дисклеймер перед редиректом](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/system-dicamer-before-payment.png?v=1) 116 | 117 | # Проверка приложения 118 | 119 | Когда отправляете на проверку, прикрепите видео, где видно процесс авторизации, окно со встроенными покупками и переход на сайт. Обязательно показать что URL совпадает с URL в заявке. 120 | 121 | > В гайдах говорят про скриншот, но нас попросили именно видео 122 | 123 | Видео можно залить на хостинг, и в комментарии к ревью указать ссылку. Комментарий для ревьюера не пропадает между версиями, так что сделать это придется один раз. 124 | 125 | Мы отправили билд на проверку, но получили реджект. Правила для UI к этому моменту обновили. Выяснилось, что нельзя показывать тарифы в самом приложении, только кнопку на сайт для платежа. 126 | 127 | Оформили в точности как в примере для американского референса для аналогичной capability, повторили даже иконку в кнопке. Символа иконки нет в SFSymbols, поэтому ниже в статье есть ссылка на картинку в векторе. 128 | 129 | ![В приложении нельзя указывать тарифы](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/reject.jpg?v=4) 130 | 131 | Мы обновили приложение и прошли в App Store: 132 | 133 | [Rentel в App Store](https://apps.apple.com/app/id1632637156): Превращает iOS устройство в кассу для приема платежей 134 | 135 | На странице приложения в App Store появилась вот такая метка. Мне больше понравился бы жёлтый восклицательный знак, но что поделать. 136 | 137 | ![Превью приложения в App Store](https://cdn.sparrowcode.io/tutorials/storekit-external-purchase-link-entitlement-ru/appstore-app-preview.jpg?v=4) 138 | 139 | # Комиссия и отчёты 140 | 141 | Компания должна регулярно отправлять 2 отчета: сводный и подробный. 142 | 143 | **Сводный** информирует об общем кол-ве продаж подписок и общей полученной девелопером сумме с продаж, за вычетом региональных налогов. 144 | 145 | **В подробном** требуется отчитаться о каждой оплаченной подписке с указанием SKU транзакции из чека на оплату. 146 | 147 | Для отправки отчетов предоставляется 15 дней. Мы уже отправили в Apple Distribution International отчеты за финансовый период 31.12.23 - 03.02.24. Если бы мы не отправили их, то сотрудники Apple связались бы с нами 18.02.24, о чем написали бы нам на почту. 148 | 149 | Каждый месяц мы отправляем отчеты по форме Apple о покупках. На основе отчетов в личном кабинете появляются счета за комиссию, 27%. 150 | 151 | Комиссию оплачиваете картой зарубежного банка или через мобильного оператора. 152 | 153 | > Мы консультируем как принимать оплаты и по реджектам. Записаться на консультацию [по ссылке](https://sparrowcode.io/ru/business/consultation) 154 | 155 | # Ссылки по теме 156 | 157 | [Официальная Инструкция для RU](https://developer.apple.com/contact/request/storekit-external-entitlement-ru): Официальная инструкция и запрос StoreKit Entitlement. Ссылка открывается только если аккаунт владельца с регионом РФ 158 | [Инструкция для US](https://developer.apple.com/support/storekit-external-entitlement-us/): Не для RU региона, но внутри полезные скриншоты. 159 | [Скачать иконку](https://developer.apple.com/support/downloads/Link-out-template.zip): Оригинальная иконка для кнопки на оплату на сайте. 160 | [Статья "Первыми в App Store внедрили оплату подписки на расчётный счет ООО в РФ"](https://vc.ru/u/rentel/1024516-pervymi-v-app-store-vnedrili-oplatu-podpiski-na-raschetnyy-schet-ooo-v-rf): Плюсы и минусы внешних покупок по закону 161 | -------------------------------------------------------------------------------- /ru/tutorials/formatters.md: -------------------------------------------------------------------------------- 1 | Она пригодится, если захотите локализовать данные в правильном формате в зависимости от выбранного языка. Например, сумму `3 000,00 ₽`, дату `24 апр. 2022 г.` или процент `54 %`. 2 | 3 | ![Пример локализации процента на разные языки с помощью форматтера.](https://cdn.sparrowcode.io/tutorials/formatters/formatters-preview.jpg) 4 | 5 | Чтобы получить идентификатор локали, вызовите `Locale.current.identifier`. Вернётся значение `языкприложения_ЯЗЫКРЕГИОНА`, например, `en_US`. Полный список таких идентификаторов найдёте [по ссылке](https://gist.github.com/jacobbubu/1836273) 6 | 7 | > Apple используют ISO стандартизацию, поэтому если на устройстве язык, который не соответствует региону, вернутся разные значения. Например, для `en_RU` вместо `₽` вернётся `RUB`. 8 | 9 | # Дата 10 | 11 | Получаем текущую дату: 12 | 13 | ```swift 14 | let currentDate = Date() 15 | ``` 16 | 17 | Создаём и настраиваем объект `DateFormatter`: 18 | 19 | ```swift 20 | let dateFormatter = DateFormatter() 21 | // Задаём стиль, например `.medium` 22 | dateFormatter.dateStyle = DateFormatter.Style.medium 23 | dateFormatter.timeStyle = DateFormatter.Style.medium 24 | 25 | // Указываем локаль 26 | dateFormatter.locale = Locale.current 27 | ``` 28 | 29 | Выводим локализованную дату: 30 | 31 | ```swift 32 | print(dateFormatter.string(from: currentDate)) 33 | ``` 34 | 35 | В консоли будет `24 апр. 2022 г., 02:05:34`. 36 | 37 | Так же можно создать свой формат даты, вместо стиля: 38 | 39 | ```swift 40 | dateFormatter.setLocalizedDateFormatFromTemplate("MMddyyyy") // Так же доступны часы `HH` и минуты `mm` 41 | ``` 42 | 43 | В консоли будет `24/04/2022`. 44 | 45 | # Время 46 | 47 | ## Продолжительность 48 | 49 | Создаем объект `DateComponentsFormatter`: 50 | 51 | ```swift 52 | let dateComponentsFormatter = DateComponentsFormatter() 53 | ``` 54 | 55 | Выбираем стиль и единицы времени для отображения: 56 | 57 | ```swift 58 | dateComponentsFormatter.unitsStyle = .abbreviated // Стиль 59 | dateComponentsFormatter.allowedUnits = [.month, .day, .hour, .minute] // Единицы, при выводе используются нужные. Можно убрать лишние 60 | ``` 61 | 62 | Доступны разные стили: 63 | 64 | - `.abbreviated` - 2 ч 32 мин 65 | - `.full` - 2 часа 32 минуты 66 | - `.spellOut` - два часа тридцать две минуты 67 | - `.positional` - 2:32 (надо убрать лишние `allowedUnits`) 68 | - `.short` - сокращение (для некоторых языков) 69 | - `.brief` - короче, чем `short` 70 | 71 | Получаем интервал, который будем локализовать: 72 | 73 | ```swift 74 | let interval = Date.current.timeIntervalSince(Date.current.addingTimeInterval(-9132)) 75 | let formattedInterval = dateComponentsFormatter.string(from: interval) 76 | ``` 77 | 78 | Выводим результат: 79 | 80 | ```swift 81 | print(formattedInterval) 82 | ``` 83 | 84 | Получаем `2 ч 32 мин` в консоли. 85 | 86 | ## Отсчет 87 | 88 | Создаем объект `RelativeDateTimeFormatter`: 89 | 90 | ```swift 91 | let relativeDateTimeFormatter = RelativeDateTimeFormatter() 92 | ``` 93 | 94 | Выбираем стиль: 95 | 96 | ```swift 97 | relativeDateTimeFormatter.unitsStyle = .full 98 | ``` 99 | 100 | Доступны разные стили: 101 | - `.full` - полное отображение «2 месяца назад» 102 | - `.short` - сокращение «2 мес. назад» 103 | - `.abbreviated` - аббревиатура «-2 м» 104 | - `.spellOut` - разговорное «два месяца назад» 105 | 106 | ```swift 107 | let start = Date.current.addingTimeInterval(-15) // время, от которого считаем сколько прошло 108 | let finish = Date() // время, к которому считаем сколько прошло 109 | 110 | let interval = relativeDateTimeFormatter.localizedString(for: start, relativeTo: finish) 111 | ``` 112 | 113 | Выводим результат: 114 | 115 | ```swift 116 | print(interval) 117 | ``` 118 | 119 | Получаем `15 секунд назад` в консоли. Если поменяем `start` на `Date.current.addingTimeInterval(15)` (будущее время), получим `через 15 секунд` в консоли. 120 | 121 | # Валюта 122 | 123 | Создадим объект `NumberFormatter`: 124 | 125 | ```swift 126 | let currencyFormatter = NumberFormatter() 127 | currencyFormatter.numberStyle = .currency 128 | ``` 129 | 130 | Укажем локаль: 131 | 132 | ```swift 133 | currencyFormatter.locale = Locale.current 134 | ``` 135 | 136 | Получим локализованное значение для 3000: 137 | 138 | ```swift 139 | print(currencyFormatter.string(from: 3000)!) 140 | ``` 141 | 142 | В консоли будет `3 000,00 ₽`. 143 | 144 | # Дробное число 145 | 146 | Создаём и настраиваем объект `NumberFormatter`: 147 | 148 | ```swift 149 | let numberFormatter = NumberFormatter() 150 | numberFormatter.numberStyle = .decimal 151 | 152 | // Указываем локаль 153 | numberFormatter.locale = Locale.current 154 | ``` 155 | 156 | Выводим локализованное число: 157 | 158 | ```swift 159 | print(numberFormatter.string(from: 123456)) 160 | ``` 161 | 162 | Получаем `123,456` в консоли. 163 | 164 | # Процент 165 | 166 | Создаем число, из которого хотим сделать процент: 167 | 168 | ```swift 169 | let number = 54 170 | 171 | // Получаем число с процентом, используя форматтер: 172 | let percent = number.formatted(.percent) 173 | ``` 174 | 175 | Выводим процент: 176 | 177 | ```swift 178 | print(percent) 179 | ``` 180 | 181 | Получаем `54 %` в консоли. 182 | 183 | # Расстояние 184 | 185 | Создаем объект `Measurement`: 186 | 187 | ```swift 188 | let measurement = Measurement( 189 | value: 43.23, // Расстояние 190 | unit: UnitLength.kilometers // Единица измерения 191 | ) 192 | ``` 193 | 194 | В `UnitLength` доступно 22 единицы измерения. 195 | 196 | Создаем объект `MeasurementFormatter`: 197 | 198 | ```swift 199 | let measurementFormatter = MeasurementFormatter() 200 | 201 | // Выбираем стиль, доступы полный `.long` и сокращенные `.short`, `.medium` 202 | measurementFormatter.unitStyle = .long 203 | ``` 204 | 205 | Выводим расстояние: 206 | 207 | ```swift 208 | print(measurementFormatter.string(from: measurement)) 209 | ``` 210 | 211 | Получаем `43,23 километра` в консоли. 212 | 213 | # Размер 214 | 215 | Создаем объект `LengthFormatter` 216 | 217 | ```swift 218 | let lengthFormatter = LengthFormatter() 219 | 220 | // Выбираем стиль, доступы полный `.long` и сокращенные `.short`, `.medium` 221 | lengthFormatter.unitStyle = .long 222 | ``` 223 | 224 | Получаем значение: 225 | 226 | ```swift 227 | let value = lengthFormatter.string(fromValue: 14.5, unit: .millimeter) 228 | ``` 229 | 230 | Доступны разные `unit`: 231 | - `millimeter` - милиметр 232 | - `centimeter` - сантиметр 233 | - `meter` - метр 234 | - `kilometer` - километр 235 | - `inch` - дюйм 236 | - `foot` - фут 237 | - `yard` - ярд 238 | - `mile` - миля 239 | 240 | Выводим размер: 241 | 242 | ```swift 243 | print(value) 244 | ``` 245 | 246 | Получаем `14,5 миллиметра` в консоли. 247 | 248 | 249 | # Энергия 250 | 251 | Создаем объект `EnergyFormatter`: 252 | 253 | ```swift 254 | let energyFormatter = EnergyFormatter() 255 | 256 | // Выбираем стиль, доступы полный `.long` и сокращенные `.short`, `.medium` 257 | energyFormatter.unitStyle = .long 258 | ``` 259 | 260 | Получаем значение: 261 | 262 | ```swift 263 | let value = energyFormatter.string(fromValue: 69.5, unit: .calorie) 264 | ``` 265 | 266 | Доступны разные `unit`: 267 | - `.calorie` - калории 268 | - `.joule` - джоули 269 | - `.kilocalorie` - килокалории 270 | - `.kilojoule` - килоджоули 271 | 272 | Выводим значение: 273 | 274 | ```swift 275 | print(value) 276 | ``` 277 | 278 | Получаем `69,5 калории` в консоли. 279 | 280 | # Вес 281 | 282 | Создаем объект `MassFormatter` 283 | 284 | ```swift 285 | let massFormatter = MassFormatter() 286 | 287 | // Выбираем стиль, доступы полный `.long` и сокращенные `.short`, `.medium` 288 | massFormatter.unitStyle = .long 289 | ``` 290 | 291 | Получаем значение: 292 | 293 | ```swift 294 | let value = massFormatter.string(fromValue: 75.2, unit: .kilogram) 295 | ``` 296 | 297 | Доступны разные `unit`: 298 | - `.kilogram` - килограмм 299 | - `.gram` - грамм 300 | - `.pound` - фунт 301 | - `.ounce` - унция 302 | - `.stone` - стоун 303 | 304 | Выводим вес: 305 | 306 | ```swift 307 | print(value) 308 | ``` 309 | 310 | Получаем `75,2 килограмма` в консоли. 311 | 312 | # Объем файла 313 | 314 | Создаем проперти с объемом файла в байтах: 315 | 316 | ```swift 317 | let number = 54347323 318 | 319 | // Получаем локализованный объем файла, используя форматтер: 320 | let byteCount = number.formatted(.byteCount(style: .file)) 321 | ``` 322 | 323 | Выводим объем: 324 | 325 | ```swift 326 | print(byteCount) 327 | ``` 328 | 329 | Получаем `54.3 МБ` в консоли. 330 | 331 | # Список 332 | 333 | Создаём массив, из которого будем делать список: 334 | 335 | ```swift 336 | let list = ["Swift", "Java", "Python"] 337 | ``` 338 | 339 | Создаём и настраиваем объект `ListFormatter`: 340 | 341 | ```swift 342 | let listFormatter = ListFormatter() 343 | 344 | // Указываем локаль 345 | listFormatter.locale = Locale.current 346 | ``` 347 | 348 | Выводим локализованный список: 349 | 350 | ```swift 351 | print(listFormatter.string(from: list)) 352 | ``` 353 | 354 | Получаем `Swift, Java и Python` в консоли. Работает с любым количеством элементов. 355 | 356 | # Имена 357 | 358 | Создаём и настраиваем объект класса `PersonNameComponents`: 359 | 360 | ```swift 361 | var nameComponents = PersonNameComponents() 362 | nameComponents.familyName = "Петров" 363 | nameComponents.givenName = "Александр" 364 | nameComponents.nameSuffix = "Младший" 365 | nameComponents.nickname = "Саня" 366 | ``` 367 | 368 | Доступны разные компоненты, например: 369 | - `namePrefix` - часть имени, до основного 370 | - `givenName` - основное имя 371 | - `nameSuffix` - часть имени, после основного 372 | - `middleName` - второе имя 373 | - `familyName` - фамилия 374 | - `nickname` - псевдоним 375 | 376 | Создаём объект класса `PersonNameComponentsFormatter`, с помощью которого будем форматировать имя: 377 | 378 | ```swift 379 | let nameFormatter = PersonNameComponentsFormatter() 380 | ``` 381 | 382 | Задаем стиль, доступны: 383 | - `.default` и `.medium` - имя, фамилия 384 | - `.short` - псевдоним 385 | - `.abbreviated` - инициалы имени, фамилии 386 | - `.long` - все компоненты, кроме псевдонима 387 | 388 | Выводим результат: 389 | 390 | ```swift 391 | formatter.style = .default // совпадает с `.medium` или отсутствием стиля 392 | print(nameFormatter.string(from: nameComponents)) 393 | // В консоли `Александр Петров` 394 | 395 | formatter.style = .short 396 | print(nameFormatter.string(from: nameComponents)) 397 | // В консоли `Саня` 398 | 399 | formatter.style = .abbreviated 400 | print(nameFormatter.string(from: nameComponents)) 401 | // В консоли `АП` 402 | 403 | formatter.style = .long 404 | print(nameFormatter.string(from: nameComponents)) 405 | // В консоли `Александр Младший Петров` 406 | ``` 407 | -------------------------------------------------------------------------------- /developers.json: -------------------------------------------------------------------------------- 1 | { 2 | "emartinson": { 3 | "apps": [ 4 | { 5 | "id": "6461120622", 6 | "added_date": "25.01.2025" 7 | } 8 | ] 9 | }, 10 | "pavel-selivanov": { 11 | "apps": [ 12 | { 13 | "id": "6461726747", 14 | "added_date": "26.03.2024" 15 | }, { 16 | "id": "6477779455", 17 | "added_date": "11.04.2024" 18 | } 19 | ] 20 | }, 21 | "ilya-kovalenko": { 22 | "apps": [ 23 | { 24 | "id": "1503981169", 25 | "added_date": "30.01.2024" 26 | } 27 | ] 28 | }, 29 | "kambala-decapitator": { 30 | "apps": [ 31 | { 32 | "id": "482487701", 33 | "added_date": "07.02.2022" 34 | }, { 35 | "id": "644228154", 36 | "added_date": "07.02.2022" 37 | } 38 | ] 39 | }, 40 | "rastaman111": { 41 | "apps": [ 42 | { 43 | "id": "1586640348", 44 | "added_date": "18.11.2021" 45 | }, { 46 | "id": "1459321621", 47 | "added_date": "22.05.2025" 48 | } 49 | ] 50 | }, 51 | "Viktorianec": { 52 | "apps": [ 53 | { 54 | "id": "1473622434", 55 | "added_date": "05.04.2022" 56 | }, { 57 | "id": "1017699433", 58 | "added_date": "05.04.2022" 59 | }, { 60 | "id": "1029476822", 61 | "added_date": "05.04.2022" 62 | }, { 63 | "id": "1088581020", 64 | "added_date": "05.04.2022" 65 | }, { 66 | "id": "1574916839", 67 | "added_date": "05.04.2022" 68 | } 69 | ] 70 | }, 71 | "baksogen": { 72 | "apps": [ 73 | { 74 | "id": "1529716191", 75 | "added_date": "05.04.2022" 76 | }, { 77 | "id": "891797540", 78 | "added_date": "05.04.2022" 79 | }, { 80 | "id": "1382928700", 81 | "added_date": "05.04.2022" 82 | }, { 83 | "id": "1386377748", 84 | "added_date": "05.04.2022" 85 | }, { 86 | "id": "576182327", 87 | "added_date": "05.04.2022" 88 | }, { 89 | "id": "1229503218", 90 | "added_date": "05.04.2022" 91 | } 92 | ] 93 | }, 94 | "artembolotov": { 95 | "apps": [ 96 | { 97 | "id": "1535523842", 98 | "added_date": "05.04.2022" 99 | } 100 | ] 101 | }, 102 | "kopsap4ik": { 103 | "apps": [ 104 | { 105 | "id": "1579159150", 106 | "added_date": "22.04.2022" 107 | } 108 | ] 109 | }, 110 | "NathanFallet": { 111 | "apps": [ 112 | { 113 | "id": "1598813588", 114 | "added_date": "03.05.2022" 115 | } 116 | ] 117 | }, 118 | "YurchukV": { 119 | "apps": [ 120 | { 121 | "id": "957083912", 122 | "added_date": "21.05.2022" 123 | }, { 124 | "id": "1238801709", 125 | "added_date": "21.05.2022" 126 | }, { 127 | "id": "942061038", 128 | "added_date": "21.05.2022" 129 | }, { 130 | "id": "1672420617", 131 | "added_date": "21.05.2022" 132 | }, { 133 | "id": "1323520875", 134 | "added_date": "21.05.2022" 135 | } 136 | ] 137 | }, 138 | "Rogue85": { 139 | "apps": [ 140 | { 141 | "id": "1619685571", 142 | "added_date": "09.06.2022" 143 | } 144 | ] 145 | }, 146 | "alxrguz": { 147 | "apps": [ 148 | { 149 | "id": "1521429599", 150 | "added_date": "15.07.2022" 151 | }, { 152 | "id": "6443957774", 153 | "added_date": "24.11.2022" 154 | } 155 | ] 156 | }, 157 | "ivanvorobei": { 158 | "repositories": [ 159 | "https://github.com/sparrowcode/PermissionsKit", "https://github.com/sparrowcode/SwiftBoost", "https://github.com/sparrowcode/SafeSFSymbols", "https://github.com/sparrowcode/SettingsIconGenerator", "https://github.com/sparrowcode/SPQRCode", "https://github.com/sparrowcode/AlertKit", "https://github.com/ivanvorobei/SPIndicator", "https://github.com/ivanvorobei/SPPerspective", "https://github.com/ivanvorobei/SPPageController" 160 | ], 161 | "projects": [ 162 | { 163 | "name": { 164 | "en": "Код Воробья", 165 | "ru": "Sparrow Code" 166 | }, 167 | "description": "Туториалы для iOS разработчиков.", 168 | "url": "https://sparrowcode.io", 169 | "added_date": "03.11.2022" 170 | } 171 | ], 172 | "apps": [ 173 | { 174 | "id": "1624477055", 175 | "added_date": "09.10.2022" 176 | }, { 177 | "id": "1625641322", 178 | "added_date": "09.10.2022" 179 | }, { 180 | "id": "875280793", 181 | "added_date": "09.10.2022" 182 | }, { 183 | "id": "743843090", 184 | "added_date": "09.10.2022" 185 | }, { 186 | "id": "537070378", 187 | "added_date": "09.10.2022" 188 | }, { 189 | "id": "1617055933", 190 | "added_date": "09.10.2022" 191 | }, { 192 | "id": "6449774982", 193 | "added_date": "16.08.2023" 194 | } 195 | ] 196 | }, 197 | "svyatoynick": { 198 | "repositories": [ 199 | "https://github.com/svyatoynick/GAuthSwiftParser" 200 | ], 201 | "apps": [ 202 | { 203 | "id": "1625641322", 204 | "added_date": "09.10.2022" 205 | } 206 | ] 207 | }, 208 | "konnnn": { 209 | "apps": [ 210 | { 211 | "id": "1193567206", 212 | "added_date": "07.03.2023" 213 | }, { 214 | "id": "1517243559", 215 | "added_date": "07.03.2023" 216 | } 217 | ] 218 | }, 219 | "TopScrech": { 220 | "apps": [ 221 | { 222 | "id": "1639409934", 223 | "added_date": "09.03.2023" 224 | }, 225 | { 226 | "id": "6624303981", 227 | "added_date": "24.03.2025" 228 | }, 229 | { 230 | "id": "6504800979", 231 | "added_date": "24.03.2025" 232 | }, 233 | { 234 | "id": "6636466492", 235 | "added_date": "24.03.2025" 236 | }, 237 | { 238 | "id": "6736841839", 239 | "added_date": "24.03.2025" 240 | }, 241 | { 242 | "id": "6502962295", 243 | "added_date": "24.03.2025" 244 | } 245 | ] 246 | }, 247 | "izyumkin": { 248 | "repositories": [ 249 | "https://github.com/izyumkin/MCEmojiPicker" 250 | ], 251 | "apps": [ 252 | { 253 | "id": "1500111859", 254 | "added_date": "09.03.2023" 255 | } 256 | ] 257 | }, 258 | "astat-narek": { 259 | "apps": [ 260 | { 261 | "id": "6447767130", 262 | "added_date": "19.04.2023" 263 | }, 264 | { 265 | "id": "6736710758", 266 | "added_date": "07.11.2024" 267 | } 268 | ] 269 | }, 270 | "tact": { 271 | "apps": [ 272 | { 273 | "id": "6446785685", 274 | "added_date": "19.04.2023" 275 | }, { 276 | "id": "1664121598", 277 | "added_date": "19.04.2023" 278 | }, { 279 | "id": "1615759035", 280 | "added_date": "19.04.2023" 281 | }, { 282 | "id": "1584162246", 283 | "added_date": "19.04.2023" 284 | } 285 | ] 286 | }, 287 | "azaiv": { 288 | "apps": [ 289 | { 290 | "id": "6449774982", 291 | "added_date": "10.08.2023" 292 | } 293 | ] 294 | }, 295 | "Kylmakalle": { 296 | "apps": [ 297 | { 298 | "id": "6471879502", 299 | "added_date": "15.12.2023" 300 | } 301 | ], 302 | "repositories": [ 303 | "https://github.com/Swiftgram/TDLibKit" 304 | ] 305 | }, 306 | "AlexeyBrilyov": { 307 | "apps": [ 308 | { 309 | "id": "667579844", 310 | "added_date": "22.12.2023" 311 | }, { 312 | "id": "1081939665", 313 | "added_date": "22.12.2023" 314 | } 315 | ] 316 | }, 317 | "ardavydov": { 318 | "apps": [ 319 | { 320 | "id": "1537409012", 321 | "added_date": "21.12.2023" 322 | } 323 | ] 324 | }, 325 | "fedafone": { 326 | "apps": [ 327 | { 328 | "id": "6474557776", 329 | "added_date": "27.12.2023" 330 | } 331 | ] 332 | }, 333 | "AppNest": { 334 | "apps": [ 335 | { 336 | "id": "6473675965", 337 | "added_date": "24.01.2024" 338 | } 339 | ] 340 | }, 341 | "prooxyyy": { 342 | "apps": [ 343 | { 344 | "id": "6476628951", 345 | "added_date": "21.02.2024" 346 | } 347 | ] 348 | }, 349 | "adrian41101": { 350 | "apps": [ 351 | { 352 | "id": "1671650132", 353 | "added_date": "23.03.2024" 354 | } 355 | ] 356 | }, 357 | "amirhanov": { 358 | "apps": [ 359 | { 360 | "id": "1520215283", 361 | "added_date": "02.10.2024" 362 | }, 363 | { 364 | "id": "1501369238", 365 | "added_date": "02.10.2024" 366 | }, 367 | { 368 | "id": "1590165697", 369 | "added_date": "02.10.2024" 370 | }, 371 | { 372 | "id": "1527768498", 373 | "added_date": "02.10.2024" 374 | }, 375 | { 376 | "id": "1495551006", 377 | "added_date": "02.10.2024" 378 | }, 379 | { 380 | "id": "1574706111", 381 | "added_date": "02.10.2024" 382 | } 383 | ] 384 | }, 385 | "masalimov": { 386 | "apps": [ 387 | { 388 | "id": "1597413161", 389 | "added_date": "03.10.2024" 390 | } 391 | ] 392 | }, 393 | "SysoevAndrey": { 394 | "apps": [ 395 | { 396 | "id": "6739433398", 397 | "added_date": "26.12.2024" 398 | } 399 | ] 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /en/tutorials/live-activities.md: -------------------------------------------------------------------------------- 1 | Live Activity combines push notifications into one interactive banner. For example, when a cab pulls up, you get a push that the driver is coming, the driver is nearby, and the driver is waiting. With the new tool, developers will be able to merge push notifications into Live Activity and update it. 2 | 3 | > Live Activity is available with iOS 16.1 and Xcode 14.1. 4 | 5 | Live Activity is not a widget - there are no timelines and therefore no updates by time. The main way to update is by pushing. See [how to update and terminate Live Activity](https://sparrowcode.io/ru/tutorials/live-activities) for the update methods. 6 | 7 | ![Compact and Expanded Live Activity.](https://cdn.sparrowcode.io/tutorials/live-activities/header.png) 8 | 9 | Live Activity is shown on devices with and without Dynamic Island. On a locked screen, it will look like a normal push notification. For devices with Dynamic Island, Live Activity is shown around the cameras. 10 | 11 | [Sample project on GitHub](https://github.com/sparrowcode/live-activity-example): How to add a Live Activity, update and close. UI for Live Activity. 12 | 13 | # Adding Live Activity to the project 14 | 15 | Live Activity uses the ActivityKit framework. Live Activity lives in the widget's targeting: 16 | 17 | ![Add the WidgetKit Target to the project.](https://cdn.sparrowcode.io/tutorials/live-activities/add-widget-target.png) 18 | 19 | Go to Target, and leave the code: 20 | 21 | ```swift 22 | @main 23 | struct LiveActivityWidget: Widget { 24 | 25 | let kind: String = "LiveActivityWidget" 26 | 27 | var body: some WidgetConfiguration { 28 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 29 | widgetEntryView(entry: entry) 30 | } 31 | .configurationDisplayName("My Widget") 32 | .description("This is an example widget.") 33 | } 34 | } 35 | ``` 36 | 37 | > If you already have widgets, use `WidgetBundle` to define multiple `Widgets`. 38 | 39 | In `Info.plist`, add the attribute `Supports Live Activities`: 40 | 41 | ``` 42 | NSSupportsLiveActivities 43 | 44 | ``` 45 | 46 | `StaticConfiguration` is used for widgets and complications. We will replace it with another one soon, but first we will define the data model. 47 | 48 | # Data model 49 | 50 | Live Activity is created in the application itself, and the model will be used in both the application and the widget. So it's a good idea to make one class and poke around between the targetets. Create a new file for the model, inherit from `ActivityAttributes`: 51 | 52 | ```swift 53 | import ActivityKit 54 | 55 | struct ActivityAttribute: ActivityAttributes { 56 | 57 | public struct ContentState: Codable, Hashable { 58 | 59 | // Dynamic data 60 | 61 | var dynamicStringValue: String 62 | var dynamicIntValue: Int 63 | var dynamicBoolValue: Bool 64 | } 65 | 66 | // Static data 67 | 68 | var staticStringValue: String 69 | var staticIntValue: Int 70 | var staticBoolValue: Bool 71 | } 72 | ``` 73 | 74 | Define dynamic data in the `ContentState` structure - it will change and update the UI. Outside `ContentState` - static data, it is available only when creating Live Activity. 75 | 76 | Share the file between the two targets by selecting the application's main target and widget target in the inspector on the right: 77 | 78 | ![The file will be available in the main and widget-targets.](https://cdn.sparrowcode.io/tutorials/live-activities/shared-file-between-targets.png) 79 | 80 | # UI 81 | 82 | In the `LiveActivityWidget` object, change the configuration to `ActivityConfiguration`: 83 | 84 | ```swift 85 | struct LiveActivityWidget: Widget { 86 | 87 | let kind: String = "LiveActivityWidget" 88 | 89 | var body: some WidgetConfiguration { 90 | ActivityConfiguration(for: ActivityAttribute.self) { context in 91 | // Here is the UI for activity on the locked screen 92 | } dynamicIsland: { context in 93 | // Here is the UI for Dynamic Island 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | Two closures, the first for the UI on the locked screen, the second for the dynamic island. Note, we specify attribute class `ActivityAttribute.self` - this is the data model we defined above. 100 | 101 | > Live Activity ignores animation modifiers. 102 | 103 | ## Lock Screen 104 | 105 | This view is shown on the locked screen. All widget tools are available in Live Activity. Specify a property `context` to pass the data model: 106 | 107 | ```swift 108 | struct LockScreenLiveActivityView: View { 109 | 110 | let context: ActivityViewContext 111 | 112 | var body: some View { 113 | VStack { 114 | Text("Dyanmic String: \(context.state.dynamicStringValue))") 115 | Text("Static String: \(context.staticStringValue))") 116 | } 117 | .activitySystemActionForegroundColor(.indigo) 118 | .activityBackgroundTint(.cyan) 119 | } 120 | } 121 | ``` 122 | 123 | > The maximum height of the Live Activity on Lock Screen is 160 points. 124 | 125 | In the example I printed both dynamic and static properties from `ActivityAttribute`. Let's specify the view in the widget: 126 | 127 | ```swift 128 | struct LiveActivityWidget: Widget { 129 | 130 | let kind: String = "LiveActivityWidget" 131 | 132 | var body: some WidgetConfiguration { 133 | ActivityConfiguration(for: ActivityAttribute.self) { context in 134 | LockScreenLiveActivityView(context: context) 135 | } dynamicIsland: { context in 136 | // Here is the UI for Dynamic Island 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ## Dynamic Island 143 | 144 | The dynamic island has three kinds: compact, minimal and expanded. 145 | 146 | > The corners of the dynamic island are rounded at 44 points. This corresponds to the rounding of the TrueDepth camera. 147 | 148 | ### Compact & Minimal 149 | 150 | If one activity is running - then the content can be placed to the left and right of the dynamic island. 151 | 152 | ![Compact Live Activity in Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-compact.png) 153 | 154 | If more than one Live Activity is running, the system will select two of them. One will show on the left, attached to the island, and the other on the right, separated from the island in a circle. 155 | 156 | ![Minimal Live Activity in Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-minimal.png) 157 | 158 | The code for each display option: 159 | 160 | ```swift 161 | DynamicIsland { 162 | // Here is the code for the expanded view. 163 | // We'll analyze it in the next paragraph. 164 | } compactLeading: { 165 | Text("Leading") 166 | } compactTrailing: { 167 | Text("Trailing") 168 | } minimal: { 169 | Text("Min") 170 | } 171 | ``` 172 | 173 | ### Expanded 174 | 175 | The expanded Live Activity is shown when a person clicks and holds the compact or minimal view. When Live Activity is updated, the expanded view appears automatically for a couple of seconds. 176 | 177 | ![Expanded Live Activity в Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-expanded.png) 178 | 179 | The code for the expanded view. Each closure defines an area on the Live Activity. 180 | 181 | ```swift 182 | DynamicIslandExpandedRegion(.center) {} 183 | DynamicIslandExpandedRegion(.leading) {} 184 | DynamicIslandExpandedRegion(.trailing) {} 185 | DynamicIslandExpandedRegion(.bottom) {} 186 | ``` 187 | 188 | Area markup: 189 | 190 | ![Dynamic Island areas.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-areas.png) 191 | 192 | - **center** content below the camera. 193 | - **leading** space from the left corner to the camera. If you use the vertical stack, the space below will be available. 194 | - **trailing** similar to `leading` but for the right edge. 195 | - **bottom** content below all other areas. 196 | 197 | If the content does not fit in the left and right areas, you can merge it with the `Bottom` area. The area will be adaptive, the screenshot shows the maximum size: 198 | 199 | ![If there is not enough space, the Dynamic Island areas can be combined.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-leading-expanded.png) 200 | 201 | To allow an area to use the space below, specify `verticalPlacement`: 202 | 203 | ```swift 204 | DynamicIslandExpandedRegion(.leading) { 205 | Text("Leading Text with merge region") 206 | .dynamicIsland(verticalPlacement: .belowIfTooWide) 207 | } 208 | ``` 209 | 210 | > The maximum height of the Live Activity on Dynamic Island is 160 points. 211 | 212 | # Add a new Live Activity 213 | 214 | Live Activity can only be created within an app. You can update and end a Live Activity both within the app and via push notification. 215 | 216 | First, check the availability of Live Activities - the user may have banned them or the system has reached the limit. To check, use the code: 217 | 218 | ```swift 219 | guard ActivityAuthorizationInfo().areActivitiesEnabled else { 220 | print("Activities are not enabled") 221 | return 222 | } 223 | ``` 224 | 225 | You can track the status: 226 | 227 | ```swift 228 | for await enabled in ActivityAuthorizationInfo().activityEnablementUpdates { 229 | // Here is your code 230 | } 231 | ``` 232 | 233 | To create a new Live Activity, create attributes and then call `request`: 234 | 235 | ```swift 236 | let attributes = ActivityAttribute(...) 237 | let contentState = ActivityAttribute.ContentState(...) 238 | do { 239 | let activity = try Activity.request( 240 | attributes: attributes, 241 | contentState: contentState 242 | ) 243 | } catch { 244 | print("LiveActivityManager: Error in LiveActivityManager: \(error.localizedDescription)") 245 | } 246 | ``` 247 | 248 | Note, here the static and updatable properties are separated into two objects. 249 | 250 | # List of current Live Activities 251 | 252 | To get the Live Activity created, you must specify an attribute model: 253 | 254 | ```swift 255 | for activity in Activity.activities { 256 | print("Activity details: \(activity.contentState)") 257 | } 258 | ``` 259 | 260 | # Update and end Live Activity 261 | 262 | The Live Activity can only be updated and terminated with dynamic parameters - Content State. 263 | 264 | > The size of the Content State update must be less than 4KB. 265 | 266 | ### Inside the app 267 | 268 | To update Live Activity from within the app: 269 | 270 | ```swift 271 | // New data 272 | let contentState = ActivityAttribute.ContentState(...) 273 | 274 | Task { 275 | await activity?.update(using: contentState) 276 | } 277 | ``` 278 | 279 | To terminate a Live Activity, call: 280 | 281 | ```swift 282 | await activity?.end(dismissalPolicy: .immediate) 283 | ``` 284 | 285 | The Live Activity will close immediately. To keep the Live Activity on the screen for a while longer: 286 | 287 | ```swift 288 | await activity?.end(using: attributes, dismissalPolicy: .default) 289 | ``` 290 | 291 | The Live Activity will be updated with the final data and will be on the screen for some more time. The system will close the activity when the user sees the new data or at most 4 hours later, whichever comes first. 292 | 293 | Live Activity does not have a timeline like widgets. To update or close Live Activity when the application is in the background, you need to use [Background Tasks](https://developer.apple.com/documentation/backgroundtasks). 294 | 295 | > Background Tasks are not guaranteed to run on time. 296 | 297 | ### Through Push Notifications 298 | 299 | When we create a Live Activity, we get a `pushToken`. It is used to update the Live Activity via push notifications. 300 | 301 | > You need to register the application to receive push notifications beforehand. 302 | 303 | Let's form a push to update a Live Activity. Headers: 304 | 305 | ``` 306 | apns-topic: {Your App Bundle ID}.push-type.liveactivity 307 | apns-push-type: {liveactivity 308 | authorization: bearer {Auth Token} 309 | ``` 310 | 311 | Body: 312 | 313 | ``` 314 | "aps": { 315 | "timestamp": 1168364460, 316 | "event": "update", // or end 317 | "content-state": { 318 | "dynamicStringValue": "New String Value" 319 | "dynamicIntValue": 5 320 | "dynamicBoolValue": true 321 | }, 322 | "alert": { 323 | "title": "Title of classic Push", 324 | "body": "Body or classic push", 325 | } 326 | } 327 | ``` 328 | 329 | The `content-state` dictionary must match the attribute model `ActivityAttribute.ContentState`. We can only update dynamic properties. Properties not in ContentState cannot be updated. 330 | 331 | # Trace Press 332 | 333 | Clicking on Live Activity is good to open the relay screen, for this you need to implement Deep Link. Set the modifier `widgetURL(_:)`. You can set a different link for each area: 334 | 335 | ```swift 336 | DynamicIslandExpandedRegion(.leading) { 337 | Text("Leading Text with merge region") 338 | .widgetURL(URL(string: "example://action")) 339 | } 340 | ``` 341 | 342 | The expanded view of Dynamic Island supports [Link](https://developer.apple.com/documentation/SwiftUI/Link). 343 | 344 | > Apple [answers 10 questions](https://developer.apple.com/news/?id=qpqf1gru) about Live Activity. 345 | -------------------------------------------------------------------------------- /ru/tutorials/live-activities.md: -------------------------------------------------------------------------------- 1 | Live Activity объединяют пуш-уведомления в один интерактивный баннер. Представим приложение для вызова такси — какие там могут быть пуши? «Водитель едет», «Водитель уже рядом» и «Водитель ждёт». С новым инструментом разработчики смогут объединить пуши в Live Activity и обновлять её. 2 | 3 | ![Про Dynamic Island в iOS.](https://cdn.sparrowcode.io/tutorials/live-activities/preview.png) 4 | 5 | > Live Activity доступны с iOS 16.1 и Xcode 14.1. 6 | 7 | Live Activity не виджет — у неё нет таймлайнов и обновлений по времени. Основной способ обновления — как раз пуши. Способы обновления разберём в секции [Как обновить и завершить Live Activity](https://sparrowcode.io/ru/tutorials/live-activities#obnovit-i-zavershit-live-activity). 8 | 9 | ![Compact и Expanded Live Activity.](https://cdn.sparrowcode.io/tutorials/live-activities/header.png) 10 | 11 | Live Activity показываются на устройствах с Dynamic Island и без него. На заблокированном экране это будет похоже на обычное пуш-уведомление. А для устройств с Dynamic Island Live Activity показывается вокруг камер. 12 | 13 | [Проект-пример на GitHub](https://github.com/sparrowcode/live-activity-example): Как добавить Live Activity, обновить и закрыть. UI для Live Activity. 14 | 15 | # Добавляем Live Activity в проект 16 | 17 | Live Activity используют фреймворк ActivityKit. Живут Live Activity в таргете виджета: 18 | 19 | ![Добавляем таргет WidgetKit в проект.](https://cdn.sparrowcode.io/tutorials/live-activities/add-widget-target.png) 20 | 21 | Перейдите в таргет и оставьте код: 22 | 23 | ```swift 24 | @main 25 | struct LiveActivityWidget: Widget { 26 | 27 | let kind: String = "LiveActivityWidget" 28 | 29 | var body: some WidgetConfiguration { 30 | StaticConfiguration(kind: kind, provider: Provider()) { entry in 31 | widgetEntryView(entry: entry) 32 | } 33 | .configurationDisplayName("My Widget") 34 | .description("This is an example widget.") 35 | } 36 | } 37 | ``` 38 | 39 | > Если у вас уже есть виджеты, используйте `WidgetBundle`, чтобы определить несколько `Widget`. 40 | 41 | В `Info.plist` добавьте атрибут `Supports Live Activities`: 42 | 43 | ``` 44 | NSSupportsLiveActivities 45 | 46 | ``` 47 | 48 | `StaticConfiguration` используется для виджетов и компликейшнов. Скоро мы заменим его на другой, но сначала определим модель данных. 49 | 50 | # Определяем модель данных 51 | 52 | Live Activity создаётся в самом приложении, а модель будет использоваться и в приложении, и в виджете. Поэтому хорошо бы сделать один класс и пошарить его между таргетами. Создайте новый файл для модели. Для этого наследуемся от `ActivityAttributes`: 53 | 54 | ```swift 55 | import ActivityKit 56 | 57 | struct ActivityAttribute: ActivityAttributes { 58 | 59 | public struct ContentState: Codable, Hashable { 60 | 61 | // Динамические данные 62 | 63 | var dynamicStringValue: String 64 | var dynamicIntValue: Int 65 | var dynamicBoolValue: Bool 66 | } 67 | 68 | // Статические данные 69 | 70 | var staticStringValue: String 71 | var staticIntValue: Int 72 | var staticBoolValue: Bool 73 | } 74 | ``` 75 | 76 | В структуре `ContentState` определяем динамические данные — они будут меняться и обновлять UI. За пределами `ContentState` статические данные, они доступны только при создании Live Activity. 77 | 78 | Пошарьте файл между двумя таргетами, для этого в инспекторе справа выберите главный таргет приложения и таргет виджета: 79 | 80 | ![Файл будет доступен в главном и виджет-таргетах.](https://cdn.sparrowcode.io/tutorials/live-activities/shared-file-between-targets.png) 81 | 82 | # UI 83 | 84 | В объекте `LiveActivityWidget` поменяйте конфигурацию на `ActivityConfiguration`: 85 | 86 | ```swift 87 | struct LiveActivityWidget: Widget { 88 | 89 | let kind: String = "LiveActivityWidget" 90 | 91 | var body: some WidgetConfiguration { 92 | ActivityConfiguration(for: ActivityAttribute.self) { context in 93 | // Здесь UI для активити на заблокированном экране 94 | } dynamicIsland: { context in 95 | // Здесь UI для Dynamic Island 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | У нас есть два замыкания, первое — для UI на заблокированном экране, второе — для динамического острова. Обратите внимание, указываем класс атрибутов `ActivityAttribute.self` — это модель данных, которую определили выше. 102 | 103 | > В Live Activity игнорируются модификаторы анимаций. 104 | 105 | ## Lock Screen 106 | 107 | Эта View показывается на заблокированном экране. Все инструменты для виджетов доступны в Live Activity. Укажите проперти `context`, чтобы передать модель данных: 108 | 109 | ```swift 110 | struct LockScreenLiveActivityView: View { 111 | 112 | let context: ActivityViewContext 113 | 114 | var body: some View { 115 | VStack { 116 | Text("Dyanmic String: \(context.state.dynamicStringValue))") 117 | Text("Static String: \(context.staticStringValue))") 118 | } 119 | .activitySystemActionForegroundColor(.indigo) 120 | .activityBackgroundTint(.cyan) 121 | } 122 | } 123 | ``` 124 | 125 | > Максимальная высота Live Activity на Lock Screen — 160 точек. 126 | 127 | В примере я распечатал и динамические, и статические проперти из `ActivityAttribute`. Давайте укажем вью в виджете: 128 | 129 | ```swift 130 | struct LiveActivityWidget: Widget { 131 | 132 | let kind: String = "LiveActivityWidget" 133 | 134 | var body: some WidgetConfiguration { 135 | ActivityConfiguration(for: ActivityAttribute.self) { context in 136 | LockScreenLiveActivityView(context: context) 137 | } dynamicIsland: { context in 138 | // Здесь UI для Dynamic Island 139 | } 140 | } 141 | } 142 | ``` 143 | 144 | ## Dynamic Island 145 | 146 | У динамического острова есть 3 вида: компактный, минимальный и развёрнутый. 147 | 148 | > Углы динамического острова закруглили в 44 точки. Это соответствует закруглению камеры TrueDepth. 149 | 150 | ### Compact & Minimal 151 | 152 | Если запущена одна активность, то контент можно разместить слева и справа от динамического острова. 153 | 154 | ![Compact Live Activity в Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-compact.png) 155 | 156 | Если запущено несколько Live Activity, система выберет две из них. Одна будет показываться слева, она прикреплена к острову, а другая справа — отделённая от острова в кружке. 157 | 158 | ![Minimal Live Activity в Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-minimal.png) 159 | 160 | Так выглядит код для каждого варианта отображения: 161 | 162 | ```swift 163 | DynamicIsland { 164 | // Здесь будет код для развёрнутого вида. 165 | // Его разберем в след. пункте. 166 | } compactLeading: { 167 | Text("Leading") 168 | } compactTrailing: { 169 | Text("Trailing") 170 | } minimal: { 171 | Text("Min") 172 | } 173 | ``` 174 | 175 | ### Expanded 176 | 177 | Развёрнутая Live Activity показывается, когда человек нажимает и удерживает компатный или минимальный вид. Когда Live Activity обновляется, развёрнутый вид появляется автоматически на пару секунд. 178 | 179 | ![Expanded Live Activity в Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-expanded.png) 180 | 181 | А вот код для развёрнутого вида. Каждое замыкание определяет область на Live Activity. 182 | 183 | ```swift 184 | DynamicIslandExpandedRegion(.center) {} 185 | DynamicIslandExpandedRegion(.leading) {} 186 | DynamicIslandExpandedRegion(.trailing) {} 187 | DynamicIslandExpandedRegion(.bottom) {} 188 | ``` 189 | 190 | Разметка областей: 191 | 192 | ![Области Dynamic Island.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-areas.png) 193 | 194 | - **center** — контент под камерой. 195 | - **leading** — пространство от левого угла до камеры. Если использовать вертикальный стек, будет доступно пространство ниже. 196 | - **trailing** — аналогично `leading`, но для правого края. 197 | - **bottom** — контент под всеми другими областями. 198 | 199 | Если контент в левой и правой областях не помещается, можно объединить его с `Bottom`. Область будет адаптивная, на скриншоте сейчас максимальные размеры: 200 | 201 | ![Если не хватает места, области Dynamic Island можно объединить.](https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-leading-expanded.png) 202 | 203 | Чтобы разрешить области использовать пространство ниже, укажите `verticalPlacement`: 204 | 205 | ```swift 206 | DynamicIslandExpandedRegion(.leading) { 207 | Text("Leading Text with merge region") 208 | .dynamicIsland(verticalPlacement: .belowIfTooWide) 209 | } 210 | ``` 211 | 212 | > Максимальная высота Live Activity на Dynamic Island 160 точек. 213 | 214 | # Как добавить новую Live Activity 215 | 216 | Live Activity можно создать только внутри приложения. Обновить и закончить Live Activity можно и внутри приложения, и по пуш-уведомлению. 217 | 218 | Сначала проверьте доступность Live Activity — пользователь мог запретить их. Вторая причина недоступности — в системе достигнут лимит. Чтобы проверить, используем код: 219 | 220 | ```swift 221 | guard ActivityAuthorizationInfo().areActivitiesEnabled else { 222 | print("Activities are not enabled") 223 | return 224 | } 225 | ``` 226 | 227 | Можно отслеживать статус: 228 | 229 | ```swift 230 | for await enabled in ActivityAuthorizationInfo().activityEnablementUpdates { 231 | // Здесь ваш код 232 | } 233 | ``` 234 | 235 | Чтобы создать новую Live Activity, создайте атрибуты и после вызовите `request`: 236 | 237 | ```swift 238 | let attributes = ActivityAttribute(...) 239 | let contentState = ActivityAttribute.ContentState(...) 240 | do { 241 | let activity = try Activity.request( 242 | attributes: attributes, 243 | contentState: contentState 244 | ) 245 | } catch { 246 | print("LiveActivityManager: Error in LiveActivityManager: \(error.localizedDescription)") 247 | } 248 | ``` 249 | 250 | Обратите внимание - здесь разделились статические и динамические проперти на два объекта. 251 | 252 | # Список активных Live Activity 253 | 254 | Чтобы получить уже созданные Live Activity, укажите модель атрибутов: 255 | 256 | ```swift 257 | for activity in Activity.activities { 258 | print("Activity details: \(activity.contentState)") 259 | } 260 | ``` 261 | 262 | # Обновить и завершить Live Activity 263 | 264 | Обновлять и завершать Live Activity можно только с динамическими параметрами — Content State. 265 | 266 | > Размер обновления Content State должен быть меньше 4KB. 267 | 268 | ## Внутри приложения 269 | 270 | Как обновить Live Activity из приложения: 271 | 272 | ```swift 273 | // Новые данные 274 | let contentState = ActivityAttribute.ContentState(...) 275 | 276 | Task { 277 | await activity?.update(using: contentState) 278 | } 279 | ``` 280 | 281 | Чтобы завершить Live Activity, вызовите: 282 | 283 | ```swift 284 | await activity?.end(dismissalPolicy: .immediate) 285 | ``` 286 | 287 | Live Activity закроется сразу. А вот как сделать, чтобы Live Activity осталась ещё некоторое время на экране: 288 | 289 | ```swift 290 | await activity?.end(using: attributes, dismissalPolicy: .default) 291 | ``` 292 | 293 | Live Activity обновится финальными данными и будет на экране ещё некоторое время. Система закроет активность через 4 часа или когда убедится, что пользователь увидел новые данные. Зависит от того, что наступит раньше. 294 | 295 | У Live Activity нет таймлайна, как для виджетов. Для обновления или закрытия Live Activity — когда приложение в фоне — используйте [Background Tasks](https://developer.apple.com/documentation/backgroundtasks). 296 | 297 | > Background Tasks не гарантируют своевременного выполнения. 298 | 299 | ## Через push-уведомления 300 | 301 | При создании Live Activity получаем `pushToken`. Он используется, чтобы обновлять Live Activity через пуш-уведомления. 302 | 303 | > Предварительно зарегистрируйте приложение для получения пушей. 304 | 305 | Сформируем пуш для обновления Live Activity. Заголовки: 306 | 307 | ``` 308 | apns-topic: {Your App Bundle ID}.push-type.liveactivity 309 | apns-push-type: liveactivity 310 | authorization: bearer {Auth Token} 311 | ``` 312 | 313 | Тело: 314 | 315 | ``` 316 | "aps": { 317 | "timestamp": 1168364460, 318 | "event": "update", // or end 319 | "content-state": { 320 | "dynamicStringValue": "New String Value" 321 | "dynamicIntValue": 5 322 | "dynamicBoolValue": true 323 | }, 324 | "alert": { 325 | "title": "Title of classic Push", 326 | "body": "Body or classic push", 327 | } 328 | } 329 | ``` 330 | 331 | Словарь `content-state` должен совпадать с моделью атрибутов `ActivityAttribute.ContentState`. Мы можем обновлять только динамические проперти. Проперти вне Content State обновить не получится. 332 | 333 | # Отследить нажатие на Live Activity 334 | 335 | По нажатию на Live Activity хорошо открывать релевантный экран, для этого реализуйте Deep Link. Установите модификатор `widgetURL(_:)`. Можно задать разные ссылки для каждой области: 336 | 337 | ```swift 338 | DynamicIslandExpandedRegion(.leading) { 339 | Text("Leading Text with merge region") 340 | .widgetURL(URL(string: "example://action")) 341 | } 342 | ``` 343 | 344 | Развёрнутый вид Dynamic Island поддерживает [Link](https://developer.apple.com/documentation/SwiftUI/Link). 345 | 346 | > Apple [отвечает на 10 вопросов](https://developer.apple.com/news/?id=qpqf1gru) про Live Activity. 347 | -------------------------------------------------------------------------------- /en/tutorials/meta/tutorials.json: -------------------------------------------------------------------------------- 1 | { 2 | "drag-and-drop": { 3 | "title": "Drag and Drop for table and collection", 4 | "description": "How to change the order of cells in a collection and a table. How to move cells to another collection. How to move multiple cells in a group.", 5 | "categories": ["uikit"], 6 | "author": "sparrowcode", 7 | "translators": ["svyatoynick"], 8 | "editors": ["ivanvorobei"], 9 | "graph_image": "https://cdn.sparrowcode.io/tutorials/drag-and-drop/preview.jpg", 10 | "google_structured_images": [ 11 | "https://cdn.sparrowcode.io/tutorials/drag-and-drop/drag-delegate.jpg", "https://cdn.sparrowcode.io/tutorials/drag-and-drop/drag-stack.jpg", "https://cdn.sparrowcode.io/tutorials/drag-and-drop/drop-delegate.jpg" 12 | ], 13 | "keywords": ["UICollectionViewDragDelegate", "UICollectionViewDropDelegate", "UITableViewDragDelegate", "UITableViewDropDelegate", "UIGestureRecognizer", "UIDrag"], 14 | "updated_date": "06.11.2023", 15 | "added_date": "26.08.2022", 16 | "is_private" : true 17 | }, 18 | "uisheetpresentationcontroller": { 19 | "title": "`UISheetPresentationController` as in the Maps application", 20 | "description": "In iOS 15, there are sheet-controllers. These are modal controllers that use a gesture to change height. You've seen these controllers in the «Maps» and «Stocks» apps.", 21 | "categories": ["uikit"], 22 | "author": "sparrowcode", 23 | "translators": ["svyatoynick"], 24 | "editors": ["ivanvorobei"], 25 | "graph_image": "https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/preview.png", 26 | "google_structured_images": [ 27 | "https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/edge-attached.png", "https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/grabber.png", "https://cdn.sparrowcode.io/tutorials/uisheetpresentationcontroller/corner-radius.png" 28 | ], 29 | "keywords": ["UISheetPresentationController", "Map", "Maps", "Modal Controllers", "iOS 15"], 30 | "updated_date": "06.11.2023", 31 | "added_date": "09.08.2022", 32 | "is_private" : true 33 | }, 34 | "sf-symbols-and-render-mode": { 35 | "title": "SF Symbols 4 and Render Mode", 36 | "description": "How `Monochrome`, `Hierarchical`, `Palette`, `Multicolor Render` work for SF Symbols. Code examples for UIKit and SwiftUI.", 37 | "categories": ["uikit"], 38 | "author": "sparrowcode", 39 | "translators": ["svyatoynick"], 40 | "editors": ["ivanvorobei"], 41 | "graph_image": "https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/preview.png", 42 | "google_structured_images": [ 43 | "https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/hierarchical-render.jpg", "https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/palette-render.jpg", "https://cdn.sparrowcode.io/tutorials/sf-symbols-and-render-mode/multicolor-render.jpg" 44 | ], 45 | "keywords": ["SF Symbols", "SFSymbols", "SwiftUI", "iOS 15"], 46 | "updated_date": "06.11.2023", 47 | "added_date": "03.08.2022", 48 | "is_private" : true 49 | }, 50 | "uiviewcontroller-lifecycle": { 51 | "title": "`UIViewController` Lifecycle", 52 | "description": "Consider when controller methods are called and what you can do inside them. When to configure views and data.", 53 | "categories": ["uikit"], 54 | "author": "sparrowcode", 55 | "translators": ["svyatoynick"], 56 | "editors": ["ivanvorobei"], 57 | "graph_image": "https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/hello.jpg", 58 | "google_structured_images": [ 59 | "https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/hello.jpg", "https://cdn.sparrowcode.io/tutorials/uiviewcontroller-lifecycle/header-en.jpg" 60 | ], 61 | "keywords": ["UIViewController", "viewDidAppear", "viewDidLoad", "uiviewcontroller lifecycle"], 62 | "updated_date": "06.11.2023", 63 | "added_date": "26.07.2022" 64 | }, 65 | "how-to-clean-userdefaults-and-realm-on-macos-catalyst": { 66 | "title": "How to clear UserDefaults and Realm for Mac Catalyst", 67 | "description": "How to clear data for Catalyst application including AppGroup, Realm and UserDefaults.", 68 | "categories": ["development"], 69 | "author": "sparrowcode", 70 | "translators": ["svyatoynick"], 71 | "editors": ["ivanvorobei"], 72 | "keywords": ["UserDefaults", "Catalyst", "MacCatalyst", "Realm", "AppGroup"], 73 | "updated_date": "06.11.2023", 74 | "added_date": "02.08.2022" 75 | }, 76 | "edge-insets-uibutton": { 77 | "title": "Edge Insets indents for `UIButton`", 78 | "description": "How to add an indent between text and picture in `UIButton`. How to place an icon to the right of the text.", 79 | "categories": ["uikit", "layout"], 80 | "author": "sparrowcode", 81 | "translators": ["svyatoynick"], 82 | "editors": ["ivanvorobei"], 83 | "graph_image": "https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/preview.png", 84 | "google_structured_images": [ 85 | "https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/content-edge-insets.png", "https://cdn.sparrowcode.io/tutorials/edge-insets-uibutton/depricated.png" 86 | ], 87 | "keywords": ["imageEdgeInsets", "imageEdgeInsets", "contentEdgeInsets"], 88 | "updated_date": "06.11.2023", 89 | "added_date": "28.07.2022", 90 | "is_private" : true 91 | }, 92 | "product-page-optimization-alternative-icons": { 93 | "title": "How to Add Alternative Icons for Product Page Optimization tests", 94 | "description": "How to add alternative icons for A/B tests on the app page in the App Store.", 95 | "categories": ["app-store-connect"], 96 | "author": "alxrguz", 97 | "translators": ["svyatoynick"], 98 | "editors": ["svyatoynick", "ivanvorobei"], 99 | "graph_image": "https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-icons-to-assets.png", 100 | "google_structured_images": [ 101 | "https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-icons-to-assets.png", "https://cdn.sparrowcode.io/tutorials/product-page-optimization-alternative-icons/adding-settings-to-target.png" 102 | ], 103 | "keywords": [], 104 | "updated_date": "25.07.2022", 105 | "added_date": "25.07.2022" 106 | }, 107 | "live-activities": { 108 | "title": "Live Activity & Dynamic Island", 109 | "description": "How to create, update, and end a Live Activity. The Live Activity interface. How to work with Dynamic Island.", 110 | "categories": ["swiftui", "extensions"], 111 | "author": "sparrowcode", 112 | "translators": ["svyatoynick"], 113 | "editors": ["ivanvorobei"], 114 | "keywords": ["Dynamic Island", "SwiftUI", "Live Activity", "WidgetKit"], 115 | "graph_image": "https://cdn.sparrowcode.io/tutorials/live-activities/preview.png", 116 | "google_structured_images": [ 117 | "https://cdn.sparrowcode.io/tutorials/live-activities/header.png", "https://cdn.sparrowcode.io/tutorials/live-activities/add-widget-target.png", "https://cdn.sparrowcode.io/tutorials/live-activities/shared-file-between-targets.png", "https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-compact.png", "https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-minimal.png", "https://cdn.sparrowcode.io/tutorials/live-activities/live-activity-type-expanded.png" 118 | ], 119 | "updated_date": "06.11.2023", 120 | "added_date": "21.10.2022", 121 | "is_private" : true 122 | }, 123 | "how-to-get-root-view-controller": { 124 | "title": "How to get a RootViewController", 125 | "description": "Example code for iOS 13 and above when scenes were added. And for iOS 12 and below when there were only windows. How to get root in SwiftUI.", 126 | "categories": ["uikit", "swiftui"], 127 | "author": "sparrowcode", 128 | "editors": ["ivanvorobei"], 129 | "keywords": ["RootViewController", "Root View Controller", "Controller", "Root View"], 130 | "graph_image": "https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindowscene.jpg", 131 | "google_structured_images": [ 132 | "https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindowscene.jpg", "https://cdn.sparrowcode.io/tutorials/how-to-get-root-view-controller/uiwindow.jpg" 133 | ], 134 | "updated_date": "06.11.2023", 135 | "added_date": "06.11.2023", 136 | "is_private" : true 137 | }, 138 | "custom-swiftui-modifier": { 139 | "title": "How to make a custom SwiftUI modifier", 140 | "description": "Example of a custom modifier. How to make modifier extension to call it natively.", 141 | "categories": ["swiftui", "extensions"], 142 | "author": "sparrowcode", 143 | "editors": ["ivanvorobei"], 144 | "keywords": ["modifiers", "swiftui", "custom modifier"], 145 | "google_structured_images": [], 146 | "updated_date": "14.11.2023", 147 | "added_date": "14.11.2023", 148 | "is_private" : true 149 | }, 150 | "set-launch-screen-via-plist": { 151 | "title": "Launch Screen without storyboard (via plist file)", 152 | "description": "Drop storyboard file and create Launch Screen via plist.", 153 | "categories": ["development"], 154 | "author": "sparrowcode", 155 | "editors": ["ivanvorobei"], 156 | "keywords": ["storyboard", "launch screen", "add Launch Screen in SwiftUI", "drop launch screen storyboard"], 157 | "graph_image": "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uiimagename-result.jpg", 158 | "google_structured_images": [ 159 | "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/delete-launchscreen-storyboard-file.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/delete-launch-screen-interface-file-base-name-key.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-uilaunchscreen-key.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-color-to-assets.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-background-color-launch-screen-key.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uicolorname-result.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uiimagename-result.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/add-uitabbar-key.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uitabbar-result.jpg", "https://cdn.sparrowcode.io/tutorials/set-launch-screen-via-plist/with-uitoolbar-result.jpg" 160 | ], 161 | "updated_date": "21.11.2023", 162 | "added_date": "21.11.2023", 163 | "is_private" : true 164 | }, 165 | "privacy-manifest": { 166 | "title": "What to add to Privacy Manifest", 167 | "description": "What to add to the Privacy Manifest, whether it is necessary to specify that third-party frameworks are used and how to fix errors.", 168 | "categories": ["development"], 169 | "author": "sparrowcode", 170 | "editors": [], 171 | "keywords": ["privacy manifest", "privacy", "manifest", "plist", "NSPrivacyAccessedAPICategoryUserDefaults"], 172 | "graph_image": "https://cdn.sparrowcode.io/tutorials/privacy-manifest/privacy-manifest-email.png", 173 | "google_structured_images": [], 174 | "updated_date": "16.04.2024", 175 | "added_date": "16.04.2024" 176 | }, 177 | "tipkit": { 178 | "title": "Using TipKit on UIKit & SwiftUI", 179 | "description": "How to add tooltips to the interface. Code examples on SwiftUI and UIKit.", 180 | "categories": ["development", "swiftui", "uikit"], 181 | "author": "sparrowcode", 182 | "editors": [], 183 | "keywords": ["tipkit", "hints", "tipkit on uikit", "tipkit framework"], 184 | "graph_image": "https://cdn.sparrowcode.io/tutorials/tipkit/tipkit-example.jpg", 185 | "google_structured_images": [], 186 | "updated_date": "05.05.2024", 187 | "added_date": "05.05.2024", 188 | "is_private" : true 189 | }, 190 | "testing-push-notifications-ios-simulator": { 191 | "title": "How to test Push Notifications on a simulator", 192 | "description": "Let's see how to test push notifications on the simulator, let's understand what apns file", 193 | "categories": ["development"], 194 | "author": "sparrowcode", 195 | "editors": [], 196 | "keywords": ["push", "notification", "manifest", "simulator", "apns"], 197 | "graph_image": "https://cdn.sparrowcode.io/tutorials/testing-push-notifications-ios-simulator/push.png", 198 | "google_structured_images": [], 199 | "updated_date": "15.05.2024", 200 | "added_date": "15.05.2024", 201 | "is_private" : true 202 | }, 203 | "cert-and-profile-for-personal-developer-account": { 204 | "title": "How to upload an app to an Individual Developer Account", 205 | "description": "In this article we will make the certificate and profile manually step by step - so the developer, who was added to the idnidivisual account, will be able to upload a build", 206 | "categories": ["development", "app-store-connect"], 207 | "author": "sparrowcode", 208 | "editors": [], 209 | "keywords": ["certificate", "profile", "p12", "provision profile", "apple distribution"], 210 | "graph_image": "https://cdn.sparrowcode.io/tutorials/cert-and-profile-for-personal-developer-account/main_page_certificates.png", 211 | "google_structured_images": [], 212 | "updated_date": "08.10.2024", 213 | "added_date": "08.10.2024" 214 | } 215 | } --------------------------------------------------------------------------------