Skip to content

Instantly share code, notes, and snippets.

@zykis
Created October 7, 2020 11:34
Show Gist options
  • Save zykis/e234764c1a109f27c4215e114800ee4e to your computer and use it in GitHub Desktop.
Save zykis/e234764c1a109f27c4215e114800ee4e to your computer and use it in GitHub Desktop.
Code sample
import Foundation
import Alamofire
let host = "https://api-dev.talala.la/api/v2"
protocol APIClientProtocol {
var authentication: APIClientAuthServiceProtocol { get }
var user: APIClientUserServiceProtocol { get }
var media: APIClientMediaServiceProtocol { get }
var challenge: APIClientChallengeServiceProtocol { get }
var helloService: HelloServiceProtocol { get }
func update(token: HelloModel)
func update(token: TokenModel)
func clearTokenInfo()
}
class APIClient: APIClientProtocol {
var authentication: APIClientAuthServiceProtocol
var user: APIClientUserServiceProtocol
var media: APIClientMediaServiceProtocol
var challenge: APIClientChallengeServiceProtocol
let helloService: HelloServiceProtocol
static let shared = APIClient()
var session: Session
private let logger: LoggerProtocol?
private let tokenService = TokenService()
private init() {
logger = Logger()
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
let interceptor = APIInterceptor(tokenService: tokenService, host: host, logger: logger)
session = Session(configuration: config, interceptor: interceptor)
helloService = HelloService(host: host, session: session, tokenService: tokenService)
interceptor.helloService = helloService
authentication = APIClientAuthService(host: host,
logger: logger,
session: session,
tokenService: tokenService,
helloService: helloService)
user = APIClientUserService(host: host,
logger: logger,
session: session,
tokenService: tokenService)
media = APIClientMediaService(host: host, logger: logger, session: session)
challenge = APIClientChallengeService(host: host, logger: logger, session: session)
}
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
protocol ChallengeViewProtocol: class {
var sortColumn: MediaSortColumn { get set }
var sortOrder: MediaSortOrder { get set }
func challengeFetchFailed(error: Error)
func update()
}
enum MediaSortColumn: Int {
case time = 0
case likes
}
enum MediaSortOrder: Int {
case ascending = 0
case descending
}
protocol ChallengePresenterProtocol: class {
var view: ChallengeViewProtocol? { get set }
var router: ChallengeRouterProtocol { get }
// In
func fetchChallenge(sortColumn: MediaSortColumn?, sortOrder: MediaSortOrder?)
func title() -> String?
func description() -> String?
func avatarURL() -> URL?
func username() -> String?
func reward() -> String?
func likes() -> String?
func dislikes() -> String?
func views() -> String?
func medias() -> String?
func reposts() -> String?
func start() -> String?
func end() -> String?
func categories() -> String?
func showStatus() -> Bool
func statusImageName() -> String?
func statusTitle() -> String?
func showFollowButton() -> Bool
func isFollowing() -> Bool?
func medias() -> [Media]
func userMediaID() -> String?
func winnerMediaID() -> String?
func challengeID() -> String?
func latitude() -> Double?
func longitude() -> Double?
// Out
func followChallenge()
func likeChallenge()
func dislikeChallenge()
func didTapMedia(index: Int)
func onProfilePressed()
}
class ChallengePresenter: ChallengePresenterProtocol {
struct Dependencies {
var challengeID: String?
var apiClient: APIClientChallengeServiceProtocol
}
weak var view: ChallengeViewProtocol?
var router: ChallengeRouterProtocol
var dependencies: Dependencies
var challenge: Challenge?
init(router: ChallengeRouterProtocol, dependencies: ChallengePresenter.Dependencies) {
self.router = router
self.dependencies = dependencies
}
func fetchChallenge(sortColumn: MediaSortColumn?, sortOrder: MediaSortOrder?) {
guard let challengeId = dependencies.challengeID else {
return
}
APIManager.shared.fetchChallenge(challengeID: challengeId,
mediaSortType: sortColumn,
mediaSortOrder: sortOrder) { (challenge, error) in
guard let challenge = challenge else {
if let error = error {
self.view?.challengeFetchFailed(error: error)
}
return
}
self.challenge = challenge
self.view?.update()
}
}
func title() -> String? {
return challenge?.title
}
func description() -> String? {
return challenge?.description
}
func avatarURL() -> URL? {
if let avatarUrl = challenge?.owner?.avatarUrl {
return URL(string: avatarUrl)
}
return nil
}
func username() -> String? {
if let owner = challenge?.owner {
return "@\(owner.username)"
}
return nil
}
func reward() -> String? {
if let challenge = challenge {
return "\(Int(challenge.reward / 100.0))$"
}
return nil
}
func likes() -> String? {
if let challenge = challenge {
return "\(challenge.likes)"
}
return nil
}
func dislikes() -> String? {
if let challenge = challenge {
return "\(challenge.dislikes)"
}
return nil
}
func views() -> String? {
if let challenge = challenge {
return "\(challenge.watch)"
}
return nil
}
func medias() -> String? {
if let challenge = challenge {
return "\(challenge.medias.count)"
}
return nil
}
func reposts() -> String? {
if let challenge = challenge {
return "\(challenge.reposts)"
}
return nil
}
func statusImageName() -> String? {
return "challenge-status-completed"
}
func statusTitle() -> String? {
guard let challenge = challenge else {
return nil
}
if Date() > challenge.endDate {
return "Successfully completed"
}
return challenge.endDate.extendedRepresentationSinceNow()
}
func start() -> String? {
if let challenge = challenge {
return challenge.startDate.readableRepresentation()
}
return nil
}
func end() -> String? {
if let challenge = challenge {
return challenge.endDate.readableRepresentation()
}
return nil
}
func categories() -> String? {
if let challenge = challenge {
let categories = challenge.tags.compactMap { $0.name }.joined(separator: " ")
return categories
}
return nil
}
func showStatus() -> Bool {
if let challenge = challenge, challenge.userMediaID != nil {
return true
}
return false
}
func showFollowButton() -> Bool {
if let challenge = challenge, challenge.owner != nil {
return true
}
return true
}
func isFollowing() -> Bool? {
if let challenge = challenge {
return challenge.isFollowed
}
return false
}
func medias() -> [Media] {
if let challenge = challenge {
return challenge.medias
}
return []
}
func userMediaID() -> String? {
return challenge?.userMediaID
}
func winnerMediaID() -> String? {
return challenge?.winnerMediaID
}
func challengeID() -> String? {
return challenge?.id
}
func latitude() -> Double? {
return challenge?.latitude
}
func longitude() -> Double? {
return challenge?.longitude
}
func followChallenge() {
guard let challengeId = dependencies.challengeID else { return }
dependencies.apiClient.followChallenge(challengeId: challengeId) { [weak self] response in
switch response {
case .success(_):
self?.fetchChallenge(sortColumn: self?.view?.sortColumn, sortOrder: self?.view?.sortOrder)
case .failure(let error):
print(error.localizedDescription)
}
}
}
func likeChallenge() {
guard let challengeId = dependencies.challengeID else { return }
dependencies.apiClient.likeChallenge(challengeId: challengeId) { [weak self] result in
switch result {
case .success(_):
#warning("Need create new method like as \"fetchChallenge\", there will getting current result and work with him instead new request")
self?.fetchChallenge(sortColumn: self?.view?.sortColumn, sortOrder: self?.view?.sortOrder)
case .failure(let error):
#warning("make code simplier")
if case let .api(errors) = error, errors.filter({ $0.key == .authRequired }).first != nil {
guard let view = self?.router.view else {
return
}
self?.router.presentSignIn(from: view) { [weak self] _ in
self?.likeChallenge()
}
return
}
}
}
}
func dislikeChallenge() {
guard let challengeId = dependencies.challengeID else { return }
dependencies.apiClient.dislikeChallenge(challengeId: challengeId) { [weak self] response in
switch response {
case .success(_):
self?.fetchChallenge(sortColumn: self?.view?.sortColumn, sortOrder: self?.view?.sortOrder)
case .failure(let error):
print(error.localizedDescription)
}
}
}
func didTapMedia(index: Int) {
guard let mediaIDs = challenge?.medias.compactMap({ $0.id }) else { return }
guard let view = router.view else { return }
router.goToMedia(from: view, index: index, mediaIDs: mediaIDs)
}
func onProfilePressed() {
#warning("Исправить после перехода на api v2")
router.goToSomeProfile(userId: "")
}
}
import Foundation
typealias CommentID = String
struct CommentModel {
let message: String
var counters: MediaModelCounters?
var isLiked: Bool?
var author: UserModel?
}
extension CommentModel: Codable {
private enum CodingKeys: String, CodingKey {
case message = "comment_message"
case counters
case isLiked = "is_liked"
case author
}
}
import UIKit
protocol ChallengeRouterProtocol: class, GoToMediaRouterProtocol, GoToProfileProtocol, SignInRouterProtocol {
var view: UIViewController? { get }
func navigationBar(hidden: Bool, animated: Bool)
func goBackToRoot(animated: Bool)
func goToSomeProfile(userId: String)
}
class ChallengeBuilder {
static func create(challengeId: String?) -> UIViewController {
let router = ChallengeRouter()
let dependencies = ChallengePresenter.Dependencies(
challengeID: challengeId,
apiClient: APIClient.shared.challenge
)
let presenter = ChallengePresenter(
router: router,
dependencies: dependencies)
let controller = ChallengeViewController(presenter: presenter, banubaService: Services.banubaEditor())
router.view = controller
return controller
}
}
class ChallengeRouter: ChallengeRouterProtocol {
weak var view: UIViewController?
func goBackToRoot(animated: Bool) {
view?.navigationController?.popToRootViewController(animated: animated)
}
func navigationBar(hidden: Bool, animated: Bool) {
view?.navigationController?.setNavigationBarHidden(hidden, animated: animated)
}
func goToSomeProfile(userId: String) {
visitProfile(from: view, userId: userId)
}
}
//
// WatchMediaView.swift
// Talala
//
// Created by Artem Zaytsev on 24.09.2020.
// Copyright © 2020 Артём Зайцев. All rights reserved.
//
import UIKit
import AVFoundation
import AVKit
class WatchMediaView: UIView {
private struct UI {
static let likedImage: UIImage = UIImage(named: "liked")!.withRenderingMode(.alwaysOriginal)
static let unlikedImage: UIImage = UIImage(named: "like")!.withRenderingMode(.alwaysOriginal)
static let playIconSize: CGFloat = 70
}
let presenter: WatchMediaPresenterProtocol
init(presenter: WatchMediaPresenterProtocol, isCustom: Bool, forcePlay: Bool) {
self.presenter = presenter
self.player = AVPlayer()
self.playerVC = AVPlayerViewController()
self.isCustom = isCustom
self.forcePlay = forcePlay
super.init(frame: .null)
self.presenter.view = self
commonInit()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
playerStatusObserver?.invalidate()
}
private let isCustom: Bool
private let forcePlay: Bool
private let player: AVPlayer
private let playerVC: AVPlayerViewController
private var playerItemLoopObserver: NSObjectProtocol?
private var playerItemStatusObserver: NSObjectProtocol?
private var playerStatusObserver: NSKeyValueObservation?
private let previewImageView = UIImageView.new {
$0.contentMode = .scaleAspectFill
}
private let playImageView = UIImageView.new {
$0.image = UIImage(named: "watch-media-play-icon")?.withRenderingMode(.alwaysTemplate)
$0.tintColor = .white
$0.alpha = 0
}
private let leftStackView = UIStackView.new {
$0.spacing = 10.0
$0.axis = .vertical
$0.distribution = .equalSpacing
$0.alignment = .leading
}
private let rightStackView = UIStackView.new {
$0.axis = .vertical
$0.alignment = .trailing
$0.distribution = .equalSpacing
$0.spacing = 16
}
private let likeStackView = UIStackView.new {
$0.axis = .vertical
$0.spacing = 5
$0.alignment = .center
}
private lazy var likeButton = VinciAnimatableButton.new {
$0.setImage(UI.unlikedImage, for: .normal)
$0.setImage(UI.unlikedImage, for: .highlighted)
$0.tintAdjustmentMode = .normal
$0.addTarget(self, action: #selector(likePressed), for: .touchUpInside)
}
private let likeLabel = UILabel.new {
$0.textColor = .white
$0.font = .boldSystemFont(ofSize: 14.0)
$0.text = "0"
}
private let commentStackView = UIStackView.new {
$0.axis = .vertical
$0.spacing = 5
$0.alignment = .center
}
private lazy var commentsButton = VinciAnimatableButton.new {
$0.setImage(UIImage(named: "icon_comments_white_60"), for: .normal)
$0.setImage(UIImage(named: "icon_comments_white_60"), for: .highlighted)
$0.tintAdjustmentMode = .normal
}
private let commentsLabel = UILabel.new {
$0.textColor = .white
$0.font = .boldSystemFont(ofSize: 14.0)
$0.text = "0"
}
private let shareStackView = UIStackView.new {
$0.axis = .vertical
$0.spacing = 5
$0.alignment = .center
}
private lazy var shareButton = VinciAnimatableButton.new {
$0.setImage(UIImage(named: "share"), for: .normal)
$0.setImage(UIImage(named: "share"), for: .highlighted)
$0.tintAdjustmentMode = .normal
$0.addTarget(self, action: #selector(sharePressed), for: .touchUpInside)
}
private let shareLabel = UILabel.new {
$0.textColor = .white
$0.font = .boldSystemFont(ofSize: 14.0)
$0.text = "0"
}
private let descriptionTextView = UITextView.new {
$0.text = "Description"
$0.textColor = .white
$0.textAlignment = .left
$0.font = .systemFont(ofSize: 15.0, weight: .regular)
$0.isEditable = false
$0.isSelectable = false
$0.isUserInteractionEnabled = false
$0.backgroundColor = .clear
$0.textContainer.lineBreakMode = .byTruncatingTail
$0.textContainer.lineFragmentPadding = 0.0
$0.contentInset.left = 0.0
$0.textContainerInset.left = 0.0
}
private let topGradient = GradientView.new {
$0.backgroundColor = .clear
$0.direction = "vertical"
$0.colors = "#000000AA, #00000000"
$0.locations = "0.0, 1.0"
}
private let descriptionGradient = GradientView.new {
$0.backgroundColor = .clear
$0.direction = "vertical"
$0.colors = "#00000000, #000000AA"
$0.locations = "0.0, 1.0"
}
private let rightButtonsGradient = UIView.new {
$0.backgroundColor = .clear
}
private lazy var radialGradient = CAGradientLayer.new {
$0.type = .radial
$0.colors = [ UIColor.black.withAlphaComponent(0.1),
UIColor.black.withAlphaComponent(0.0)]
$0.locations = [ 0, 1 ]
$0.startPoint = CGPoint(x: 0.5, y: 0.5)
$0.endPoint = CGPoint(x: 1, y: 1)
rightButtonsGradient.layer.addSublayer($0)
}
private lazy var profileStackView = UIStackView.new {
$0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(profilePressed)))
$0.axis = .horizontal
$0.spacing = 8.0
$0.alignment = .center
}
private let profileImageView = UIImageView.new {
$0.heightAnchor.constraint(equalToConstant: 24.0).isActive = true
$0.widthAnchor.constraint(equalToConstant: 24.0).isActive = true
$0.layer.cornerRadius = 12.0
$0.layer.masksToBounds = true
$0.backgroundColor = .white
}
private let profileVerticalStackView = UIStackView.new {
$0.axis = .vertical
$0.spacing = 0.0
$0.alignment = .leading
}
private let profileLabel = UILabel.new {
$0.font = .boldSystemFont(ofSize: 11.0)
$0.text = "Smithsonian institution"
$0.textColor = .white
}
private let postDateLabel = UILabel.new {
$0.font = .systemFont(ofSize: 11.0)
$0.text = "20 November 2020"
$0.textColor = .white
}
// WORKAROUND: we need a pace between elements look the same
// So instead of button, we should use StackView with invisible title
private let moveToChallengeStackView = UIStackView.new {
$0.axis = .vertical
$0.spacing = 0
$0.alignment = .center
}
private let moveToChallengeLabel = UILabel.new {
$0.textColor = .clear
$0.font = .systemFont(ofSize: 17.0, weight: .light)
$0.text = "0"
$0.heightAnchor.constraint(equalToConstant: 8.0).isActive = true
}
private lazy var moveToChallengeButton = UIButton.new {
$0.setImage(UIImage(named: "arrow"), for: .normal)
$0.addTarget(self, action: #selector(moveToChallengePressed), for: .touchUpInside)
}
}
// MARK: User input
extension WatchMediaView {
@objc
func profilePressed() {
pause()
presenter.handleProfilePressed()
}
@objc
func commentsPressed() {
}
@objc
func sharePressed() {
presenter.handleSharePressed()
}
@objc
func likePressed() {
presenter.handleLikePressed()
}
@objc
func moveToChallengePressed() {
}
@objc
func handleTap() {
let isPlaying = player.rate != 0.0
isPlaying ? pause() : play()
}
}
// MARK: Initialization
extension WatchMediaView {
private func commonInit() {
player.automaticallyWaitsToMinimizeStalling = true
playerItemLoopObserver = NotificationCenter.default.addObserver(
forName: .AVPlayerItemDidPlayToEndTime,
object: player.currentItem,
queue: .main) { [weak self] notification in
if self?.player.currentItem === notification.object as? AVPlayerItem {
self?.player.seek(to: CMTime.zero)
self?.player.play()
}
}
playerStatusObserver = player.observe(\.timeControlStatus) { [weak self] (object, _) in
if object.timeControlStatus == .playing {
self?.previewImageView.isHidden = true
}
}
playerVC.player = player
playerVC.showsPlaybackControls = false
playerVC.videoGravity = .resizeAspectFill
addSubviews()
setupConstraints()
presenter.updateMediaMetadata()
presenter.getMediaVideo()
}
private func addSubviews() {
addSubview(playerVC.view)
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
addSubview(descriptionGradient)
addSubview(topGradient)
addSubview(rightButtonsGradient)
addSubview(previewImageView)
addSubview(leftStackView)
addSubview(rightStackView)
addSubview(playImageView)
leftStackView.addArrangedSubview(descriptionTextView)
leftStackView.addArrangedSubview(profileStackView)
profileStackView.addArrangedSubview(profileImageView)
profileStackView.addArrangedSubview(profileVerticalStackView)
profileVerticalStackView.addArrangedSubview(profileLabel)
profileVerticalStackView.addArrangedSubview(postDateLabel)
rightStackView.addArrangedSubview(moveToChallengeStackView)
rightStackView.addArrangedSubview(likeStackView)
rightStackView.addArrangedSubview(commentStackView)
rightStackView.addArrangedSubview(shareStackView)
moveToChallengeStackView.addArrangedSubview(moveToChallengeButton)
moveToChallengeStackView.addArrangedSubview(moveToChallengeLabel)
likeStackView.addArrangedSubview(likeButton)
likeStackView.addArrangedSubview(likeLabel)
commentStackView.addArrangedSubview(commentsButton)
commentStackView.addArrangedSubview(commentsLabel)
shareStackView.addArrangedSubview(shareButton)
shareStackView.addArrangedSubview(shareLabel)
}
private func setupConstraints() {
playImageView.widthAnchor.constraint(equalToConstant: UI.playIconSize).isActive = true
playImageView.heightAnchor.constraint(equalTo: playImageView.widthAnchor, multiplier: 1.0).isActive = true
playImageView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
playImageView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
descriptionGradient.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
descriptionGradient.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
descriptionGradient.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
descriptionGradient.topAnchor.constraint(equalTo: leftStackView.topAnchor, constant: -20.0).isActive = true
topGradient.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
topGradient.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
topGradient.topAnchor.constraint(equalTo: topAnchor).isActive = true
topGradient.heightAnchor.constraint(equalToConstant: 130.0).isActive = true
rightButtonsGradient.leadingAnchor.constraint(equalTo: rightStackView.leadingAnchor,
constant: -20.0).isActive = true
rightButtonsGradient.topAnchor.constraint(equalTo: rightStackView.topAnchor, constant: -20.0).isActive = true
rightButtonsGradient.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
rightButtonsGradient.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
playerVC.view.frame = bounds
descriptionTextView.widthAnchor.constraint(equalToConstant: bounds.width * 0.6).isActive = true
likeButton.widthAnchor.constraint(equalToConstant: 40.0).isActive = true
likeButton.heightAnchor.constraint(equalTo: likeButton.widthAnchor).isActive = true
commentsButton.widthAnchor.constraint(equalToConstant: 40.0).isActive = true
commentsButton.heightAnchor.constraint(equalTo: commentsButton.widthAnchor).isActive = true
leftStackView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor,
constant: 17.0).isActive = true
leftStackView.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor,
constant: isCustom ? -25.0 : -10.0).isActive = true
leftStackView.trailingAnchor.constraint(equalTo: rightStackView.leadingAnchor).isActive = true
leftStackView.bottomAnchor.constraint(equalTo: rightStackView.bottomAnchor).isActive = true
rightStackView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor,
constant: -20.0).isActive = true
leadingAnchor.constraint(equalTo: previewImageView.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: previewImageView.trailingAnchor).isActive = true
topAnchor.constraint(equalTo: previewImageView.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: previewImageView.bottomAnchor).isActive = true
}
}
// MARK: WatchMediaViewProtocol
extension WatchMediaView: WatchMediaViewProtocol {
func isPlaying() -> Bool {
return player.rate != 0.0
}
func play() {
playImageView.alpha = 0
player.play()
}
func pause() {
playImageView.alpha = 0.54
player.pause()
}
func rewind() {
player.seek(to: .zero)
}
func updateMetadata(media: MediaModel, isLiked: Bool, likes: Int) {
likeLabel.text = likes.kmb
likeButton.setImage(isLiked ? UI.likedImage : UI.unlikedImage, for: .normal)
likeButton.setImage(isLiked ? UI.likedImage : UI.unlikedImage, for: .highlighted)
commentsLabel.text = media.counters.comments?.kmb
descriptionTextView.text = media.description
postDateLabel.text = media.createdAt.readableRepresentation2()
if let username = media.author.userName {
profileLabel.text = "@\(username)"
}
if let urlString = media.author.userAvatarUrl, let avatarUrl = URL(string: urlString) {
profileImageView.af.setImage(withURL: avatarUrl)
}
}
func updateVideo(videoUrl: URL) {
let asset = AVAsset(url: videoUrl)
let item = AVPlayerItem(asset: asset)
playerItemStatusObserver = item.observe(\.status) { [weak self] (object, _) in
guard let self = self else { return }
if object.status == AVPlayerItem.Status.readyToPlay, self.forcePlay {
self.play()
} else if let error = object.error {
print(error.localizedDescription)
}
}
player.replaceCurrentItem(with: item)
}
func updatePreview(previewUrl: URL) {
previewImageView.af.cancelImageRequest()
previewImageView.af.setImage(withURL: previewUrl)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment