Keep view portrait but let iOS Rotate with phone - ios

I'm looking for a way to rotate iOS UI, without rotating the game view.
The game I'm working on supports multiple orientations, but the game view takes care of the rotation and all animation related to that orientation change and should be rendered as if it's portrait at all times.
Currently I have the app locked to only support portrait and when the phone is rotated the game rotates. But the home bar and control center stay portrait, which is sort of ok, but I'd like the app to fully support landscape.
As it i now, I listen to orientation notifications
func ListenToRotationUpdates() {
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
NotificationCenter.default.addObserver(
self,
selector: #selector(deviceOrientationDidChange),
name: UIDevice.orientationDidChangeNotification,
object: nil
)
}
Which will set the orientation within the game
#objc func deviceOrientationDidChange() {
let orientation = UIDevice.current.orientation
if orientation == .unknown || orientation == .faceUp || orientation == .faceDown {
return
}
self.gameScene.Set(orientation: orientation)
setNeedsStatusBarAppearanceUpdate()
}
is there a way to keep a view portrait while the app rotates the OS normally?

Well it depends on how are rotating the game view. Do you use some external library that handles it itself? Do you recalculate view frame and bounds? So final execution of your idea really depends on these factors. Generally the most common way of handling rotation in UIViewController is in viewWillTransition function, which handles all rotations animation aligned and happening at the same time. Therefore if you want to somehow recalculate the view back to portrait sizes, I recommend using this function. here is an example:
override func viewWillTransition(to size: CGSize,
with coordinator: UIViewControllerTransitionCoordinator)
{
coordinator.animate( alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in
// calculate your view e.g. self.rotateToCurrentPosition()
}, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
// e.g. self.view.layoutSubviews()
})
super.viewWillTransition(to: size, with: coordinator)
}

One of possible solutions is to split your application to "rotating" part and "non-rotating" part using windows.
It is not an ideal choice but lately the tools that we get do not give us much options. The problem you are facing when using this procedure is that you can have some chaos when presenting new view controllers. In your case this may not be issue at all but still...
In short what you do is:
Leave main window as it is but enable your application to rotate into all directions that you need
Create a view controller that only supports one orientation (whichever you prefer) and show it a new window over your main one but below status bar (Default behavior)
Create a view controller with transparent background and that can pass touch events through to the window below it. Also show it in a new window over the previous one. Also this window needs to pass touch events through to bottom window.
You can create all of these in code or with storyboard. But there are a few components to manage. These are all I used when validating this approach:
class StandStillViewController: WindowViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
func setupManually() {
self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.darkGray
let label = UILabel(frame: .zero)
label.text = "A part of this app that stands still"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addConstraint(.init(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0.0))
let button = UIButton(frame: .zero)
button.setTitle("Test button", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(testButton), for: .touchUpInside)
view.addSubview(button)
view.addConstraint(.init(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 50.0))
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
override var shouldAutorotate: Bool { false }
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait }
#IBAction private func testButton() {
print("Button was pressed")
}
}
This is the controller at the bottom. I expect this one will host your game. You need to preserve
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
override var shouldAutorotate: Bool { false }
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait }
the rest may be changed, removed.
class RotatingViewController: WindowViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
func setupManually() {
self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.clear // Want to see through it
let label = UILabel(frame: .zero)
label.text = "A Rotating part of this app"
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
view.addConstraint(.init(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 60.0))
let button = UIButton(frame: .zero)
button.setTitle("Test rotating button", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(testButton), for: .touchUpInside)
view.addSubview(button)
view.addConstraint(.init(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
view.addConstraint(.init(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: -50.0))
}
#IBAction private func testButton() {
print("Rotating button was pressed")
}
}
This controller will deal with rotating stuff. You need to preserve
self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.clear // Want to see through it
which may both be set in storyboard as well. Anything you put onto this controller will rotate with your device. A nice place to put some GUI stuff for instance.
class WindowViewController: UIViewController {
private var window: UIWindow?
func shownInNewWindow(delegatesTouchEvents: Bool, baseWindow: UIWindow? = nil) {
let scene = baseWindow?.windowScene ?? UIApplication.shared.windows.first!.windowScene!
let newWindow: UIWindow
if delegatesTouchEvents {
newWindow = DelegatedTouchEventsWindow(windowScene: scene)
} else {
newWindow = UIWindow(windowScene: scene)
}
newWindow.rootViewController = self
newWindow.windowLevel = .normal
newWindow.makeKeyAndVisible()
self.window = newWindow
}
func dismissFromWindow(completion: (() -> Void)? = nil) {
removeFromWindow()
completion?()
}
func removeFromWindow() {
self.window?.isHidden = true
self.window = nil
}
}
This is what I used as base class for both view controllers above. It is not much but it allows view controllers to be shown in a new window. This code was pasted from one of my older projects and could use some minor improvements. But it does works so...
class DelegatedTouchEventsView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
class DelegatedTouchEventsWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
These are the two subclasses to let touch events go through. A quick explanation on how this works: When event is received your system will first send this event through your view hierarchy asking "who is going to handle this event?". Returning nil means "not me" and default for UIView is self. So in this code we say: "If any of my subviews wants to handle this event (such as a button) then it may handle this event. But if none of them wants to handle them then neither I will."
And UIWindow is a subclass of UIView so we need to deal with both of them.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// let standingStillController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "StandStillViewController") as! StandStillViewController
// standingStillController.shownInNewWindow(delegatesTouchEvents: false, baseWindow: self.view.window)
//
// let rotatingViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "RotatingViewController") as! RotatingViewController
// rotatingViewController.shownInNewWindow(delegatesTouchEvents: true, baseWindow: self.view.window)
let standingStillController = StandStillViewController()
standingStillController.setupManually()
standingStillController.shownInNewWindow(delegatesTouchEvents: false, baseWindow: self.view.window)
let rotatingViewController = RotatingViewController()
rotatingViewController.setupManually()
rotatingViewController.shownInNewWindow(delegatesTouchEvents: true, baseWindow: self.view.window)
}
}
This is an example on how to use it all together. As promised, either using Storyboards or manually, both should work.
Seems like a lot of work but you need to set it up once and never look at it again.

