Воспользуйтесь простотой документации Tenor API для создания ячеек воспроизведения GIF.

Tenor, популярная поисковая машина GIF, является отличной конечной точкой API, с помощью которой можно продемонстрировать, как реализовать классы горизонтальной и вертикальной прокрутки Swift с встроенной функциональностьюAVFoundation в Objective-C!

Благодаря обширной документации, краткому руководству по началу работы, передовым практикам и подробным curl командам Tenor предлагает широкие возможности для разработки приложений для iOS.

Это также отлично подходит для тех, кто забывает о сложности интеграции API для простоты SDK!



Что вы добьетесь

В этом блоге вы воспользуетесь элементарной простотой документации Tenor API для создания ячеек воспроизведения GIF с помощью класса UICollectionViewCells Swift.

В частности, вы покажете следующее:

  • Знание как Swift, так и Objective-C, где класс представления находится в Swift, а класс видеоплеера - в Objective-C.
  • Родной iOS UIKit (раскадровка, автоматический макет и классы размеров).
  • Надлежащие сторонние сети или фреймворки для кэширования изображений, например _4 _ / _ 5_.

Показывая эти вещи, вы добьетесь следующего:

  • Получите данные конечной точки, отобразите их в виде коллекции.
  • Панель поиска, которая обновляет представление коллекции по мере ввода пользователем.
  • При нажатии на ячейку представления коллекции приложение начинает воспроизведение видео.
  • Правильное отображение на iPhone и iPad во всех четырех ориентациях.
  • Модульные тесты.

Настраивать

Откройте шаблон приложения с одним представлением в Xcode. Перейдите в корневой каталог через терминал интерфейса командной строки.

В корневом каталоге напишите pod init. После инициализации модуля добавьте Alamofire и SDWebImage в свой подфайл и запустите pod install.

Вот и все! Все готово.

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

Следующим шагом является получение данных конечной точки. По мере того, как вы создаете свое приложение в соответствии с представлением модели, представлением структуры модели, вы создадите четыре structs, каждое из которых соответствует новому протоколу Apple codable.

Первая структура - это структура GIF.

GIF struct

В своем GIF struct объявите следующие константы: tags: [String]?, url: URL?, media: [MediaCollection]?, created: Double?, shares: Int?, itemURL: URL?, hasAudio: Bool?, title: String?, id: String? и ключи кодирования.

Для наших ключей кодирования вы будете использовать enum с регистрами:

enum CodingKeys: String, CodingKey {
    case tags, url, media, created, shares
    case itemURL = "itemURL"
    case hasAudio = "has audio"
    case title, id
}

MediaCollection struct

Ваш следующий struct называется MediaCollection. Это намного проще, чем другая структура, поскольку она просто декодирует различные типы мультимедиа в тип Swift.

В вашем MediaCollection struct объявите следующие константы: nanoMP4, nanoWebM, tinyGIF, tinyMP4, tinyWebM, webM, gif, mp4, loopedMP4, mediumGIF и nanoGIF, как необязательный тип Media.

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

СМИ struct

Ваша следующая структура называется Media.

В своей структуре Media объявите следующие константы: url: URL?, dimension: [Int]?, duration: Double?, preview: URL?, size: Int64?.

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

Ответ struct

И последнее, но не менее важное - это ваша структура Response. Вы объявите эту структуру в общем виде, чтобы Response можно было декодировать тогда и только тогда, когда она соответствует декодируемому протоколу.

Объявите константы для webURL:URL?, results: T? и next: String?.

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

Потрясающие! Вы создали свои структуры. Это первый шаг к получению данных конечной точки. Следующим шагом является настройка вызовов конечных точек API.

Конечные точки

Создайте struct для URLManager, который будет управлять методами convertItems, getURL, getAuthenticationParameters, getLimitingParameters и getAnonymousIdParameters.

Запрограммируйте convertItems как частную константу:

static private let convertItems: ((Parameters) -> [URLQueryItem]) = { 
parameters in return parameters.map { 
      return URLQueryItem(name: $0, value: $1) 
     }    
}

Программа ТЗ getURL как общедоступный метод:

Программа getAuthenticationParameters как частный метод:

static private func getAuthenticationParameters() -> Parameters {            
    return ["key"   : Configuration.key]    
}

Программа getAnonymousIdParameters как частный метод:

static private func getAnonymousIdParameters() -> Parameters? {        
    guard let anonymoudId = UserDefaults.standard.string(forKey: kAnonymousIdKey) 
    else { return nil }        
    return ["anon_id"   : anonymoudId]
}

