I have two view controllers (recorder and player) with tapGuestureRecognizers inside. I'm trying to use both of them in one place, lets call it container view controller, and replacing one with another with methods:
private func displayContentController(content: UIViewController) {
addChildViewController(content)
content.view.frame = view.frame
view.addSubview(content.view)
content.didMoveToParentViewController(self)
}
private func hideContentController(content: UIViewController) {
content.willMoveToParentViewController(nil)
content.view.removeFromSuperview()
content.removeFromParentViewController()
}
This is container view controller code:
private let recorder = CMRecorder()
private let player = CMPlayer()
override func viewDidLoad() {
super.viewDidLoad()
displayContentController(recorder)
recorder.finishRecordingCallback = { url in
self.hideContentController(self.recorder)
self.displayContentController(self.player)
}
}
Everything is ok with taping on recorder, but player don't want to recognize my taps. If I swap recorder with player (so player is loaded first), player has no problem with guesture recognizer. What did I miss?
I found a problem.
In my player view controller I have added imageView and forgot to set:
thumbnailView.userInteractionEnabled = true
Related
We have the following app, where user can switch to different "page" (purple, yellow, ... colours) from side menu.
I was wondering, should the "page" be implemented as UIView, or should the "page" be implemented as UIViewController?
The pages shall responsible to
Read/ write from/ to CoreData.
Possible holding a UIPageView, which user can swipe through multiple child pages as shown in https://i.stack.imgur.com/v0oNo.gif
Holding a UICollectionView.
User can drag and move the items in the UICollectionView
User can perform various contextual action (Delete, clone, ...) on the items in UICollectionView.
Can easily port to iPad in the future.
Currently, my implementation of using UIView are as follow.
private func archive() {
if let trashView = self.trashView {
trashView.removeFromSuperview()
self.trashView = nil
}
if self.archiveView != nil {
return
}
let archiveView = ArchiveView.instanceFromNib()
self.view.addSubview(archiveView)
archiveView.translatesAutoresizingMaskIntoConstraints = false
archiveView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
archiveView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
archiveView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
archiveView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.archiveView = archiveView
}
private func trash() {
if let archiveView = self.archiveView {
archiveView.removeFromSuperview()
self.archiveView = nil
}
if self.trashView != nil {
return
}
let trashView = TrashView.instanceFromNib()
self.view.addSubview(trashView)
trashView.translatesAutoresizingMaskIntoConstraints = false
trashView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
trashView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
trashView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
trashView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
self.trashView = trashView
}
I notice that if I implement the "pages" using UIView, I will lost some capability of UIViewController like
viewDidLoad callback.
viewWillLoad callback.
viewDidLayoutSubviews callback.
...
However, I am not clear whether losing those capabilities will stop me from implementing a proper "page"?
May I know, should I implement those "pages" using UIView, or using UIViewController?
I would do this with UIViewController because of all the UIKit callback reasons you listed in the question already.
I assume that you have a UINavigationController instance that's set as window.rootViewController for your app. You have a reference to this instance using which you can easily switch between different screens.
Example
class SlideMenuViewController: UIViewController {
enum Option {
case archive
case trash
}
var onSelect: ((_ option: Option) -> Void)?
}
class ArchiveViewController: UIViewController {}
class TrashViewController: UIViewController {}
class AppNavigator {
let mainNavigationController: UINavigationController
init(navigationController: UINavigationController) {
self.mainNavigationController = navigationController
}
private lazy var slideMenuVC: SlideMenuViewController = {
let slideMenu = SlideMenuViewController()
slideMenu.onSelect = { [weak self] (option) in
self?.openScreen(for: option)
}
return slideMenu
}()
private lazy var archiveVC: ArchiveViewController = {
return ArchiveViewController()
}()
private lazy var trashVC: TrashViewController = {
return TrashViewController()
}()
func openScreen(for option: SlideMenuViewController.Option) {
let targetVC: UIViewController
switch option {
case .archive: targetVC = archiveVC
case .trash: targetVC = trashVC
}
mainNavigationController.setViewControllers([targetVC], animated: true)
}
}
Perhaps I totally misunderstood your question, but your UIView should only hold logic related to how the view itself will appear to the user. The UIView should not contain any logic related to the other views or to the model.
UIKit assumes that you use the MVC model to implement apps on the Apple platforms. This means that any code related to controlling which view has to appear and which data the view should get from the model, should be written in the ViewController.
In Xcode you have both the UICollectionViewController and the UIPageViewController to implement page swipes and dragging an dropping views.
View controllers can give the control to other view controllers to present views. The view controller also determine the data that should be presented by the view. Please, check out this article about the MVC model.
Kind regards,
MacUserT
I would like to add a share button to share a video (by mail for example) with the UIActivityController.
I tried to access to the properties of the AVPlayerViewController, but it seems we are not allowed to it anymore...
for subview in playerViewController.view.subviews
{
}
How to add a simple button next to the play button on the right in the playbackControls?
Any ideas?
If you look at the view controller's view's subviews, you will see its a single view called AVPlayerViewControllerContentView. You can do something like this when making your view controller:
func showFullScreenVideo(using player: AVPlayer) {
// setup your player view controller
let playerViewController = AVPlayerViewController()
playerViewController.player = player
present(playerViewController, animated: true) { [weak self] in
self?.addShareButton(to: playerViewController)
}
}
private func addShareButton(to playerViewController: AVPlayerViewController) {
if let playerContentView = playerViewController.view.subviews.first {
let someView = SomeView()
playerContentView.addSubview(someView)
// use your own layout code:
someView.constrain(.centerX, .centerY, to: playerContentView)
}
}
Adding the share button inside the completion block ensures that playerViewController.view is loaded and available to use. You can try to force the view to load in many ways. The choice is yours. Another possible way of doing this is adding this line of code just before adding your share button:
_ = playerViewController.view
instead of using the completion block. May or may not work, but the completion block method definitely works.
I want my several view controllers to have a player in the bottom. This player consists of 2 views: the player and a button which toggles it (can be hidden or expanded).
Now I use the code below in each view controller to add this player.
#IBOutlet weak var broadcastView: BroadcastView!
#IBOutlet weak var broadcastViewBottomConstraint: NSLayoutConstraint!
#IBOutlet weak var avatarImageView: UIImageView!
#IBAction func toggleBroadcastMode(_ sender: ToggleBroadcastButton) {
if sender.isExpanded {
broadcastViewBottomConstraint.hideBroadcastView()
} else {
broadcastViewBottomConstraint.expandBroadcastView()
}
animateBroadcastToggle()
sender.toggle()
broadcastView.toggleBroadcastView()
}
Is there a way not to duplicate the code over and over? Maybe I can create parent VC or View to do it? If so, then how?
I personally would subclass a UINavigationController and have it in there, that way you can navigate through the flow while the player stays looking good at the bottom, if you need a VC to interact with it then you can
if let nav = navigationController as? MyPlayerNavController {
nav.PlayThis()
}
you can have it change size and everything from there and you wont lose it during transitions and stuff like the music app when playing music.
Add it as a subview of the keyWindow. That way it will always stay in all the viewControllers.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
var overlayViewFrame = UIScreen.main.bounds
overlayViewFrame.origin.y = overlayViewFrame.height - 200
overlayViewFrame.size.height = 200
let overlayView = UIView(frame:overlayViewFrame)
overlayView.backgroundColor = UIColor.cyan
UIApplication.shared.keyWindow?.addSubview(overlayView)
}
This code is a sample generic code. If you want certain VCs to not show this, keep this overlay view in a singleton, and hide in appropriate VCs.
Output screenshots:
I have a view which I set up as input accessory view for view controller the following way:
#IBOutlet private weak var bottomPane: UIView!
override func canBecomeFirstResponder() -> Bool {
return true
}
override var inputAccessoryView: UIView {
return bottomPane
}
Everything works just fine until I try to view YouTube video in fullscreen mode (video is loaded in UIWebView). When video enters fullscreen mode, keyboard and my input accessory view disappear (which is normal, I guess), but when I exit fullscreen mode, they do not appear. If I keep the reference to bottomPane weak, it becomes nil and application crashes, if I change it to strong, input accessory view remains hidden until the keyboard appears next time.
Can anybody explain what's going on and how to fix this?
Here's what's going on.
When user interacts with UIWebView, it becomes first responder and inputAccessoryView provided by view controller disappears (no idea why behavior in this case is different from, say, UITextField). Subclassing UIWebView and overriding inputAccessoryView property does not work (never gets called). So I block interaction with UIWebView until user loads video.
private func displayVideo(URL: String) {
if let video = Video(videoURL: URL) {
// load video in webView
webView.userInteractionEnabled = true
} else {
webView.userInteractionEnabled = false
}
}
When user loads video, the only way to detect that user has entered/exited fullscreen mode is to listen to UIWindowDidBecomeKeyNotification and UIWindowDidResignKeyNotification and detect when our window loses/gains key status:
//in view controller:
private func windowDidBecomeKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this restores our inputAccessoryView
becomeFirstResponder()
}
}
private func windowDidResignKey(notification: NSNotification!) {
let isCurrentWindow = (notification.object as! UIWindow) == view.window
if isCurrentWindow {
// this hides our inputAccessoryView so that it does not obscure video
resignFirstResponder()
}
}
And, of course, since inputAccessoryView can be removed at some point, we should recreate it if needed:
//in view controller:
override var inputAccessoryView: UIView {
if view == nil {
// load view here
}
return view
}
So I have a TableView that for each cell a user can hold down to view a video/image. The holding down part works but when I release it doesn't go back to the TableView.
Heres the code for presenting the view with the video/image:
func playVideo() {
// get path and url of movie
let path = NSBundle.mainBundle().pathForResource("static2", ofType:"mov")
println(path)
let url = NSURL.fileURLWithPath(path!)
var moviePlayer = MPViewController()//took out video handling right now so this is just basically a regular view controller
moviePlayer.navController = self.navigationController
// construct the views
moviePlayer.addGestRecognizer()
self.navigationController.pushViewController(moviePlayer, animated: true)
}
and then I have this gesture recognizer added to the view cell:
let singleTap = UILongPressGestureRecognizer(target: self, action: Selector("tapped:"))
self.addGestureRecognizer(singleTap)
and here is the "tapped:" function:
func tapped(gestureRecognizer:UIGestureRecognizer){
println("held down")
if gestureRecognizer.state == UIGestureRecognizerState.Began {
playVideo()
}
if gestureRecognizer.state == UIGestureRecognizerState.Ended {
println("hold ended")
self.navigationController.popViewControllerAnimated(true)
}
//playVideo()
}
for some reason once I push the view controller onto the main navigationController I no longer get the "hold ended". How can I set this up so it works exactly like snapchat?
This is not a solution for what you're asking but another idea, you might want to think about presenting the media files with a UIView instead of a UIViewController.
Hope that helps :)