Related

MacOS Catalyst: how to allow double click on UIView to zoom window?

By default, in MacOS, you can double click the titlebar of a window to zoom it (resize to fit the screen - different from maximize). Double clicking it again brings it back to the previous size.
This works fine on my Catalyst app too. However I need to hide the titlebar and need to give my own custom UIView in that titlebar area the double click behavior. I am able to hide it using this:
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
UIApplication.shared.connectedScenes.forEach({
if let titlebar = ($0 as? UIWindowScene)?.titlebar {
titlebar.titleVisibility = .hidden
titlebar.toolbar = nil
}
})
#endif
Is there a method which lets me toggle the window zoom?
I was able to figure this out using a workaround. UIKit/Catalyst itself doesn't provide any way to do this. But I was able to use the second method outline in this post on
How to Access the AppKit API from Mac Catalyst Apps
https://betterprogramming.pub/how-to-access-the-appkit-api-from-mac-catalyst-apps-2184527020b5
I used the second method and not the first one as the first one seems to be private API (I could be wrong) and will get rejected in App Store. The second method of using a plugin bundle and calling methods on that works well for me. This way I was able to not just perform the zoom, I was also able to perform other MacOS Appkit functionality like listening for keyboard, mouse scroll, hover detection etc.
After creating the plugin bundle, here's my code inside the plugin:
Plugin.swift:
import Foundation
#objc(Plugin)
protocol Plugin: NSObjectProtocol {
init()
func toggleZoom()
func macOSStartupStuff()
}
MacPlugin.swift:
import AppKit
class MacPlugin: NSObject, Plugin {
required override init() {}
func macOSStartupStuff() {
NSApplication.shared.windows.forEach({
$0.titlebarAppearsTransparent = true
$0.titleVisibility = .hidden
$0.backgroundColor = .clear
($0.contentView?.superview?.allSubviews.first(where: { String(describing: type(of: $0)).hasSuffix("TitlebarDecorationView") }))?.alphaValue = 0
})
}
func toggleZoom(){
NSApplication.shared.windows.forEach({
$0.performZoom(nil)
})
}
}
extension NSView {
var allSubviews: [NSView] {
return subviews.flatMap { [$0] + $0.allSubviews }
}
}
Then I call this from my iOS app code. This adds a transparent view at the top where double clicking calls the plugin code for toggling zoom.
NOTE that you must call this from viewDidAppear or somewhere when the windows have been initialized and presented. Otherwise it won't work.
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
#objc func zoomTapped(){
plugin?.toggleZoom()
}
var pluginWasLoaded = false
lazy var plugin : Plugin? = {
pluginWasLoaded = true
if let window = (UIApplication.shared.delegate as? AppDelegate)?.window {
let transparentTitleBarForDoubleClick = UIView(frame: .zero)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(zoomTapped))
tapGesture.numberOfTapsRequired = 2
transparentTitleBarForDoubleClick.addGestureRecognizer(tapGesture)
transparentTitleBarForDoubleClick.isUserInteractionEnabled = true
transparentTitleBarForDoubleClick.backgroundColor = .clear
transparentTitleBarForDoubleClick.translatesAutoresizingMaskIntoConstraints = false
window.addSubview(transparentTitleBarForDoubleClick)
window.bringSubviewToFront(transparentTitleBarForDoubleClick)
window.addConstraints([
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .leading, relatedBy: .equal, toItem: window, attribute: .leading, multiplier: 1, constant: 0),
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .top, relatedBy: .equal, toItem: window, attribute: .top, multiplier: 1, constant: 0),
NSLayoutConstraint(item: transparentTitleBarForDoubleClick, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1, constant: 0),
transparentTitleBarForDoubleClick.bottomAnchor.constraint(equalTo: window.safeTopAnchor)
])
window.layoutIfNeeded()
}
guard let bundleURL = Bundle.main.builtInPlugInsURL?.appendingPathComponent("MacPlugin.bundle") else { return nil }
guard let bundle = Bundle(url: bundleURL) else { return nil }
guard let pluginClass = bundle.classNamed("MacPlugin.MacPlugin") as? Plugin.Type else { return nil }
return pluginClass.init()
}()
#endif
Calling it from viewDidAppear:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
#if os(OSX) || os(macOS) || targetEnvironment(macCatalyst)
if !Singleton.shared.pluginWasLoaded {
Singleton.shared.plugin?.macOSStartupStuff()
}
#endif
}