Программа getLimitingParameters как частный метод:

static private func getLimitingParameters() -> Parameters {          
    return ["limit" : "\(Configuration.pageLimit)"]    
}

И последнее, но не менее важное: в этом списке нужно запрограммировать enum с парой случаев:

import Foundation 
enum EndPoint: String {        
case anonymousId    =   "/v1/anonid"        
case search         =   "/v1/search"
}

Вам нужно добавить еще два файла: один с именем Alias.swift, а другой с именем Constants.swift. В первом запрограммируйте typealias как:

typealias Parameters = [String:String]

Во втором запрограммируйте константу kAnonymousIdKey как AnonymousId. Сгруппируйте эти последние два файла в одну папку под названием Helper.

Панель поиска

Наша архитектура для реализации панели поиска состоит из двух частей: во-первых, это контроллер представления с именем SearchVC.swift, во-вторых, расширение категории с именем SearchVC+CollectionView.swift.

Добавьте файл с именем SearchVC.swift. Внутри файла импортируйте:

import UIKit
import Alamofire
import AlamofireImage

Создать торговые точки:

@IBOutlet weak var searchBar: UISearchBar!        
@IBOutlet weak var resultCollectionView: UICollectionView!

Создайте свойства для данных:

// MARK: - Data        
internal lazy var resultsArray: [GIF] = []        
internal let cellIdentifier = "PreviewCell"        
internal let cellHeight: CGFloat = 250        
private let anonIdViewModel = AnonIdViewModel()        
private let searchViewModel = SearchViewModel()

Звоните fetchAnonymousId() в viewDidLoad().

Создайте функцию для получения анонимных идентификаторов:

private func fetchAnonymousId() {                
    ActivityIndicator.startAnimating()                  
    anonIdViewModel.getAnonymousId { 
[weak self] success in                        
    ActivityIndicator.stopAnimating()                        
         if success {                
             self?.fetchResult()            
             } else {
                self?.showRetryAlert()
            }
        }
    }

Если наш вызов будет успешным, вы получите результаты в функции с именем fetchResult().

private func fetchResult(for keyword: String = "") {                
ActivityIndicator.startAnimating()                
searchViewModel.search(using: keyword) 
{ [weak self] (data, error) in                        
DispatchQueue.main.async {                
    if let error = error {
                    // if canceled, do not show alert
                    guard (error as NSError).code != -999 else { return }
                    self?.showErrorAlert(with: error.localizedDescription)
                }
                else if let data = data {                      self?.resultsArray = data                    self?.resultCollectionView.reloadData()
                }                                ActivityIndicator.stopAnimating()
            }
        }
    }

Запрограммируйте вызов предупреждения об ошибке:

private func showErrorAlert(with message: String) {                
let alertController = UIAlertController(title: "Error",                                                message: message,                                            preferredStyle: .alert)
                let okayAction = UIAlertAction(title: "Okay", style: .default)
        alertController.addAction(okayAction)                present(alertController, animated: true, completion: nil)
    }}

Если вызов не удался, запрограммируйте функцию с именем showRetryAlert():

private func showRetryAlert() {
                let alertController = UIAlertController(title: "Error",                                                message: "Unable to fetch anonymous id.",
                                                preferredStyle: .alert)
                let retryAction = UIAlertAction(title: "Retry", style: .default) { [weak self] _ in            
self?.fetchAnonymousId()        }        
alertController.addAction(retryAction)
                let withoutAction = UIAlertAction(title: "Search without anonymous id.", style: .default) { [weak self] _ in            
self?.fetchResult()        }        
alertController.addAction(withoutAction)                
present(alertController, animated: true, completion: nil)
    }

Запрограммируйте изменение ориентации:

@objc 
func orientationDidChange() { resultCollectionView.collectionViewLayout.invalidateLayout()
}

Кроме того, вам необходимо запрограммировать обновление коллекции признаков:

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {        resultCollectionView.collectionViewLayout.invalidateLayout()    
}

Наблюдатель за ориентациями

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

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

