ios - replacing an image with a video player dynamically - ios

My app (Swift 3, with a UIStoryboard) shows a list of episodes from a show. Whenever the user clicks on an episode from the table, he/she gets a detail view which shows (top to bottom):
the episode's image
the title
a description of the episode and technical information
Depending on the user type, instead of showing the episode's image, we would like to have a video player so people who are subscribed can just start watching the video (unsubscribed users would just see the image).
Based on the subscribed status of our users, a video player should be playing the content (subscribed) or showing an image (unsubscribed).
Is there a way to dynamically remove the image and replace it with the video player in the exact same spot and dimensions? And if so, what would happen with the Auto Layout contraints currently applied to the image?

Place a UIView on top of the UIImageView in storyboard. Align it to all 4 edges of UIImageView. And then hide and show them according to requirement by setting isHidden property.
Then, Insert the AVPlayer in the UIView through code.

Steps
The following solution works for both UIStoryboard and UI in code.
In InterfaceBuilder drag a UIView of preferred size onto the canvas, add your auto layout constraints and create an outlet (called "containerView")
Configure an AVPlayer with a URL or file, initialize an AVPlayerLayer instance with the player and add the layer to the containerView's layer hierarchy
Add an UIImageView on top of the container view
Based on your condition, show / hide / stop / play your movie and image
Quick code example
import UIKit
import AVFoundation
class ViewController: UIViewController {
// MARK: - Properties
#IBOutlet var containerView: UIView!
private var imageView: UIImageView!
private var isUserSubscribed = true
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Video
let url = URL(string: "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8")!
let avPlayer = AVPlayer(playerItem: AVPlayerItem(url: url))
let avPlayerLayer = AVPlayerLayer(player: avPlayer)
avPlayerLayer.frame = containerView.bounds
containerView.layer.insertSublayer(avPlayerLayer, at: 0)
// Image
imageView = UIImageView(image: UIImage(named: "someImage"))
imageView.frame = containerView.frame
containerView.addSubview(imageView)
// Condition
if isUserSubscribed {
imageView.isHidden = true
avPlayer.play()
} else {
imageView.isHidden = false
}
}
}
Result
Further options
With this approach you need to add your own playback controls, which basically boils down to adding buttons with actions, triggering AVPlayer.play(), AVPlayer.stop() etc.

Related

Is there a way to pinch and zoom a programmatically added image?

I am trying to figure out how to add pinch/zoom capabilities to an imageView,
but specifically to one that I added to the scrollView programmatically. I was able to find some great examples on SO to pinch and zoom an image using a scrollview with viewForZooming, but in that instance I had to return the image view being pinched and zoomed, which doesn't work if I am returning it programatically.
My ultimate goal is to have an array of images where the user can scroll left and right to see all the images AND be able to zoom in on them, basically just as if they were flipping through their photoStream. I found an ok tutorial for adding the images dynamically for scrolling here https://www.codementor.io/taiwoadedotun/ios-swift-implementing-photos-app-image-scrolling-with-scroll-views-bkbcmrgz5#comments-bkbcmrgz5 but I am not clear how to add the viewForZooming since the image views are being .addSubview dynamically in a loop.
I created a little example with a collection view of 0-n images associated to a post. Once the collectionViewCell with the image is tapped a hidden scrollView appears with a new dynamic UIImageView added as a subView. All works great but I don't know how to add the pinch/zoom now.
#objc func imageTapped(_ sender: UITapGestureRecognizer) {
print("BlipeFile Image Tapped")
let imageView = sender.view as! UIImageView
let newImageView = UIImageView(image: imageView.image)
//newImageView.frame = UIScreen.main.bounds
newImageView.contentMode = .scaleAspectFit
newImageView.clipsToBounds = true
newImageView.layer.borderColor = UIColor.gray.cgColor
newImageView.layer.borderWidth = 3.0
newImageView.frame = self.view.frame
newImageView.backgroundColor = .black
newImageView.isUserInteractionEnabled = true
newImageView.image = imageView.image
let tap = UITapGestureRecognizer(target: self, action: #selector(dismissFullscreenImage))
newImageView.addGestureRecognizer(tap)
scroller.isHidden = false
scroller.addSubview(newImageView)
}
#objc func dismissFullscreenImage(_ sender: UITapGestureRecognizer) {
scroller.isHidden = true
self.navigationController?.isNavigationBarHidden = false
self.tabBarController?.tabBar.isHidden = false
sender.view?.removeFromSuperview()
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return image //Can't return dynamically created newImageView?
}
My ultimate goal is to have an array of images where the user can scroll left and right to see all the images AND be able to zoom in on them
Apple has explained many times, in WWDC videos and in sample code that you can download, how this is done. Basically it’s just a scroll view in a scroll view. The outer scroll view permits scrolling horizontally. The inner scroll view contains an image view and permits zooming.
So instead of adding an image view, add a scroll view containing an image view.

