Статьи

Кодирование для большого экрана с Apple TVOS SDK

Будущее ТВ — это приложения!

Это то, о чем заявил Тим Кук на недавнем мероприятии Apple в сентябре, и это будущее уже на нас, поскольку все больше телевизионного контента транслируется через мобильные устройства и приложения.

Недавно анонсированный tvOS SDK порадует разработчиков iOS. Впервые мы сможем создавать приложения для нового Apple TV и публиковать их в App Store. Это прекрасная возможность доставлять контент и удобное взаимодействие с пользователями через приложения на большой экран в гостиной каждого.

Давайте углубимся в это и посмотрим на SDK tvOS.

Мы рассмотрим основы каждого нового фреймворка, определим их назначение и опишем, какие требования предъявляются к приложению. Вооружившись нашими новыми знаниями, мы шаг за шагом создадим простое пользовательское приложение для tvOS.

Обзор SDK

Существует два типа приложений для TVOS.

Пользовательские приложения

Эти приложения используют новейшие платформы iOS и похожи на традиционные приложения iOS для iPhone и iPad. Вы можете использовать раскадровки или код для создания пользовательских интерфейсов.

Клиент-серверные приложения

Эти приложения используют новый набор фреймворков, уникальных для tvOS. Если вы веб-разработчик, вы найдете эти фреймворки интересными, поскольку они предоставляют возможность создавать приложения с использованием JavaScript и языка разметки. Да, вы правильно поняли, Apple впервые представила среду JavaScript в качестве средства для создания приложений.

Вот список всех новых фреймворков:

TVML

TVML — это пользовательский язык разметки Apple, который используется для создания интерфейсов. Он имеет стиль XML, и если вы знаете HTML, вы найдете его знакомым.

Apple сделала доступным набор повторно используемых шаблонов TVML, чтобы помочь вам начать работу. Вы можете найти подробное описание этих шаблонов здесь .

TVJS

Как следует из названия, TVJS — это набор API-интерфейсов JavaScript, используемых для загрузки страниц TVML. Он содержит необходимый функционал для взаимодействия с оборудованием Apple TV. Из того, что я вижу, это стандартный JavaScript без какого-либо специального синтаксиса.

Дополнительную информацию можно найти в справочной документации по TVJS Framework .

TVMLKit

Это основная структура, где все объединяется. TVMLKit предоставляет способ включить элементы TVJS, JavaScript и TVML в ваше приложение. Думайте об этой среде как о контейнере и мосте для доставки кода и разметки как о собственном приложении для Apple TV.

Посмотрите Ссылку Платформы TVMLKit для получения дополнительной информации.

TVServices

TVServices — это фреймворк, позволяющий добавить расширение верхней полки в ваше приложение. Верхняя полка — это область главного экрана Apple TV, предназначенная для отображения контента из приложения, размещенного в первом ряду. Он предоставляет возможность отображать контент и информацию пользователю, когда фокус находится на значке приложения, без необходимости запуска приложения.

TopSelf

См. Ссылку Платформы TVServices .

Архитектура клиент-серверных приложений

Все вышеперечисленные платформы являются частью клиентской части приложения. Для того, чтобы приложение работало, вам потребуется также сторона сервера. Сервер будет содержать вашу разметку TVML, код JavaScript и ваши данные. TVMLKit в вашем упакованном приложении tvOS будет обрабатывать поиск и отображение данных.

Мне нравится думать об этой архитектуре как о типичном веб-сайте, где все файлы HTML и JS находятся на веб-сервере, а приложение, отвечающее за их отображение, похоже на браузер пользователя.

Вопросы развития

Прежде чем переходить в Xcode для создания приложения, важно учесть следующие ограничения:
— Размер приложения ограничен 200 МБ.
— Вы не можете сохранить данные на устройстве, CloudKit — это то, с чем вам нужно ознакомиться, если вам нужно сохранить пользовательские настройки.

Важно понимать, что у Apple TV появился новый пользовательский интерфейс, и Apple определила набор рекомендаций по интерфейсу для людей, специфичных для tvOS.

