How to show a customizable view into all view controller? - ios

the question is little bit elaborate what I really want to know.I want to make an app whose some functionality looks like SoundCloud and gaana. Below here I show some screenshot of gaana.
Here if we click any song they automatically update the above view and play the song and also this view will show into all view controller.Clicking this up arrow button how they showing up a new view where the previous small view is no where and also load a collection view into this new view.how to do all of this?the new view is here.

You can do that by creating an overlay, i.e. you add a UIView as a child element to the app's main window like so:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
showOverlay()
}
func showOverlay() {
guard let window = UIApplication.shared.keyWindow else {
//if this block runs we were unable to get a reference to the main window
print("you have probably called this method in viewDidLoad or at some earlier point where the main window reference might be nil")
return
}
//add some custom view, for simplicity I've added a black view
let blackView = UIView()
blackView.backgroundColor = .black
blackView.translatesAutoresizingMaskIntoConstraints = false
blackView.frame = CGRect(x: 0, y: window.frame.height - 100, width: window.frame.width, height: 100.0)
window.addSubview(blackView)
}
Now this view should always be floating above the current UIViewController

Related

Putting loading animation over VNDocumentViewController Swift

Is it possible to put a loading animation over the VNDocumentViewController? As in, when the user presses the Save button, is there a way for me to somehow indicate that the Vision is processing the image and hasn't frozen? Right now, in my app, there is a long pause between the user pressing Save and the actual image being processed.Here is an example from another post of what I'm trying to create
Here is one example of adding a loading indicator using UIActivityIndicatorView().
startAnimating() to start the animation and stopAnimation() to stop the animation.
iOS - Display a progress indicator at the center of the screen rather than the view
guard let topWindow = UIApplication.shared.windows.last else {return}
let overlayView = UIView(frame: topWindow.bounds)
overlayView.backgroundColor = UIColor.clear
topWindow.addSubview(overlayView)
let hudView = UIActivityIndicatorView()
hudView.bounds = CGRect(x: 0, y: 0, width: 20, height: 20)
overlayView.addSubview(hudView)
hudView.center = overlayView.center
hudView.startAnimating()
Alternatively, you could look into using Cocoapod MBProgressHud
https://cocoapods.org/pods/MBProgressHUD
There's a way you can extend a class in Swift that captures this problem well. The idea is you want a UIActivityIndicator in your VNDocumentCameraViewController. But we'd like that to be a part of every version of this we use. We could simply embed the DocumentVC's view into our current view and superimpose a UIActivityIndicator above it in the view stack, but that's pretty hacky. Here's a quick way we can extend any class and solve this problem
import VisionKit
import UIKit
extension VNDocumentCameraViewController {
private struct LoadingContainer {
static var loadingIndicator = UIActivityIndicatorView()
}
var loadingIndicator: UIActivityIndicatorView {
return LoadingContainer.loadingIndicator
}
func animateLoadingIndicator() {
if loadingIndicator.superview == nil {
view.addSubview(loadingIndicator)
//Setup your constraints through your favorite method
//This constrains it to the very center of the controller
loadingIndicator.frame = CGRect(
x: view.frame.width / 2.0,
y: view.frame.height / 2.0,
width: 20,
height: 20)
//Setup additional state like color/etc here
loadingIndicator.color = .white
}
loadingIndicator.startAnimating()
}
func stopAnimatingLoadingIndicator() {
loadingIndicator.stopAnimating()
}
}
The place we can call these functions are in the delegate methods for VNDocumentCameraViewController that you implement in your presenting ViewController:
func documentCameraViewController(
_ controller: VNDocumentCameraViewController,
didFinishWith scan: VNDocumentCameraScan
) {
controller.animateLoadingIndicator()
}

Maintain custom UIView that is added to KeyWindow