AVPlayerLayer not sizing correctly

I have a custom UITableViewCell that displays either an image or a video. If there's a video, it initially downloads and displays the thumbnail from that video while the video is loading and preparing to play.
It all works, the problem is that I get the ratio of the thumbnail, store it in the database and when I download the thumbnail, it makes the height based on that ratio. It works great for images. But the issue is that the AVPlayerLayer frame doesn't cover the entire thumbnail and it looks like this:
Here's how I do it:
func updateView() {
if let videoUrlString = post?.videoUrl, let videoUrl = URL(string: videoUrlString) {
volumeView.isHidden = false
player = AVPlayer(url: videoUrl)
playerLayer = AVPlayerLayer(player: player)
playerLayer!.frame = postImageView.frame // thumbnail image
playerLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill
contentView.layer.addSublayer(playerLayer!)
self.volumeView.layer.zPosition = 1
layoutIfNeeded()
player?.play()
player?.isMuted = videoIsMuted
}
if let ratio = post?.ratio {
photoHeightConstraint.constant = UIScreen.main.bounds.width / ratio
layoutIfNeeded()
}
}
I'm not doing something right apparently or I'm missing something based on my final result. Any clue?
UPDATE:
I notice that if you scroll the cells slowly, the video is being displayed according to the size of the thumbnail. But if you scroll fast, it mismatches the frame. I assume it has something to do with reusability or it simply cannot catch up with the frame? Here's my code for prepareForReuse() method:
override func prepareForReuse() {
super.prepareForReuse()
profileImageView.image = UIImage(named: "placeholder")
postImageView.image = UIImage(named: "profile_placeholder")
volumeView.isHidden = true
if let p = player, let pLayer = playerLayer {
p.pause()
pLayer.removeFromSuperlayer()
}
}
The solution turned out to be rather simple. Basically each post contains either post or a video, but either way, it always contains an image. I also set the ratio of that image (the width devided by the height). So, all I had to do is set the player's width to the screen width devided by the ratio and it gives your the proper frame.
playerLayer.frame.size.height = UIScreen.main.bounds.width / postRatio

AVPlayer get rid of black bars/background at top & bottom (Swift)