Setting Constraint programmatically like in storyboard

I need to set constraint to have a view equal height with multiplier 0.8
I use this code
override func willTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
if(presentationStyle == .compact){
let videoController = recordingController.videoController
let containerView = videoController?.containerView
self.addConstraint(value: 0.1, name: "ViewAltezza",videoView: (videoController?.mainView)!, containerView: containerView!)
print("Compact")
}else if(presentationStyle == .expanded){
let videoController = recordingController.videoController
let containerView = videoController?.containerView
self.addConstraint(value: 0.1, name: "ViewAltezza",videoView: (videoController?.mainView)!, containerView: containerView!)
}
print("Expand")
}
}
func addConstraint(value : CGFloat, name: String, videoView: UIView, containerView: UIView){
videoView.translatesAutoresizingMaskIntoConstraints = false
for constraint in videoView.constraints{
if(constraint.identifier == name){
print("Ecco la constraint \(constraint)")
videoView.removeConstraint(constraint)
let heightConstraint = NSLayoutConstraint(item: containerView , attribute: .height, relatedBy: .equal, toItem: videoView, attribute: .height, multiplier: value, constant: 0.0)
heightConstraint.identifier = name
heightConstraint.priority = UILayoutPriority(rawValue: 1000)
print("Aggiungo questa constraint \(heightConstraint)")
videoView.addConstraint(heightConstraint)
}
}
}
The code seems correct, and the debug shows that the initial constraint and the created constraint are the same... is there any errors ? or just I am forgetting something
I need to do this because in iMessage extension when I create a viewController using present(_viewControllerFromStoryboard . . . ), it seems like the view can't auto resize like in MSMessagesAppViewController when the user expand or compact the iMessage app; so if there is any way to avoiding this and let the viewController auto fix the presentationStyle, it will be easier for me to fix the issue I am experimenting.

How do I add constraints to a subview loaded from a nib file?