I am working on a sync feature for my app.
What I want to achieve is to display a custom UIView that will serve as an indicator to the user, wherever screen the user at (such as navigate to dashboard, settings, profile page, etc), that the synchronization is in progress. In short term, it will stick to a position within the app statically.
After done some researches from the web, I come to a conclusion to use keyWindow and add a subview to it.
Here is my code
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let window = UIApplication.shared.keyWindow {
window.windowLevel = .statusBar
let uiView = UIView()
uiView.backgroundColor = .green
window.addSubview(uiView)
uiView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
uiView.heightAnchor.constraint(equalToConstant: 150),
uiView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 25),
uiView.topAnchor.constraint(equalTo: view.topAnchor, constant: 150),
uiView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4)
])
}
}
Above code is working fine, I can attach a custom view to the keyWindow. However, when the user navigate to another screen, the added customView will dissappear, and only show when the user goes back to the previous screen.
Can someone guide on what part I am missing? On which part I did wrong?
Thanks
As per comments I am adding a minimum requirement to create a new window which will can now always be on top of your view controller or even other windows:
class OverlayViewController: UIViewController {
private var myWindow: UIWindow? // Our own window needs to be retained
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
view.addSubview({
let label = UILabel(frame: .zero)
label.text = "This is an overlay."
label.sizeToFit()
label.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
return label
}())
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap() {
myWindow?.isHidden = true
myWindow?.removeFromSuperview()
myWindow = nil
}
static func showInNewWindow() {
let window = UIWindow(frame: UIScreen.main.bounds)
// window.windowLevel = .alert
// window.isUserInteractionEnabled = false
window.rootViewController = {
let controller = OverlayViewController() // OR UIStoryboard(name: "<#FileName#>", bundle: nil).instantiateViewController(withIdentifier: "<#Storyboard Identifier#>")
controller.myWindow = window
return controller
}()
window.makeKeyAndVisible()
}
}
So this is just all-in-code simple view controller example which can be used anywhere in the project by simply calling OverlayViewController.showInNewWindow(). The view controller can be from storyboard though.
The interesting part is the showInNewWindow which creates a new window and an instance of this view controller that is set as a root view controller. To show the new window all you need to call is makeKeyAndVisible. And to remove it simply hid it and remove it from superview as done in onTap. In this case the window is referenced by the view controller which should create a retain cycle. It seems that this is mandatory. It also means that we need to do some cleanup once window is dismissed by calling myWindow = nil.
There are some settings possible as well like you can disable user interaction and enable user to still use the window below this one (window.isUserInteractionEnabled = false). And you can set the level of window as to where is it displayed (window.windowLevel = .alert). You can play around a bit with these values. Which to set depends on what level you want your window to be.

Swift iOS -View Controller deinit runs when adding it to a new keyWindow

I have a view controller (OrangeVC) that I add to a class that contains a new keyWindow(NewKeyWindowClass). A button in a different vc is tapped and it triggers this new window to get shown over the app's main window and it animates from the right side bottom of the screen to fill to the top. The animation works fine, it starts from the bottom and fills the screen with a new vc with a orange background. The problem is once the OrangeVC is added to the NewKeyWindowClass the orangeVC's deinit keeps getting triggered.
Why is it's deinit running?
Class that goes inside Animator Class:
class OrangeController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
deinit {
print("OrangeVC -Deinit")
}
}
AnimatorClass:
import UIKit
class NewKeyWindowClass: NSObject {
func animateOrangeVCFromBottomToTop() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
let orangeVC = OrangeController()
// 1. starting frame
orangeVC.view.frame = CGRect(x: keyWindow.frame.width - 10, y: keyWindow.frame.height - 10, width: 10, height: 10)
keyWindow.addSubview(orangeVC.view)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
// 2. ending frame
orangeVC.view.frame = keyWindow.frame
})
}
}
Button from a different class that triggers the animation:
#IBAction func triggerAnimationButtonPressed(_ sender: UIButton) {
let newKeyWindowClass = NewKeyWindowClass()
newKeyWindowClass.animateOrangeVCFromBottomToTop()
}
I got the answer from this reddit
An iOS application must have a rootViewController, create one and set
the keyWindow.rootViewController property to it. Then present your
view controller from that. Or just the rootViewController to be your
View Controller actually.
The reason the RedVC kept running it's deinit was because the keyWindow didn't have a rootViewController. I added the RedVC's view as a subview to the keyWindow keyWindow.addSubview(orangeVC.view) instead of making it it's rootVC:
keyWindow.rootViewController = redVC
Once I added it that the RedVC's deinit no longer ran when the animation occurred.
It should be noted that although it stopped the deinit from running I lost the animation and it also made the original keyWindow disappear. I should actually add this to a different UIWindow.

Set video on old page to 0.0 seconds when scrolled to new page