Quick question, does anybody know how i can get rid of the black bars at the top and bottom of my video? I just started using AVPlayer and i'm just removing codes here and there in attempt to remove the black layers. Appreciate those who can help me, Thanks!
UIViewController
import UIKit
import AVKit
import AVFoundation
private var looper: AVPlayerLooper?
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let path = Bundle.main.path(forResource: "Innie-Kiss", ofType:"mp4")
let player = AVQueuePlayer(url: URL(fileURLWithPath: path!))
looper = AVPlayerLooper(player: player, templateItem: AVPlayerItem(asset: AVAsset(url: URL(fileURLWithPath: path!))))
let controller = AVPlayerViewController()
controller.player = player
let screenSize = UIScreen.main.bounds.size
let videoFrame = CGRect(x: 0, y: 200, width: screenSize.width, height: (screenSize.height - 130) / 2)
controller.view.frame = videoFrame
self.view.addSubview(controller.view)
player.play()
}
}
You need to set the videoGravity of the AVLayer. The default value is .resizeAspect which will preserve the aspect ratio with black bars on top/bottom or left/right. What you want is .resize
The AVLayer you need to set the videoGravity on is the layer property of your AVPlayer.
see this for more info: https://developer.apple.com/documentation/avfoundation/avplayerlayer/1388915-videogravity
#import AVKit
var playerController = AVPlayerViewController()
playerController.videoGravity = .resizeAspect
/*playerController.videoGravity = .resizeAspectFill*/
playerController.view.backgroundColor = UIColor.white //set color as per super or custom view.
Note: If you set the videoGravity of the AVPlayerViewController. The default value is .resizeAspect then it is possible to visibility of black color(default) in background if you wants this color would similar to your super view's color then you must set superview's or custom view's color to your playercontroller's view background color.
*Example:Suppose super view's or custom view's background color is white then playerController view's background color must be set as playerController.view.backgroundColor = UIColor.white *
Note :playerController's backgroundColor should not be set as UIColor.white ,always. It must me set as super view' background color.
If you set playerController.videoGravity = .resizeAspectFill then it will fill the player content to super view or custom view, here also no black color would be shown. So choose either .resizeAspect or resizeAspectFill as per your requirements.

UIImageView view Image from Left to Right

So I have a big image in an UIImageView (Swift) with "Aspect Fill"
But What I want is to have like an Animation or Ken Burn Effect on the Image.
In the Image Below... What I want is to go from Red to Yellow seen the image.
Take a normal uiview give it proper height width as per ur wish... Take an imageview inside of it and give the height, x coordinate and y coordinate same as uiview and keep the width as much wide you want to keep the image....
Use UIView.animateWithDuration method and in its animation block set the final desired frame such that the end of image view comes equal to the end of uiview... The image view will slide outside of vision of the uiview as you desire...
You can even repeatedly animate from left to right calling animation into each others completion block
I found an extension for UIImageView called UIImageViewAligned. You can use the alignment properties provided by the extension and animate the changes.
import UIKit
import UIImageViewAligned
class ViewController: UIViewController {
// MARK: Outlets
#IBOutlet var imageView: UIImageViewAligned!
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
imageView?.contentMode = .scaleAspectFill
imageView?.alignLeft = true
imageView?.alignRight = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 5) {
self.imageView?.alignLeft = false
self.imageView?.alignRight = true
}
}
}

Blur effect does not fill the entire View

I have a View controller that features a full screen UIImageView. When the controller loads, I just set a blur effect over the whole screen. The content mode for the UIImageView is set to scaleAspectFill. I set the blur effect like this:
#IBOutlet weak var previewImage: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
previewImage.image = userPhoto
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .Light))
blur.frame = self.view.bounds
previewImage.addSubview(blur)
}
when I test the app on an iPhone 6 I get this result:
There's an offset on the right an I can't figure out why. The picture underneath if is properly full screened but I can get the blur effect to fit over it...
****EDITED****
I applied the blur effect to the VC's view directly and now it works just fine, but I'd like to know why it doesn't work when I add the subview to the UIImage View...
I see two possible problems here:
Views are not laid out on the viewDidLoad stage. In this case you can force them to layout manualy:
override func viewDidLoad() {
super.viewDidLoad()
self.view.layoutIfNeeded()
...
}
You add subview to the previewImage, not to the root view. You must check your storyboard, or fix code:
override func viewDidLoad() {
super.viewDidLoad()
previewImage.image = userPhoto
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .Light))
blur.frame = self.view.bounds
self.view.insertSubview(blur, aboveSubview: previewImage)
}
Based on #kelin answer. You can even add the blur effect above your image in the interface builder.Just search for Visual Effect View with Blur in the objects section in the interface builder. Then add the Visual Effect View view above your image. It is also fully customizable through Attributes inspector. That way you you make sure that the blur will occupy the entire screen. It is also cleaner from architecture perspective , you do not need to configure your view through code.

Resources