I'm trying to load a sub view on to every single page of my app from a nib file. Right now I'm using a somewhat unusual approach to loading this sub view in that I am doing it through an extension of UIStoryboard (probably not relevant to my problem, but I'm not sure). So this is how the code looks when I load the nib file:
extension UIStoryboard {
public func appendCustomView(to viewController: UIViewController) {
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
viewController.view.addSubview(myCustomSubview)
}
}
}
This code does what it's supposed to do and adds "MyCustomSubview" to the view controller (I won't go in to detail on exactly how this method gets called because it works so it doesn't seem important). The problem is I can't for the life of me figure out how to add constraints that effect the size of myCustomSubview. I have tried putting code in the function I showed above as well as in the MyCustomSubview swift file to add constraints but no matter what I do the subview never changes.
Ideally the constraints would pin "MyCustomSubview" to the bottom of the ViewController, with width set to the size of the screen and a hard coded height constraint.
Here are the two main methods I tried (with about 100 minor variations for each) that did NOT work:
Method 1 - Add constraint directly from "appendCustomView"
public func appendCustomView(to viewController: UIViewController) {
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
let top = NSLayoutConstraint(item: myCustomSubview, attribute: .top, relatedBy: .equal
, toItem: viewController.view, attribute: .top, multiplier: 1, constant: 50.0)
viewController.view.addSubview(myCustomSubview)
viewController.view.addConstraint(top)
}
}
Method 2 - Add constraint outlets and setter method in MyCustomSubview
class MyCustomSubview: UIView {
#IBOutlet weak var widthConstraint: NSLayoutConstraint!
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
func setConstraints(){
self.widthConstraint.constant = UIScreen.main.bounds.size.width
self.heightConstraint.constant = 20
}
}
And call setter method in "appendCustomView"
public func appendCustomView(to viewController: UIViewController) {
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
myCustomSubview.setConstraints()
viewController.view.addSubview(myCustomSubview)
}
}
(*note: the actual constraints of these examples are irrelevant and I wasn't trying to meet the specs I mentioned above, I was just trying to make any sort of change to the view to know that the constraints were updating. They weren't.)
Edit : Changed "MyCustomNib" to "MyCustomSubview" for clarity.
When you add constraints onto a view from a Nib, you have to call yourView.translatesAutoresizingMaskIntoConstraints = false, and you also need to make sure that you have all 4 (unless it's a label or a few other view types which only need 2) constraints in place:
Here's some sample code that makes a view fill it's parent view:
parentView.addSubview(yourView)
yourView.translatesAutoresizingMaskIntoConstraints = false
yourView.topAnchor.constraint(equalTo: parentView.topAnchor).isActive = true
yourView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor).isActive = true
yourView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor).isActive = true
yourView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor).isActive = true
Edit: I've actually come around to perferring this method of adding NSLayoutConstraints, even though the results are the same
NSLayoutConstraint.activate([
yourView.topAnchor.constraint(equalTo: parentView.topAnchor),
yourView.leadingAnchor.constraint(equalTo: parentView.leadingAnchor),
yourView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor),
yourView.trailingAnchor.constraint(equalTo: parentView.trailingAnchor),
])
For anyone who comes across this in the future, this is the solution I came up with by tweaking this answer a little bit
Add a setConstraints(withRelationshipTo) method in the swift class that corresponds to the nib file:
class MyCustomSubview: UIView {
func setConstraints(withRelationshipTo view: UIView){
self.translatesAutoresizingMaskIntoConstraints = false
// Replace with your own custom constraints
self.heightAnchor.constraint(equalToConstant: 40.0).isActive = true
self.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
self.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
Then call the setConstraints method after you add the nib file to the view (probably in viewWillAppear or viewDidLoad of a view controller )
class MyViewController: UIViewController {
override func viewWillAppear(_ animated: Bool){
super.viewWillAppear(animated)
if let myCustomSubview = Bundle.main.loadNibNamed("MyCustomSubview", owner: nil, options: nil)![0] as? MyCustomSubview {
let view = self.view // Added for clarity
view.addSubview(myCustomSubview)
myCustomSubview.setConstraints(withRelationshipTo: view)
}
}
}
You can use this extension for anywhere you're going to add a subview to a existing UIView.
extension UIView {
func setConstraintsFor(contentView: UIView, left: Bool = true, top: Bool = true, right: Bool = true, bottom: Bool = true) {
contentView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(contentView)
var constraints : [NSLayoutConstraint] = []
if left {
let constraintLeft = NSLayoutConstraint(item: contentView, attribute: .left, relatedBy: .equal, toItem: self, attribute: .left, multiplier: 1, constant: 0)
constraints.append(constraintLeft)
}
if top {
let constraintTop = NSLayoutConstraint(item: contentView, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 0)
constraints.append(constraintTop)
}
if right {
let constraintRight = NSLayoutConstraint(item: contentView, attribute: .right, relatedBy: .equal, toItem: self, attribute: .right, multiplier: 1, constant: 0)
constraints.append(constraintRight)
}
if bottom {
let constraintBottom = NSLayoutConstraint(item: contentView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1, constant: 0)
constraints.append(constraintBottom)
}
self.addConstraints(constraints)
}
}
You can call this method like this:
containerView.setConstraintsFor(contentView: subView!, top: false)
This will add subView to the containerView and constraint to all sides except top side.
You can modify this method to pass left, top, right, bottom Constant value if you want.

View size is automatically being set to window size, when one of window's subview is removed

I have two views one of them is the mainVideoView (Grey color) and the other is the subVideoView (red color). Both of them are the subViews of UIApplication.shared.keyWindow.
When I try and minimise them (using func minimiseOrMaximiseViews, they get minimised (as shown in the below image).
After which, I would like to remove the subVideoView from the window. The moment I try and remove the subVideoView (using func removeSubVideoViewFromVideoView() called from func minimiseOrMaximiseViews), the mainVideoView enlarges to the full screen size, I am not sure why this is happening, I want it to stay at the same size.
Could someone please advise/ suggest how I could achieve this ?
This is how I am setting up the views
func configureVideoView(){
videoView.backgroundColor = UIColor.darkGray
videoView.tag = 0 // To identify view during animation
// ADDING TAP GESTURE TO MAXIMISE THE VIEW WHEN ITS SMALL
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(gestureRecognizer:)))
videoView.addGestureRecognizer(tapGestureRecognizer)
// ADDING PAN GESTURE RECOGNISER TO MAKE VIDEOVIEW MOVABLE INSIDE PARENT VIEW
videoView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.dragView)))
guard let window = UIApplication.shared.keyWindow else{
return
}
window.addSubview(videoView)
videoView.translatesAutoresizingMaskIntoConstraints = false
let viewsDict = ["videoView" : videoView] as [String : Any]
// SETTING CONSTRAINTS
window.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[videoView]|", options: [], metrics: nil, views: viewsDict))
window.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[videoView]|", options: [], metrics: nil, views: viewsDict))
window.layoutIfNeeded() // Lays out subviews immediately
window.bringSubview(toFront: videoView) // To bring subView to front
print("Videoview constraints in configureVideoView \(videoView.constraints)")
}
func configureSubVideoView(){
subVideoView.backgroundColor = UIColor.red
subVideoView.tag = 1 // To identify view during animation
subVideoView.isHidden = true
// Adding Pan Gesture recogniser to make subVideoView movable inside parentview
subVideoView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.dragView)))
// Constraining subVideoView to window to ensure that minimising and maximising animation works properly
constrainSubVideoViewToWindow()
}
func constrainSubVideoViewToWindow(){
//self.subVideoView.setNeedsLayout()
guard let window = UIApplication.shared.keyWindow else{
print("Window does not exist")
return
}
guard !window.subviews.contains(subVideoView)else{ // Does not allow to go through the below code if the window already contains subVideoView
return
}
if self.videoView.subviews.contains(subVideoView){ // If videoView contains subVideoView remove subVideoView
subVideoView.removeFromSuperview()
}
window.addSubview(subVideoView)
// Default constraints to ensure that the subVideoView is initialised with maxSubVideoViewWidth & maxSubVideoViewHeight and is positioned above buttons
let bottomOffset = buttonDiameter + buttonStackViewBottomPadding + padding
subVideoView.translatesAutoresizingMaskIntoConstraints = false
let widthConstraint = NSLayoutConstraint(item: subVideoView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1.0, constant: maxSubVideoViewWidth)
let heightConstraint = NSLayoutConstraint(item: subVideoView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1.0, constant: maxSubVideoViewHeight)
let rightConstraint = NSLayoutConstraint(item: subVideoView, attribute: .trailing, relatedBy: .equal, toItem: window, attribute: .trailing, multiplier: 1.0, constant: -padding)
let bottomConstraint = NSLayoutConstraint(item: subVideoView, attribute: .bottom, relatedBy: .equal, toItem: window, attribute: .bottom, multiplier: 1.0, constant: -bottomOffset)
var constraintsArray = [NSLayoutConstraint]()
constraintsArray.append(widthConstraint)
constraintsArray.append(heightConstraint)
constraintsArray.append(rightConstraint)
constraintsArray.append(bottomConstraint)
window.addConstraints(constraintsArray)
window.layoutIfNeeded() // Lays out subviews immediately
subVideoView.setViewCornerRadius()
window.bringSubview(toFront: subVideoView) // To bring subView to front
}
This is how I am animating views and removing subVideoView
func minimiseOrMaximiseViews(animationType: String){
let window = UIApplication.shared.keyWindow
let buttonStackViewHeight = buttonsStackView.frame.height
if animationType == "maximiseView" {
constrainSubVideoViewToWindow()
}
UIView.animate(withDuration: 0.3, delay: 0, options: [],
animations: { [unowned self] in // "[unowned self] in" added to avoid strong reference cycles,
switch animationType {
case "minimiseView" :
self.hideControls()
// Minimising self i.e videoView
self.videoView.frame = CGRect(x: self.mainScreenWidth - self.videoViewWidth - self.padding,
y: self.mainScreenHeight - self.videoViewHeight - self.padding,
width: self.videoViewWidth,
height: self.videoViewHeight)
// Minimising subVideoView
self.subVideoView.frame = CGRect(x: self.mainScreenWidth - self.minSubVideoViewWidth - self.padding * 2,
y: self.mainScreenHeight - self.minSubVideoViewHeight - self.padding * 2,
width: self.minSubVideoViewWidth,
height: self.minSubVideoViewHeight)
window?.layoutIfNeeded()
self.videoView.setViewCornerRadius()
self.subVideoView.setViewCornerRadius()
print("self.subVideoView.frame AFTER setting: \(self.subVideoView.frame)")
default:
break
}
}) { [unowned self] (finished: Bool) in // This will be called when the animation completes, and its finished value will be true
if animationType == "minimiseView" {
// **** Removing subVideoView from videoView ****
self.removeSubVideoViewFromVideoView()
}
}
}
func removeSubVideoViewFromVideoView(){
//self.subVideoView.setNeedsLayout()
guard let window = UIApplication.shared.keyWindow else{
return
}
guard !self.videoView.subviews.contains(subVideoView)else{ // Does not allow to go through the below code if the videoView already contains subVideoView
return
}
if window.subviews.contains(subVideoView) { // removing subVideoView from window
subVideoView.removeFromSuperview() // *** CAUSE OF ISSUE ***
}
guard !window.subviews.contains(subVideoView) else{
return
}
videoView.addSubview(subVideoView)
}
The videoView and subVideoView are layout by Auto Layout. In Auto Layout if you want to change the frame, you have to update the constraints.
Calling removeFromSuperView will removes any constraints that refer to the view you are removing, or that refer to any view in the subtree of the view you are removing. This means the auto layout system will to rearrange views.