override func viewDidAppear(_ animated: Bool) {           
    super.viewDidAppear(animated)                  
    NotificationCenter.default.addObserver(self, selector: #selector(SearchVC.orientationDidChange), name: Notification.Name.UIDeviceOrientationDidChange, object: nil)    
}        
override func viewDidDisappear(_ animated: Bool) {          
    super.viewDidDisappear(animated)                
    NotificationCenter.default.removeObserver(self)    
}

Запрограммируйте UISearchBarDelegate

Ранее вы добавляли экземпляр UISearchBar в качестве розетки.

Однако экземпляры UISearchBar должны делегировать свои функции контроллеру, в котором они объявлены.

Вот как расширить функциональные возможности контроллера представления поиска для обработки функциональности панели поиска:

extension SearchVC: UISearchBarDelegate {        
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {      
          
fetchResult(for: searchText) 
   
   }

Этот код добавляется в нижнюю часть контроллера панели поиска.

Запрограммируйте SearchVC + CollectionView.swift

SearchVC+CollectionView.swift является расширением SearchVC, но в отдельном файле, чтобы код представления коллекции контроллера представления был хорошо организован (т. Е. Разделение задач).

Первая часть - принять протокол UICollectionViewDataSource, реализовав необходимые методы для количества элементов в каждом разделе и ячеек для каждого элемента в этих разделах:

extension SearchVC: UICollectionViewDataSource {
     func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
          return resultsArray.count
    }
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? PreviewCell
        if let url = resultsArray[indexPath.row].media?.first?.mp4?.preview {
             cell?.thumbImageView.af_setImage(withURL: url,
placeholderImage:  imageLiteral(resourceName: "placeholder"),imageTransition: .crossDissolve(0.1))
        }
        return cell ?? UICollectionViewCell()
     }
}

Следующим шагом является реализация делегата представления коллекции:

extension SearchVC: UICollectionViewDelegate {     
   func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {   
             guard let url = resultsArray[indexPath.row].media?.first?.mp4?.url else { return }                let _ = VideoPlayer.playVideo(with: url)    
             }
}

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

extension SearchVC: UICollectionViewDelegateFlowLayout {
        func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {          
      switch UIDevice.current.userInterfaceIdiom {    
      case .carPlay, .tv, .unspecified: fallthrough        
      case .phone:            
          return CGSize(width: collectionView.frame.width, height: cellHeight)        
      case .pad: 
        return CGSize(width: collectionView.frame.width/2, height: cellHeight)        
      }     
    }
}

Воспроизведение видео

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

AVFoundation - это собственный SDK Apple для воспроизведения видео, но он написан на Objective-C. Это не написано на Swift.

Хотя мы можем реализовать это в Swift, нет никаких преимуществ, поскольку реализация в Objective-C ближе к ядру AVFoundation, чем Swift.

Создайте два файла Objective-C: один VideoPlayer.h, другой VideoPlayer.m.

В заголовочный файл добавьте следующее:

////  VideoPlayer.h
//  Tenorize
////  Created by Eric Giannini on 8/29/18.
//  Copyright © 2018 Eric Giannini. All rights reserved.
// 
#import <AVKit/AVKit.h>
#import <Foundation/Foundation.h> 
@interface VideoPlayer : NSObject
 +(nonnull AVPlayerViewController *)playVideoWithURL:(NSURL *_Nonnull)url; 
@end

Вы объявляете метод в файле заголовка, но фактически программируете этот метод в файле реализации.

Вот:

#import "VideoPlayer.h"
#import <Foundation/Foundation.h> 
@implementation VideoPlayer
+(AVPlayerViewController *)playVideoWithURL:(NSURL *)url {
   AVPlayer *player = [[AVPlayer alloc] initWithURL:url];
   AVPlayerViewController *playerVC = [[AVPlayerViewController alloc] init];    
   [playerVC setPlayer:player];
   UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
   [[window rootViewController] presentViewController:playerVC animated:YES completion:^{        
   [player play];    
}];
    return playerVC;
}
@end

Ваш метод вызывается с URL-адресом (т. Е. URL-адресом GIF Tenor API).

Экземпляр AVPlayer одновременно инициализируется и выделяется ресурсы. Экземпляр AVPlayerViewController инициализируется и выделяет ресурсы.

Затем экземпляр AVPlayer устанавливается как проигрыватель для экземпляра AVPlayerViewController. Контроллер представления установлен как игрок.

Синопсис

Вы получаете данные из конечной точки API с помощью Alamofire, отображая их в представлении коллекции с ячейками представления коллекции с помощью SDWebImage.

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

Вы создаете приложение, чтобы оно правильно отображалось на iPhone и iPad во всех четырех ориентациях, изначально с iOS UIKit (т.е. раскадровка, автоматическая компоновка и классы размера). Вы создаете модульные тесты.

  • Панель поиска, которая обновляет представление коллекции по мере ввода пользователем.
  • При нажатии на ячейку представления коллекции приложение начинает воспроизведение видео.
  • Правильно отображается на iPhone и iPad во всех четырех ориентациях.
  • Функциональность модульных тестов

Кодовая база

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