Вы должны знать, что в отличие от iOS-устройств пользователь имеет ограниченные сенсорные возможности (смахивание, нажатие и касание), которые влияют на то, как они могут взаимодействовать с вашим приложением. Это то, что делает фокусировку важной для Apple TV, и, учитывая, что это новая концепция, я настоятельно рекомендую вам потратить некоторое время на изучение поддержки фокусировки в вашем приложении .

Создайте свое первое приложение tvOS

Время повеселиться с TvOS SDK.

Мы создадим приложение, которое вызывает API-интерфейс базы данных ТВ, извлекает список популярных телешоу и отображает их в нашем приложении для ТВ.

К тому времени, когда мы закончим этот урок, приложение будет выглядеть так:

образ

Настройка среды разработки

Разработка для Apple TV требует Xcode 7.1, который все еще находится в бета-версии. Как и все, что находится в стадии разработки, вы можете столкнуться с неожиданными сбоями и низкой производительностью.

Загрузите Xcode 7.1 здесь

Мы собираемся использовать API themoviedb.org и, в частности, следующий запрос API, который возвращает данные json, содержащие список популярных телевизионных шоу. Вы можете увидеть информацию о запросе здесь

API требует ключ, который вы можете запросить бесплатно, но вы должны сначала зарегистрироваться.

Разработка приложения

Откройте Xcode и выберите новое приложение Single view в разделе tvOS.

образ

Дайте приложению имя, я назвал мои PopularTVShows , выберите местоположение для файлов и нажмите « Создать» .

образ

Разработка пользовательского интерфейса

Выберите основную раскадровку, вы заметите, что это очень большая область раскадровки. Это потому, что теперь нам нужно создать интерфейс для Full HD на телевизоре.

Выберите вид по умолчанию, и вы заметите, что по умолчанию он имеет разрешение 1920 x 1080.

образ

Помимо этой разницы, все остальное должно быть знакомо.

Возможно, вы захотите уменьшить масштаб, чтобы увидеть полную раскадровку.

Начните с перетаскивания представления коллекции на раскадровку, поместите его в верхний левый угол и добавьте ограничения нулевого поля на всех 4 сторонах.

образ

образ

Нажмите на обновление кадров .

образ

Представление коллекции теперь займет все место на раскадровке.

На большинстве устройств iOS прокрутка, когда имеется больше данных, которые могут уместиться на экране, обычно осуществляется вертикально. Для Apple TV рекомендуемое направление прокрутки — горизонтальное. Установите это в представлении коллекции.

образ

Затем выберите Ячейку Представления Коллекции и дайте ей нестандартный размер 260 на 430.

образ

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

В инспекторе атрибутов назначьте идентификатор ShowCell .

образ

Добавьте представление изображения в ячейку и назовите его ShowImg

образ

Добавьте следующие ограничения: верхнее поле 0, ширина 225, высота 354 и выровняйте по горизонтали в контейнере.

образ

образ

Перетащите метку под представление «Изображение» и установите для нее следующие ограничения: верхнее поле 20, ширина 225, высота 35

образ

образ

Выровняйте горизонтально в контейнере.

образ

Установите выравнивание текста по центру и измените его на любое понравившееся вам телешоу. Я выбрал «Доктор Кто».

образ

Установите вид изображения, чтобы использовать постер по вашему выбору или тот, который я использовал здесь

Загрузите изображение и перетащите его в каталог активов вашего проекта.

образ

Дайте ему соответствующее имя, я назвал свой плакат background_.

Вернитесь к раскадровке, выберите вид изображения и установите его в качестве фона в инспекторе свойств.

образ

Это все, что нам нужно для нашего пользовательского интерфейса, если вы не выполнили до сих пор или не смогли выполнить какой-либо шаг, загрузите копию кода из ветви пользовательского интерфейса здесь

Получение данных

Время делать наши запросы API и получать данные в нашем приложении.

Сериализация и десериализация json с помощью Swift могут занять много времени из-за необязательных значений. Мне нравится использовать фреймворк под названием SwiftyJSON . Это значительно упрощает процесс работы с json и предоставляет оболочку, которая отнимает много кода.

Загрузите копию фреймворка и убедитесь, что вы находитесь на ветке Xcode 7.