how could I add a uiactivity indicator in in bottom/footer of collection view?

i am new to swift . i can not figure how can i do it . basically i need to show uiactivity indicator when collection view loaded all data then you try to scrolling then showing uiactivity indicator . it mean load more data from web just wait .
what i did .
///define
var indicatorFooter : UIActivityIndicatorView!
//set up UIActivityIndicatorView to the collection view
override func setupViews() {
indicatorFooter = UIActivityIndicatorView(frame: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(collectionView.frame.width), height: CGFloat(44)))
indicatorFooter.color = UIColor.black
collectionView.addSubview(indicatorFooter)
}
//end of the scroll view then load next 20 data from api
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// UITableView only moves in one direction, y axis
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
// Change 10.0 to adjust the distance from bottom
if maximumOffset - currentOffset <= 10.0 {
self.loadMore()
}
}
//request web to down load data
func loadMore(){
//but it is not working indicator
indicatorFooter.startAnimating()
}
Add the following extension to your code:
extension UIView {
// MARK: Activity Indicator
func activityIndicator(show: Bool) {
activityIndicator(show: show, style: .gray)
}
func activityIndicator(show: Bool, style: UIActivityIndicatorViewStyle) {
var spinner: UIActivityIndicatorView? = viewWithTag(NSIntegerMax - 1) as? UIActivityIndicatorView
if spinner != nil {
spinner?.removeFromSuperview()
spinner = nil
}
if spinner == nil && show {
spinner = UIActivityIndicatorView.init(activityIndicatorStyle: style)
spinner?.translatesAutoresizingMaskIntoConstraints = false
spinner?.hidesWhenStopped = true
spinner?.tag = NSIntegerMax - 1
if Thread.isMainThread {
spinner?.startAnimating()
} else {
DispatchQueue.main.async {
spinner?.startAnimating()
}
}
insertSubview((spinner)!, at: 0)
NSLayoutConstraint.init(item: spinner!, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1.0, constant: 0.0).isActive = true
NSLayoutConstraint.init(item: spinner!, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1.0, constant: 0.0).isActive = true
spinner?.isHidden = !show
}
}
}
To add a spinner to your collectionView you would simply call:
collectionView.activityIndicator(show: true)

Resources