I have a scrollView with pagingEnabled set to true. Each of these pages contains one video covering the entire screen. The videos are added using a for loop and a class called PlayerView (not only adding the video but also information about the video). Within the PlayerView class I have a function which starts/stops playing the video in the respective page and all that works fine.
My problem is I would like to set the video to the start of it and stop it there (at 0.0 seconds so to say) whenever the page containing that video is not visible anymore (with self.player?.seek(to: kCMTimeZero) and self.player?.pause()).
In my scrollView I have a function which detects what page has appeared and also states the previous page when the user scrolls from one page to another (in form of an index, e.g. when scrolled from page 2 to page 3 the function will print: "1 2"). To stick to the example: I would like to set the video on page 2 (index: 1) to 0 and pause it.
create scrollView (in class HomeController: UIViewController, UIScrollViewDelegate)
func initiateScrollView() {
//create scrollView with paging enabled
let scrollView = UIScrollView(frame: view.bounds)
scrollView.isPagingEnabled = true
scrollView.delegate = self
view.addSubview(scrollView)
//get page size
let pageSize = view.bounds.size
//three simple UIView are created (view1, view2, view3)
//array with individual views
let pagesViews = [view1, view2, view3]
//amount of views
let numberOfPages = pagesViews.count
//add subviews (pages)
for (pageIndex, page) in pagesViews.enumerated(){
//add individual pages to scrollView
page.frame = CGRect(origin: CGPoint(x:0 , y: CGFloat(pageIndex) * pageSize.height), size: pageSize)
scrollView.addSubview(page)
//add Subview with videoplayer frame to individual "pages"
let videoPlayerFrame = CGRect(x: 0, y: 0, width: pageSize.width, height: pageSize.height)
let videoPlayerView = PlayerView(frame: videoPlayerFrame)
page.addSubview(videoPlayerView)
}
//adjust size of scrollView
scrollView.contentSize = CGSize(width: pageSize.width, height: pageSize.height * CGFloat(numberOfPages))
}
Do you have an idea how to pause the video on the previous page? I would really appreciate your help!
There are about two ways I think you could do this depending on how exactly you want the video to stop.
Method 1: The Video on the Previous View Stops and the Video on the New View Does Not Auto-Play
You can create an Notification observer in each of the views which listens for a change of scroll view and fires a common function to stop the video (it is good practice to create a protocol to constrain these views to the common function although I will not do it here). So in each of the views you add an observer like this in the ViewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.stopVideo), name: NSNotification.Name(rawValue: "stopVideo"), object: nil)
And you add the corresponding function that will be called in the view:
func stopVideo() {
self.player.stop()
}
Then in your scrollview, in the function you mentioned that "detects what page has appeared and also states the previous page", you fire a notification whenever the scroll view changes:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "stopVideo"), object: nil)
This will stop any video playing in all the views whenever there is a change of view. P.S Also remember to remove the observer in the deinit()
Method 2: The Video on the Previous View Stops and the Video on the New View Does Will Auto-Play
If the previous video will stop and new video will auto-play you could make a protocol for each of the views that they must adhere to like so:
protocol StopVideoDelegate: class {
func stopVideo()
func startVideo()
}
Each of your views must implement this protocol, for example:
class ViewController1InScrollView: StopVideoDelegate {
func stopVideo() {
self.playerLayer.stop()
}
func startVideo() {
self.playerLayer.start()
}
}
Then in your scrollview, in the function you mentioned that "detects what page has appeared and also states the previous page", can simply cast the previous view and next view to the protocol type and call the corresponding function:
func theFunctionThatDetectsViewChangeInScrollView() {
nextView as! StopVideoDelegate
nextView.startVideo()
previousView as! StopVideoDelegate
previousView.stopVideo()
}

ViewController as Overlay on MapView in Swift (like Google Maps Application User Interface)

I'm building a small app with Swift where I want to achieve following:
- MapView with several Annotations
- Click on one of the annotations shows some Details in an overlay that is moving in from the bottom and fill approx. half of the screen
- The upper half of the display still shows the MapView. A click on the map closes the details
Now I've read a lot about different possibilities. First I've tried using a ContainerView with something like this:
#IBOutlet weak var detailsContainerYPosition: NSLayoutConstraint!
func mapView(mapView: MKMapView!, didSelectAnnotationView view: MKAnnotationView!) {
UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseOut, animations: {
self. detailsContainerYPosition.constant = 0
self.view.layoutIfNeeded()
}, completion: nil)
}
The problem with that was, that the viewDidLoad() method was only fired once and I wanted to use that to build up the Details page after passing some data to the controller class.
So next I've played around with a custom segue and cam up with this:
class DetailsSegue: UIStoryboardSegue {
override func perform() {
// Assign the source and destination views to local variables.
var mapView = self.sourceViewController.view as UIView!
var detailsView = self.destinationViewController.view as UIView!
// Get the screen width and height.
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
// Specify the initial position of the destination view.
detailsView.frame = CGRectMake(0.0, screenHeight, screenWidth, 140)
// Access the app's key window and insert the destination view above the current (source) one.
let window = UIApplication.sharedApplication().keyWindow
window?.insertSubview(detailsView, aboveSubview: mapView)
// Animate the transition.
UIView.animateWithDuration(0.4, animations: { () -> Void in
detailsView.frame = CGRectOffset(detailsView.frame, 0.0, -140)
}, completion: nil)
}
}
This seems to be the better solution because I could send some data to the new controller within the prepareForSegue() function AND the viewDidLoad() method is fired every time this segue is initiated but now I'm struggling:
- Everytime the annotation is clicked and the segue is called, a new View is inserted. But if ONE detail view is already visible, I would like to just update this one
- I can't find any way to unwind this segue when didDeselectAnnotationView is called from the MapView.
But perhaps anybody here has even a better way to achieve an UI like this.
I've finally managed to get what I wanted by manually adding a ViewController and a view like this:
let detailsWidth: CGFloat = view.bounds.width
let detailsHeight: CGFloat = 150
let detailsViewFrame: CGRect = CGRectMake(0, view.bounds.height, detailsWidth, detailsHeight)
detailsController = storyboard?.instantiateViewControllerWithIdentifier("details") as! DetailsViewController
detailsController.descriptionText = "I'm a text that was passed from the MainViewController"
self.addChildViewController(detailsController)
detailsController.view.frame = detailsViewFrame
view.addSubview(detailsController.view)
detailsController.didMoveToParentViewController(self)
And when dismissing the Details, I remove the Controller like this:
self.detailsController.removeFromParentViewController()
I created a small sample to demonstrate my solution:
https://github.com/flavordaaave/iOS-Container-View-Overlay

Resources