образ

Самый простой способ интегрировать библиотеку в проект — перетащить файл SwiftyJSON.swif в проект, выбрав цель проекта.

образ

образ

Большой! Теперь мы готовы добавить немного кода.

Добавьте новый файл Swift в проект и назовите его ApiService .

class ApiService { static let sharedInstance = ApiService() func apiGetRequest(path: String, onCompletion: (JSON, NSError?) -> Void) { let request = NSMutableURLRequest(URL: NSURL(string: path)!) let session = NSURLSession.sharedSession() let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in let json:JSON = JSON(data: data!) onCompletion(json, error) }) task.resume() } } 

В коде есть функция, которая создает сетевую задачу с запросом, она принимает URL-адрес в качестве параметра и возвращает данные в случае успеха или ошибку, если нет.

Эта функция является общей и может использоваться для каждого запроса API, который будет делать приложение.

Давайте посмотрим на API. Чтобы получить доступ к данным, мы делаем запрос GET по следующему URL-адресу: « http://api.themoviedb.org/3/tv/popular?api_key= », включая ключ в конце.

Добавьте следующую строку кода вверху класса.

 let API_TV_POPULAR_URL = "http://api.themoviedb.org/3/tv/popular?api_key=YourKeyHere" 

YourKeyHere замените YourKeyHere на ваш ключ API.

Отлично, теперь давайте добавим еще одну функцию, которая берет этот URL-адрес и передает его обобщенной функции, которую мы создали ранее, которая затем должна вернуть данные или ошибку из запроса.

 func getPopularTVShows(onCompletion: (JSON, NSError?) -> Void) { apiGetRequest(API_TV_POPULAR_URL, onCompletion: { json, err in onCompletion(json as JSON, err as NSError?) }) } 

Это все, что нам нужно для этого класса. Теперь это должно выглядеть так:

 import Foundation let API_TV_POPULAR_URL = "http://api.themoviedb.org/3/tv/popular?api_key=yourKeyHere" class ApiService { static let sharedInstance = ApiService() func apiGetRequest(path: String, onCompletion: (JSON, NSError?) -> Void) { let request = NSMutableURLRequest(URL: NSURL(string: path)!) let session = NSURLSession.sharedSession() let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in let json:JSON = JSON(data: data!) onCompletion(json, error) }) task.resume() } func getPopularTVShows(onCompletion: (JSON, NSError?) -> Void) { apiGetRequest(API_TV_POPULAR_URL, onCompletion: { json, err in onCompletion(json as JSON, err as NSError?) }) } } 

Давайте создадим наши объекты данных, это классы, которые будут содержать данные, полученные от json. Мы будем использовать эти объекты для заполнения элементов интерфейса приложения.

Если вы откроете браузер и перейдете по URL-адресу, добавленному в приведенном выше коде, вы получите данные json, с которыми мы будем работать. Это должно выглядеть так.

 { "page": 1, "results": [ { "backdrop_path": "/aKz3lXU71wqdslC1IYRC3yHD6yw.jpg", "first_air_date": "2011-04-17", "genre_ids": [ 10765, 18 ], "id": 1399, "original_language": "en", "original_name": "Game of Thrones", "overview": "Seven noble families fight for control of the mythical land of Westeros. Friction between the houses leads to full-scale war. All while a very ancient evil awakens in the farthest north. Amidst the war, a neglected military order of misfits, the Night's Watch, is all that stands between the realms of men and icy horrors beyond.\n\n", "origin_country": [ "US" ], "poster_path": "/jIhL6mlT7AblhbHJgEoiBIOUVl1.jpg", "popularity": 36.072708, "name": "Game of Thrones", "vote_average": 9.1, "vote_count": 273 }, { "backdrop_path": "/kohPYEYHuQLWX3gjchmrWWOEycD.jpg", "first_air_date": "2015-06-12", "genre_ids": [ 878 ], "id": 62425, "original_language": "en", "original_name": "Dark Matter", "overview": "The six-person crew of a derelict spaceship awakens from stasis in the farthest reaches of space. Their memories wiped clean, they have no recollection of who they are or how they got on board. The only clue to their identities is a cargo bay full of weaponry and a destination: a remote mining colony that is about to become a war zone. With no idea whose side they are on, they face a deadly decision. Will these amnesiacs turn their backs on history, or will their pasts catch up with them?", "origin_country": [ "CA" ], "poster_path": "/iDSXueb3hjerXMq5w92rBP16LWY.jpg", "popularity": 27.373853, "name": "Dark Matter", "vote_average": 6.4, "vote_count": 4 } ], "total_pages": 3089, "total_results": 61761 } 

При более внимательном рассмотрении вы можете увидеть, что у нас есть структура, содержащая следующее на верхнем уровне:
— Номер страницы.
— Массив результатов.
— Общее количество страниц.
— итоговые результаты.

Давайте создадим класс, который будет представлять эту структуру данных. Добавьте новый файл Swift и назовите его ApiResults .

Добавьте следующий код

 class ApiResults { var page : Int! var results : [ApiTVResult]! var totalPages : Int! var totalResults : Int! /** * Instantiate the instance using the passed json values to set the properties values */ init(fromJson json: JSON!){ if json == nil{ return } page = json["page"].intValue results = [ApiTVResult]() let resultsArray = json["results"].arrayValue for resultsJson in resultsArray{ let value = ApiTVResult(fromJson: resultsJson) results.append(value) } totalPages = json["total_pages"].intValue totalResults = json["total_results"].intValue } } 

Это создает переменные page , totalPages и totalResults виде целых чисел, поскольку они будут содержать числовые значения json. Это также создает массив результатов.

Код инициализации берет данные json и присваивает значения соответствующим переменным.

Вы, вероятно, заметили, что у нас есть объект ApiTVResult объявленный и сопоставленный с массивом results . Это будет наш второй объект, который будет содержать данные каждого результата, который в данном случае является подробностями телешоу.

Добавьте новый файл Swift и назовите его ApiTVResult .

Добавьте следующий код

 let imagesBasePath = "http://image.tmdb.org/t/p/w500" class ApiTVResult { var backdropPath : String! var firstAirDate : String! var genreIds : [Int]! var id : Int! var originalLanguage : String! var originalName : String! var overview : String! var originCountry : [Int]! var posterPath : String! var popularity : Float! var name : String! var voteAverage : Float! var voteCount : Int! /** * Instantiate the instance using the passed json values to set the properties values */ init(fromJson json: JSON!){ if json == nil{ return } let apiBackDropPath = json["backdrop_path"].stringValue backdropPath = "\(imagesBasePath)\(apiBackDropPath)" firstAirDate = json["first_air_date"].stringValue genreIds = [Int]() let genreIdsArray = json["genre_ids"].arrayValue for genreIdsJson in genreIdsArray { genreIds.append(genreIdsJson.intValue) } id = json["id"].intValue originalLanguage = json["original_language"].stringValue originalName = json["original_name"].stringValue overview = json["overview"].stringValue originCountry = [Int]() let originCountryArray = json["origin_country"].arrayValue for originCountryJson in originCountryArray { originCountry.append(originCountryJson.intValue) } let apiPosterPath = json["poster_path"].stringValue posterPath = "\(imagesBasePath)\(apiPosterPath)" popularity = json["popularity"].floatValue name = json["name"].stringValue voteAverage = json["vote_average"].floatValue voteCount = json["vote_count"].intValue } } 

Подход здесь такой же, как и раньше, мы создали переменные, соответствующие данным json, и добавили код инициализации для заполнения объекта. Глядя на полученные данные, posterPath и posterPath содержит только имя изображения. Чтобы создать полный URL-адрес изображения, нам нужно объединить имя файла с imagesBasePath следующим образом:

 let apiBackDropPath = json["backdrop_path"].stringValue backdropPath = "\(imagesBasePath)\(apiBackDropPath)" 

Везде, где у нас есть массив значений, таких как var genreIds : [Int]! мы перебираем каждое значение и добавляем его в массив следующим образом:

 let genreIdsArray = json["genre_ids"].arrayValue for genreIdsJson in genreIdsArray { genreIds.append(genreIdsJson.intValue) } 

