Skip to content

Commit

Permalink
Updated project structure
Browse files Browse the repository at this point in the history
  • Loading branch information
Danil Gontovnik committed Jan 18, 2016
1 parent 3db74e1 commit 6189730
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 470 deletions.
96 changes: 69 additions & 27 deletions VideoViewController/TimelineView.swift
Original file line number Diff line number Diff line change
@@ -1,100 +1,142 @@
// TimelineView.swift
//
// TimelineView.swift
// VideoViewControllerExample
// Copyright (c) 2016 Danil Gontovnik (http://gontovnik.com/)
//
// Created by Danil Gontovnik on 1/4/16.
// Copyright © 2016 Danil Gontovnik. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import UIKit

class TimelineView: UIView {
public class TimelineView: UIView {

// MARK: - Vars

var duration: Double = 0.0 {
/// The duration of the video in seconds.
public var duration: NSTimeInterval = 0.0 {
didSet { setNeedsDisplay() }
}

var initialTime: Double = 0.0 {
/// Time in seconds when rewind began.
public var initialTime: NSTimeInterval = 0.0 {
didSet {
currentTime = initialTime
}
}

var currentTime: Double = 0.0 {
/// Current timeline time in seconds.
public var currentTime: NSTimeInterval = 0.0 {
didSet {
setNeedsDisplay()
currentTimeDidChange?(currentTime)
}
}

/// Internal zoom variable.
private var _zoom: CGFloat = 1.0 {
didSet { setNeedsDisplay() }
}

var zoom: CGFloat {
/// The zoom of the timeline view. The higher zoom value, the more accurate rewind is. Default is 1.0.
public var zoom: CGFloat {
get { return _zoom }
set { _zoom = max(min(newValue, maxZoom), minZoom) }
}

var minZoom: CGFloat = 1.0 {
/// Indicates minimum zoom value. Default is 1.0.
public var minZoom: CGFloat = 1.0 {
didSet { zoom = _zoom }
}

var maxZoom: CGFloat = 3.5 {
/// Indicates maximum zoom value. Default is 3.5.
public var maxZoom: CGFloat = 3.5 {
didSet { zoom = _zoom }
}

var intervalWidth: CGFloat = 24.0 {
/// The width of a line representing a specific time interval on a timeline. If zoom is not equal 1, then actual interval width equals to intervalWidth * zoom. Value will be used during rewind for calculations — for example, if zoom is 1, intervalWidth is 30 and intervalDuration is 15, then when user moves 10pixels left or right we will rewind by +5 or -5 seconds;
public var intervalWidth: CGFloat = 24.0 {
didSet { setNeedsDisplay() }
}

var intervalDuration: CGFloat = 15.0 {
/// The duration of an interval in seconds. If video is 55 seconds and interval is 15 seconds — then we will have 3 full intervals and one not full interval. Value will be used during rewind for calculations.
public var intervalDuration: CGFloat = 15.0 {
didSet { setNeedsDisplay() }
}

var currentTimeDidChange: ((Double) -> ())?
/// Block which will be triggered everytime currentTime value changes.
public var currentTimeDidChange: ((NSTimeInterval) -> ())?

// MARK: - Constructors

init() {
public init() {
super.init(frame: .zero)

opaque = false
}

required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Methods

func currentIntervalWidth() -> CGFloat {
/**
Calculate current interval width. It takes two variables in count - intervalWidth and zoom.
*/
private func currentIntervalWidth() -> CGFloat {
return intervalWidth * zoom
}

func durationFromWidth(width: CGFloat) -> Double {
return Double(width * intervalDuration / currentIntervalWidth())
/**
Calculates time interval in seconds from passed width.
- Parameter width: The distance.
*/
public func timeIntervalFromDistance(distance: CGFloat) -> NSTimeInterval {
return NSTimeInterval(distance * intervalDuration / currentIntervalWidth())
}

func widthFromDuration(duration: Double) -> CGFloat {
return currentIntervalWidth() * CGFloat(duration) / intervalDuration
/**
Calculates distance from given time interval.
- Parameter duration: The duration of an interval.
*/
public func distanceFromTimeInterval(timeInterval: NSTimeInterval) -> CGFloat {
return currentIntervalWidth() * CGFloat(timeInterval) / intervalDuration
}

func rewindByWidth(width: CGFloat) {
let newCurrentTime = currentTime + durationFromWidth(width)
/**
Rewinds by distance. Calculates interval width and adds it to the current time.
- Parameter distance: The distance how far it should rewind by.
*/
public func rewindByDistance(distance: CGFloat) {
let newCurrentTime = currentTime + timeIntervalFromDistance(distance)
currentTime = max(min(newCurrentTime, duration), 0.0)
}

// MARK: - Draw

override func drawRect(rect: CGRect) {
override public func drawRect(rect: CGRect) {
super.drawRect(rect)

let intervalWidth = currentIntervalWidth()

let originX: CGFloat = bounds.width / 2.0 - widthFromDuration(currentTime)
let originX: CGFloat = bounds.width / 2.0 - distanceFromTimeInterval(currentTime)
let context = UIGraphicsGetCurrentContext()
let lineHeight: CGFloat = 5.0

Expand All @@ -111,12 +153,12 @@ class TimelineView: UIView {
// Draw elapsed line
CGContextSetFillColorWithColor(context, UIColor.whiteColor().CGColor)

let elapsedPath = UIBezierPath(roundedRect: CGRect(x: originX, y: 0.0, width: widthFromDuration(currentTime), height: lineHeight), cornerRadius: lineHeight).CGPath
let elapsedPath = UIBezierPath(roundedRect: CGRect(x: originX, y: 0.0, width: distanceFromTimeInterval(currentTime), height: lineHeight), cornerRadius: lineHeight).CGPath
CGContextAddPath(context, elapsedPath)
CGContextFillPath(context)

// Draw current time dot
CGContextFillEllipseInRect(context, CGRect(x: originX + widthFromDuration(initialTime), y: 7.0, width: 3.0, height: 3.0))
CGContextFillEllipseInRect(context, CGRect(x: originX + distanceFromTimeInterval(initialTime), y: 7.0, width: 3.0, height: 3.0))

// Draw full line separators
CGContextSetFillColorWithColor(context, UIColor(white: 0.0, alpha: 0.5).CGColor)
Expand Down
91 changes: 57 additions & 34 deletions VideoViewController/VideoViewController.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
// VideoViewController.swift
//
// VideoViewController.swift
// VideoViewControllerExample
// Copyright (c) 2016 Danil Gontovnik (http://gontovnik.com/)
//
// Created by Danil Gontovnik on 1/4/16.
// Copyright © 2016 Danil Gontovnik. All rights reserved.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import UIKit
import AVFoundation

class VideoViewController: UIViewController {
public class VideoViewController: UIViewController {

// MARK: - Vars

Expand All @@ -27,45 +41,49 @@ class VideoViewController: UIViewController {

private let rewindDimView = UIVisualEffectView()
private let rewindContentView = UIView()
private let rewindTimelineView = TimelineView()

public let rewindTimelineView = TimelineView()
private let rewindPreviewShadowLayer = CALayer()
private let rewindPreviewImageView = UIImageView()
private let rewindCurrentTimeLabel = UILabel()

var rewindPreviewMaxHeight: CGFloat = 112.0 {
/// Indicates the maximum height of rewindPreviewImageView. Default value is 112.
public var rewindPreviewMaxHeight: CGFloat = 112.0 {
didSet {
assetGenerator.maximumSize = CGSize(width: CGFloat.max, height: rewindPreviewMaxHeight * UIScreen.mainScreen().scale)
}
}

/// Indicates whether player should start playing on viewDidLoad. Default is true.
public var autoplays: Bool = true

// MARK: - Constructors

init(videoURL: NSURL) {
/**
Returns an initialized VideoViewController object
- Parameter videoURL: Local URL to the video asset
*/
public init(videoURL: NSURL) {
super.init(nibName: nil, bundle: nil)

self.videoURL = videoURL

asset = AVURLAsset(URL: videoURL)

playerItem = AVPlayerItem(asset: asset)

player = AVPlayer(playerItem: playerItem)
player.actionAtItemEnd = .None

playerLayer = AVPlayerLayer(player: player)

assetGenerator = AVAssetImageGenerator(asset: asset)
assetGenerator.maximumSize = CGSize(width: CGFloat.max, height: rewindPreviewMaxHeight * UIScreen.mainScreen().scale)
}

required init?(coder aDecoder: NSCoder) {
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: -

override func loadView() {
override public func loadView() {
super.loadView()

view.backgroundColor = .blackColor()
Expand Down Expand Up @@ -95,7 +113,10 @@ class VideoViewController: UIViewController {

dispatch_async(dispatch_get_main_queue()) {
strongSelf.rewindPreviewImageView.image = image
strongSelf.layoutRewindPreviewImageViewIfNeeded()

if strongSelf.rewindPreviewImageView.bounds.size != image.size {
strongSelf.viewWillLayoutSubviews()
}
}
}
}
Expand All @@ -121,15 +142,27 @@ class VideoViewController: UIViewController {
rewindContentView.addSubview(rewindPreviewImageView)
}

override func viewDidLoad() {
override public func viewDidLoad() {
super.viewDidLoad()

player.play()
if autoplays {
play()
}
}

// MARK: - Methods

func longPressed(gesture: UILongPressGestureRecognizer) {
/// Resumes playback
public func play() {
player.play()
}

/// Pauses playback
public func pause() {
player.pause()
}

public func longPressed(gesture: UILongPressGestureRecognizer) {
let location = gesture.locationInView(gesture.view!)
rewindTimelineView.zoom = (location.y - rewindTimelineView.center.y - 10.0) / 30.0

Expand All @@ -141,7 +174,7 @@ class VideoViewController: UIViewController {
self.rewindContentView.alpha = 1.0
}, completion: nil)
} else if gesture.state == .Changed {
rewindTimelineView.rewindByWidth(previousLocationX - location.x)
rewindTimelineView.rewindByDistance(previousLocationX - location.x)
} else {
player.play()

Expand All @@ -159,13 +192,13 @@ class VideoViewController: UIViewController {
}
}

override func prefersStatusBarHidden() -> Bool {
override public func prefersStatusBarHidden() -> Bool {
return true
}

// MARK: - Layout

override func viewWillLayoutSubviews() {
override public func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()

playerLayer.frame = view.bounds
Expand All @@ -180,18 +213,8 @@ class VideoViewController: UIViewController {
rewindPreviewImageView.frame = CGRect(x: (rewindContentView.bounds.width - rewindPreviewImageViewWidth) / 2.0, y: (rewindContentView.bounds.height - rewindPreviewMaxHeight - verticalSpacing - rewindCurrentTimeLabel.bounds.height - verticalSpacing - timelineHeight) / 2.0, width: rewindPreviewImageViewWidth, height: rewindPreviewMaxHeight)
rewindCurrentTimeLabel.frame = CGRect(x: 0.0, y: rewindPreviewImageView.frame.maxY + verticalSpacing, width: rewindTimelineView.bounds.width, height: rewindCurrentTimeLabel.frame.height)
rewindTimelineView.frame = CGRect(x: 0.0, y: rewindCurrentTimeLabel.frame.maxY + verticalSpacing, width: rewindContentView.bounds.width, height: timelineHeight)

layoutRewindPreviewImageViewIfNeeded()
}

private func layoutRewindPreviewImageViewIfNeeded() {
guard let image = rewindPreviewImageView.image where rewindPreviewImageView.bounds.size != image.size else {
return
}

rewindPreviewImageView.frame = CGRect(x: (rewindContentView.bounds.width - image.size.width) / 2.0, y: rewindPreviewImageView.frame.minY, width: image.size.width, height: rewindPreviewImageView.bounds.height)
rewindPreviewShadowLayer.frame = rewindPreviewImageView.frame

let path = UIBezierPath(roundedRect: rewindPreviewImageView.bounds, cornerRadius: 5.0).CGPath
rewindPreviewShadowLayer.shadowPath = path
(rewindPreviewImageView.layer.mask as! CAShapeLayer).path = path
Expand Down

0 comments on commit 6189730

Please sign in to comment.