Это все, что нам нужно сделать для нашей модели данных.

Код выше можно найти в Модельной ветке здесь

Собираем все вместе

Давайте подключим пользовательский интерфейс к нашему коду и сделаем вызовы API для извлечения данных.

Во-первых, мы собираемся создать класс, который позволит нам заполнить нашу ячейку коллекции. Создайте новый файл Swift и назовите его ShowCell .

Добавьте следующий код

 import UIKit class ShowCell: UICollectionViewCell { @IBOutlet weak var showImg: UIImageView! @IBOutlet weak var showLbl: UILabel! func configureCell(tvShow: ApiTVResult) { if let title = tvShow.name { showLbl.text = title } if let path = tvShow.posterPath { let url = NSURL(string: path)! dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { let data = NSData(contentsOfURL: url)! dispatch_async(dispatch_get_main_queue()) { let img = UIImage(data: data) self.showImg.image = img } } } } } 

Этот код создает выход для плаката телешоу, а другой — для заголовка телешоу с именами showImg и showLbl соответственно.

Он содержит функцию, которая принимает объект ApiTVResult , назначает заголовок метке и использует URL плаката для асинхронной загрузки изображения и заполняет imageView .

Теперь нам нужно подключить розетки к коду. Откройте раскадровку, выберите showCell и установите класс, который мы только что создали

образ

Щелкните правой кнопкой мыши ShowCell и подключите розетку showImg , выбрав + рядом с названием розетки и перетащив к изображению плаката. Повторите шаг, описанный выше, чтобы подключить выход showlbl к этикетке заголовка

образ

Это все, что требуется для ShowCell .

Теперь давайте подключим CollectionView к ViewController .

Перейдите в View Controller и добавьте следующую строку кода вверху класса:

 @IBOutlet weak var collectionView: UICollectionView! 

Вернитесь к раскадровке, щелкните правой кнопкой мыши по коллекции, выберите + рядом с новым выходом ссылок и перетащите на желтый значок View Controller. Выберите viewCollection .

Это завершает все необходимые ссылки, поэтому выберите класс ViewController и замените следующий код

 class ViewController: UIViewController { 

с этим

 class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 

Мы просто добавили несколько протоколов в CollectionView , это минимально необходимые реализации для добавления данных и управления макетом представления.

Давайте реализуем их. Начните с удаления следующего кода, так как мы не будем выполнять с ним никаких действий.

 override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } 

Замените следующий код:

  override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } 

с:

  override func viewDidLoad() { super.viewDidLoad() collectionView.delegate = self collectionView.dataSource = self } 

Мы заявили, что ViewController будет выступать в качестве источника данных и делегата для CollectionView .

Прежде чем мы реализуем наши протоколы, мы должны создать функцию, которая будет вызывать API и заполнять наши объекты, таким образом, мы можем назначить соответствующие данные для CollectionView .

Первое, что нам нужно, это переменная, которая будет содержать наши данные, создайте ее, добавив следующий код под IBOutlet

 var tvShows = [ApiTVResult]() 

Эта переменная будет содержать массив телешоу.

Создайте метод для вызова API, используя ранее созданный ApiService.

 func downloadData () { ApiService.sharedInstance.getPopularTVShows {JSON, NSError in if NSError != nil { print(NSError!.debugDescription) } else { let apiResults = ApiResults(fromJson: JSON) self.tvShows = apiResults.results dispatch_async(dispatch_get_main_queue()) { self.collectionView.reloadData() } } } } 

В приведенном выше коде после успешного вызова API и получения данных json мы заполняем объекты ApiResuts . Затем мы присваиваем переменным viewController данные, содержащиеся в результатах.

Здесь вы можете увидеть, как библиотека SwiftyJSON помогает в сериализации и заполнении объектов.

Вызовите этот метод в функции viewDidLoad . Ваш код должен выглядеть так:

  override func viewDidLoad() { super.viewDidLoad() collectionView.delegate = self collectionView.dataSource = self downloadData() } func downloadData () { ApiService.sharedInstance.getPopularTVShows {JSON, NSError in if NSError != nil { print(NSError!.debugDescription) } else { let apiResults = ApiResults(fromJson: JSON) self.tvShows = apiResults.results dispatch_async(dispatch_get_main_queue()) { self.collectionView.reloadData() } } } } 

Теперь мы можем реализовать необходимые протоколы UICollectionView . Добавьте следующий код в ваш файл:

 func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCellWithReuseIdentifier("ShowCell", forIndexPath: indexPath) as? ShowCell { let tvShow = self.tvShows[indexPath.row] cell.configureCell(tvShow) return cell } else { return ShowCell() } } func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { return 1 } func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return tvShows.count } func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { return CGSizeMake(260, 430) } 

Код здесь является стандартным для реализации протокола.

В numberOfSectionsInCollectionView мы возвращаем 1, поскольку у нас будет только одна категория в пользовательском интерфейсе.

В numberOfItemsInSection мы возвращаем количество TvShows в данных. По сути, мы говорим, что хотим отображать столько элементов, сколько TVShows вернуло из API.

В макете мы устанавливаем размер нашего элемента в соответствии с размером showcell в раскадровке.

Наконец, в том, что выглядит как наиболее сложный фрагмент кода в cellForItemAtIndexPath , мы создаем ячейку на ShowCell класса ShowCell мы создали ранее. Мы выбираем TVShow из массива tvShows на основе его индекса и выполняем его настройку для отображения необходимых данных.

Теперь о захватывающей части … Давайте запустим приложение!

К сожалению …

образ

iOS 9 теперь требует явного использования протокола HTTPS для передачи данных в приложениях. Поскольку наши вызовы API в настоящее время являются HTTP, мы должны включить его в настройках приложения.

Откройте файл Info.plist и нажмите кнопку + на последнем элементе. Выберите запись настроек безопасности транспорта приложения.

образ

Установите для параметра Разрешить произвольные загрузки значение Да .

образ

Это позволит все типы соединений для приложения.

Давай бегать снова …

Если вам удалось следовать за вами, теперь вы должны увидеть успех! Приложение отображает ТВ-шоу с соответствующими постерами и заголовками.

Но все еще чего-то не хватает.

На Apple TV важен фокус, и навигация между элементами в приложении требует этого.

Давайте реализуем одну последнюю функцию, которая поможет с этим.

Добавьте следующий код в верхней части ViewController (под var TVShows )

 let originalCellSize = CGSizeMake(225, 354) let focusCellSize = CGSizeMake(240, 380) 

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

 override func didUpdateFocusInContext(context: UIFocusUpdateContext, withAnimationCoordinator coordinator: UIFocusAnimationCoordinator) { if let previousItem = context.previouslyFocusedView as? ShowCell { UIView.animateWithDuration(0.2, animations: { () -> Void in previousItem.showImg.frame.size = self.originalCellSize }) } if let nextItem = context.nextFocusedView as? ShowCell { UIView.animateWithDuration(0.2, animations: { () -> Void in nextItem.showImg.frame.size = self.focusCellSize }) } } 

Запустите приложение еще раз и проведите пальцем по телевизору, чтобы увидеть, какой элемент сейчас выбран. Это завершает нашу заявку.

Полная копия кода может быть найдена здесь

Последние мысли

Опытные разработчики iOS найдут знакомое создание приложений для tvOS. Есть небольшая кривая обучения, особенно вокруг дизайна пользовательского интерфейса и взаимодействия с пользователем, но оно того стоит. Для тех, кто заинтересован в обслуживании мультимедиа и большого контента, приложения TVML предоставляют простой способ сделать это, используя знакомые веб-технологии.

Для новичков в разработке для iOS или для мобильных устройств нет лучшего времени для прыжка. Навыки, которые вы приобретете при разработке пользовательских приложений для tvOS, позже будут перенесены на iOS.

Apple TV предоставит новую платформу для пользовательских приложений и, скорее всего, будет иметь собственный независимый магазин приложений. Не каждый день такая возможность предоставляется разработчикам.

У меня уже есть список приложений, которые я бы хотел использовать на большом экране своего телевизора, и я с нетерпением жду возможности насладиться контентом и опытом, которые вы предоставите, держа пульт Siri в одной руке и немного попкорна в другой.