How to change volume programmatically on iOS 11.4 - ios

Before, I was setting sound volume programmatically using this approach:
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
UISlider *volumeViewSlider = nil;
for (UIView *view in [volumeView subviews])
{
if ([view.class.description isEqualToString:#"MPVolumeSlider"])
{
volumeViewSlider = (UISlider *)view;
break;
}
}
[volumeViewSlider setValue:0.5 animated:YES];
[volumeViewSlider sendActionsForControlEvents:UIControlEventTouchUpInside];
Till iOS 11.4 it was working well (even on iOS 11.3), but on iOS 11.4 it doesn't. Volume value remains unchanged. Can someone help with this issue? Thanks.

Changing volumeViewSlider.value after a small delay resolves problem.
- (IBAction)increase:(id)sender {
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
UISlider *volumeViewSlider = nil;
for (UIView *view in volumeView.subviews) {
if ([view isKindOfClass:[UISlider class]]) {
volumeViewSlider = (UISlider *)view;
break;
}
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
volumeViewSlider.value = 0.5f;
});
}
Swift version

I solved it by adding new MPVolumeView to my UIViewController view, otherwise it didn't set the volume anymore. As I added it to the controller I also need to set the volume view position to be outside of the screen to hide it from the user.
I prefer not to use delayed volume setting as it make things more complicated especially if you need to play sound immediately after setting the volume.
The code is in Swift 4:
let volumeControl = MPVolumeView(frame: CGRect(x: 0, y: 0, width: 120, height: 120))
override func viewDidLoad() {
self.view.addSubview(volumeControl);
}
override func viewDidLayoutSubviews() {
volumeControl.frame = CGRect(x: -120, y: -120, width: 100, height: 100);
}
func setVolume(_ volume: Float) {
let lst = volumeControl.subviews.filter{NSStringFromClass($0.classForCoder) == "MPVolumeSlider"}
let slider = lst.first as? UISlider
slider?.setValue(volume, animated: false)
}

I just added the MPVolumeView as a subview to another view (that was never drawn on screen).
This had to be done prior to any attempt to set or get the volume.
private let containerView = UIView()
private let volumeView = MPVolumeView()
func prepareWorkaround() {
self.containerView.addSubview(self.volumeView)
}

I had to have a MPVolumeView as subview to a view in the hierarchy for the hud not to show up on iOS 12. It needs to be slightly visible:
let volume = MPVolumeView(frame: .zero)
volume.setVolumeThumbImage(UIImage(), for: UIControl.State())
volume.isUserInteractionEnabled = false
volumelume.alpha = 0.0001
volume.showsRouteButton = false
view.addSubview(volume)
When setting the volume I get the slider from MPVolumeView as with previous posters and set the value:
func setVolumeLevel(_ volumeLevel: Float) {
guard let slider = volume.subviews.compactMap({ $0 as? UISlider }).first else {
return
}
slider.value = volumeLevel
}

Related

Using same UIActivityIndicatorView in many view controllers

I have a simple iOS app with various view controllers.
Each view controller has different functionality but each view controller has 'load' button, that when triggered, sending a request and getting a result to delegate method.
I want to use an UIActivityIndicatorView that will start when the user will click the button and will stop on the delegate method.
Obviously, I want the indicator to look the same on each VC, so I've made property of it, and on each viewDidLoad method I am using this code:
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
self.indicator.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.6f];
self.indicator.frame = CGRectMake(40.0, 20.0, 100.0, 100.0);
self.indicator.center = self.view.center;
The problem is, I am using the same parameters, on the same object, copping and pasting these lines on every view controller.
Let's say I want to change the style in the next version, I need to change it 10 times.
What would be the best way to use some kind of static indicator that would be set with these parameters and would be set on and off by demand?
Here is the one i use in swift 4.1
import UIKit
class ProgressView {
// MARK: - Variables
private var containerView = UIView()
private var progressView = UIView()
private var activityIndicator = UIActivityIndicatorView()
static var shared = ProgressView()
// To close for instantiation
private init() {}
// MARK: - Functions
func startAnimating(view: UIView = (UIApplication.shared.keyWindow?.rootViewController?.view)!) {
containerView.center = view.center
containerView.frame = view.frame
containerView.backgroundColor = UIColor(hex: 0xffffff, alpha: 0.5)
progressView.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
progressView.center = containerView.center
progressView.backgroundColor = UIColor(hex: 0x444444, alpha: 0.7)
progressView.clipsToBounds = true
progressView.cornerRadius = 10
activityIndicator.frame = CGRect(x: 0, y: 0, width: 60, height: 60)
activityIndicator.center = CGPoint(x: progressView.bounds.width/2, y: progressView.bounds.height/2)
activityIndicator.style = .whiteLarge
view.addSubview(containerView)
containerView.addSubview(progressView)
progressView.addSubview(activityIndicator)
activityIndicator.startAnimating()
}
/// animate UIActivityIndicationView without blocking UI
func startSmoothAnimation(view: UIView = (UIApplication.shared.keyWindow?.rootViewController?.view)!) {
activityIndicator.frame = CGRect(x: 0, y: 0, width: 60, height: 60)
activityIndicator.center = view.center
activityIndicator.style = .whiteLarge
activityIndicator.color = UIColor.gray
view.addSubview(activityIndicator)
activityIndicator.startAnimating()
}
func stopAnimatimating() {
activityIndicator.stopAnimating()
containerView.removeFromSuperview()
}
}
extension UIColor {
convenience init(hex: UInt32, alpha: CGFloat) {
let red = CGFloat((hex & 0xFF0000) >> 16)/256.0
let green = CGFloat((hex & 0xFF00) >> 8)/256.0
let blue = CGFloat(hex & 0xFF)/256.0
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
}
// usage
ProgressView.shared.startAnimating()
// to stop
ProgressView.shared.stopAnimatimating()
Hope it helps
I would suggest that you create a superclass to your view controllers and add the spinner functionality there, and let your view controllers inherit from it.
The superclass view controller would look something like this:
// .h-file
#interface SuperclassViewController : UIViewController
- (void)showIndicator;
- (void)hideIndicator;
#end
// .m file
#import "SuperclassViewController.h"
#interface SuperclassViewController ()
#property (nonatomic, strong) UIActivityIndicatorView *indicator;
#end
#implementation SuperclassViewController
- (void)viewDidLoad {
[super viewDidLoad];
_indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
self.indicator.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.6f];
self.indicator.frame = CGRectMake(40.0, 20.0, 100.0, 100.0);
self.indicator.layer.cornerRadius = 6;
self.indicator.center = self.view.center;
[self.indicator startAnimating];
}
- (void)showIndicator {
[self.view addSubview:self.indicator];
}
- (void)hideIndicator {
[self.indicator removeFromSuperview];
}
#end
Now, to inherit it do the following in your view controllers .h file:
#import "SuperclassViewController.h"
#interface YourViewController : SuperclassViewController;
/** properties and methods */
#end
Then you can call [self showIndicator] and [self hideIndicator] in your view controllers whenever needed without any extra coding.
You can create single view controller to display loading indicator in all view controller. You need to write code once, put following code in AppDelegate file.
Note: I'm not working in Objective-C, following code in Swift. So you need to transform code in objective C.
First add following code in ProgressVC:
ProgressVC.swift:
class func viewController() -> ProgressVC {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "ProgressVC") as! ProgressVC
}
Add following code in your AppDelegate.
AppDelegate.swift:
var progressVC : ProgressVC?
static let shared = UIApplication.shared.delegate as! AppDelegate
func showLoading(isShow: Bool) {
if isShow {
// Remove progress view if already exist
if progressVC != nil {
progressVC?.view.removeFromSuperview()
progressVC = nil
}
progressVC = ProgressVC.viewController()
AppDelegate.shared.window?.addSubview((progressVC?.view)!)
} else {
if progressVC != nil {
progressVC?.view.removeFromSuperview()
}
}
}
Now, you need to call just above method with AppDelegate's shared instance. Enable animated property of UIActivityIndicatorView from storyboard.
Show:
AppDelegate.shared.showLoading(isShow: true)
Hide:
AppDelegate.shared.showLoading(isShow: false)
Screenshot:
You could create the activity indicator in the UIWindow and then you could show/hide it from any UIViewController.
To get the window use:
UIApplication.shared.keyWindow
Thank you all for your assistance,
I decided to make a singleton class that has a variable of UIActivityIndicatorView.
This is the declaration of the class:
#import "ProgressView.h"
#interface ProgressView()
#property (nonatomic) UIActivityIndicatorView *indicator;
+(ProgressView *)shared;
#end
#implementation ProgressView
+ (ProgressView *)shared {
static ProgressView* sharedVC = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedVC = [[self alloc] init];
});
return sharedVC;
}
- (instancetype)init {
self = [super init];
if (self) {
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
self.indicator.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.6f];
self.indicator.frame = CGRectMake(40.0, 20.0, 100.0, 100.0);
}
return self;
}
- (void)startAnimation:(UIView *)view {
self.indicator.center = view.center;
self.indicator.hidden = NO;
[self.indicator startAnimating];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, 12 * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if ([self.indicator isAnimating])
[self stopAnimation];
});
[view addSubview:self.indicator];
}
- (void)stopAnimation {
if ([self.indicator isAnimating]) {
[self.indicator stopAnimating];
[self.indicator removeFromSuperview];
}
}
#end
Please note I have added a rule that if the indicator didn't get triggered to stop in 12 seconds the class would stop the indicator by itself.
Now, all I have to do is to add this line in every place in my code where I would like to start the indicator:
[[ProgressView shared] startAnimation:self.view];
And to add this line to stop it:
[[ProgressView shared] stopAnimation];

Negative spacer for UIBarButtonItem in navigation bar on iOS 11

In iOS 10 and below, there was a way to add a negative spacer to the buttons array in the navigation bar, like so:
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
negativeSpacer.width = -8;
self.navigationItem.leftBarButtonItems = #[negativeSpacer, [self backButtonItem]];
This no longer works on iOS 11 (the spacer becomes positive, instead of negative). I have inspected the view hierarchy of the bar button item, and it is now embedded into _UIButtonBarStackView. How to adjust the position of the bar button on iOS 11?
EDIT:
This may no longer work as of iOS 13. You may get the error:
Client error attempting to change layout margins of a private view
OLD ANSWER:
I found a somewhat hacky solution on the Apple developer forums:
https://forums.developer.apple.com/thread/80075
It looks like the problem comes from how iOS 11 handles the UIBarButtonItem .fixedSpace buttons and how a UINavigationBar is laid out in iOS 11. The navigation bars now use autolayout and the layout margins to layout the buttons. The solution presented in that post (at the bottom) was to set all the layout margins to some value you want.
class InsetButtonsNavigationBar: UINavigationBar {
override func layoutSubviews() {
super.layoutSubviews()
for view in subviews {
// Setting the layout margins to 0 lines the bar buttons items up at
// the edges of the screen. You can set this to any number to change
// the spacing.
view.layoutMargins = .zero
}
}
}
To use this new nav bar with custom button spacing, you will need to update where you create any navigation controllers with the following code:
let navController = UINavigationController(navigationBarClass: InsetButtonsNavigationBar.self,
toolbarClass: UIToolbar.self)
navController.viewControllers = [yourRootViewController]
Just a workaround for my case, it might be helpful to some people. I would like to achieve this:
and previously I was using the negativeSpacer as well. Now I figured out this solution:
let logoImage = UIImage(named: "your_image")
let logoImageView = UIImageView(image: logoImage)
logoImageView.frame = CGRect(x: -16, y: 0, width: 150, height: 44)
logoImageView.contentMode = .scaleAspectFit
let logoView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))
**logoView.clipsToBounds = false**
logoView.addSubview(logoImageView)
let logoItem = UIBarButtonItem(customView: logoView)
navigationItem.leftBarButtonItem = logoItem
Based on keithbhunter's answer I've created a custom UINavigationBar:
NavigationBarCustomMargins.h:
#import <UIKit/UIKit.h>
#interface NavigationBarCustomMargins : UINavigationBar
#property (nonatomic) IBInspectable CGFloat leftMargin;
#property (nonatomic) IBInspectable CGFloat rightMargin;
#end
NavigationBarCustomMargins.m:
#import "NavigationBarCustomMargins.h"
#define DefaultMargin 16
#define NegativeSpacerTag 87236223
#interface NavigationBarCustomMargins ()
#property (nonatomic) BOOL leftMarginIsSet;
#property (nonatomic) BOOL rightMarginIsSet;
#end
#implementation NavigationBarCustomMargins
#synthesize leftMargin = _leftMargin;
#synthesize rightMargin = _rightMargin;
- (void)layoutSubviews {
[super layoutSubviews];
if (([[[UIDevice currentDevice] systemVersion] compare:#"11.0" options:NSNumericSearch] != NSOrderedAscending)) {
BOOL isRTL = [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
for (UIView *view in self.subviews) {
view.layoutMargins = UIEdgeInsetsMake(0, isRTL ? self.rightMargin : self.leftMargin, 0, isRTL ? self.leftMargin : self.rightMargin);
}
} else {
//left
NSMutableArray *leftItems = [self.topItem.leftBarButtonItems mutableCopy];
if (((UIBarButtonItem *)leftItems.firstObject).tag != NegativeSpacerTag) {
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
negativeSpacer.tag = NegativeSpacerTag;
negativeSpacer.width = self.leftMargin - DefaultMargin;
[leftItems insertObject:negativeSpacer atIndex:0];
[self.topItem setLeftBarButtonItems:[leftItems copy] animated:NO];
}
//right
NSMutableArray *rightItems = [self.topItem.rightBarButtonItems mutableCopy];
if (((UIBarButtonItem *)rightItems.firstObject).tag != NegativeSpacerTag) {
UIBarButtonItem *negativeSpacer = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
negativeSpacer.tag = NegativeSpacerTag;
negativeSpacer.width = self.rightMargin - DefaultMargin;
[rightItems insertObject:negativeSpacer atIndex:0];
[self.topItem setRightBarButtonItems:[rightItems copy] animated:NO];
}
}
}
- (CGFloat)leftMargin {
if (_leftMarginIsSet) {
return _leftMargin;
}
return DefaultMargin;
}
- (CGFloat)rightMargin {
if (_rightMarginIsSet) {
return _rightMargin;
}
return DefaultMargin;
}
- (void)setLeftMargin:(CGFloat)leftMargin {
_leftMargin = leftMargin;
_leftMarginIsSet = YES;
}
- (void)setRightMargin:(CGFloat)rightMargin {
_rightMargin = rightMargin;
_rightMarginIsSet = YES;
}
#end
After that I set custom class to my UINavigationController in Interface Builder and just set needed margins:
Screenshot 1
Works fine. Supports RTL and iOS prior 11:
Screenshot 2
Another way is that , you can wrapper your content to a offset view
class CustomBarItemView : UIView {
var offsetContentView : UIView = UIView.init(frame: .zero)
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(offsetContentView)
offsetContentView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 0, left: -8, bottom: 0, right: 0))
}
// implement add your content on offsetContentView
// todo ...
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
because CustomBarItemView layer not use maskToBounds = true,so it looks like OK
let naviItem = UIBarButtonItem.init(customView: CustomBarItemView())
For me this answer help https://stackoverflow.com/a/44896832
In particular i've set both imageEdgeInsets and titleEdgeInsets because my button has image and title together

Can't set titleView in the center of navigation bar because back button

I'm using an image view to display an image in my nav bar. The problem is that I can't set it to the center correctly because of the back button. I checked the related questions and had almost the same problem earlier that I solved, but this time I have no idea.
Earlier I solved this problem with fake bar buttons, so I tried to add a fake bar button to the right (and left) side, but it doesn't helped.
- (void) searchButtonNavBar {
CGRect imageSizeDummy = CGRectMake(0, 0, 25,25);
UIButton *dummy = [[UIButton alloc] initWithFrame:imageSizeDummy];
UIBarButtonItem
*searchBarButtonDummy =[[UIBarButtonItem alloc] initWithCustomView:dummy];
self.navigationItem.rightBarButtonItem = searchBarButtonDummy;
}
- (void)setNavBarLogo {
[self setNeedsStatusBarAppearanceUpdate];
CGRect myImageS = CGRectMake(0, 0, 44, 44);
UIImageView *logo = [[UIImageView alloc] initWithFrame:myImageS];
[logo setImage:[UIImage imageNamed:#"color.png"]];
logo.contentMode = UIViewContentModeScaleAspectFit;
self.navigationItem.titleView = logo;
[[UIBarButtonItem appearance] setTitlePositionAdjustment:UIOffsetMake(0.0f, 0.0f) forBarMetrics:UIBarMetricsDefault];
}
I think it should be workin fine because in this case the titleView has bar buttons on the same side. Is there any explanation why it worked with bar buttons that was created programmatically but doesn't works with the common back button?
UINavigationBar automatically centers its titleView as long as there is enough room. If the title isn't centered that means that the title view is too wide to be centered, and if you set the backgroundColor if your UIImageView you'll see that's exactly what is happening.
The title view is too wide because that navigation bar will automatically resize the title to hold its content, using -sizeThatFits:. This means that your title view will always be resized to the size of your image.
Two possible fixes:
The image you're using is way too big. Use a properly sized 44x44 pt image with 2x and 3x versions.
Wrap UIImageView inside of a regular UIView to avoid resizing.
Example:
UIImageView* imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"test.jpeg"]];
imageView.contentMode = UIViewContentModeScaleAspectFit;
UIView* titleView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 44, 44)];
imageView.frame = titleView.bounds;
[titleView addSubview:imageView];
self.navigationItem.titleView = titleView;
An example in Swift 3 version of Darren's second way:
let imageView = UIImageView(image: UIImage(named: "test"))
imageView.contentMode = UIViewContentMode.scaleAspectFit
let titleView = UIView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
imageView.frame = titleView.bounds
titleView.addSubview(imageView)
self.navigationItem.titleView = titleView
I suggest you Override the function - (void)setFrame:(CGRect)fram
like this:
- (void)setFrame:(CGRect)frame {
[super setFrame:frame]; //systom function
self.center = CGPointMake(self.superview.center.x, self.center.y); //rewrite function
}
so that the titleView.center always the right location
Don't use titleView.
Just add your image to navigationController.navigationBar
CGRect myImageS = CGRectMake(0, 0, 44, 44);
UIImageView *logo = [[UIImageView alloc] initWithFrame:myImageS];
[logo setImage:[UIImage imageNamed:#"color.png"]];
logo.contentMode = UIViewContentModeScaleAspectFit;
logo.center = CGPointMake(self.navigationController.navigationBar.width / 2.0, self.navigationController.navigationBar.height / 2.0);
logo.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
[self.navigationController.navigationBar addSubview:logo];
Qun Li's worked perfectly for me. Here's the swift 2.3 code:
override var frame: CGRect {
set(newValue) {
super.frame = newValue
if let superview = self.superview {
self.center = CGPoint(x: superview.center.x, y: self.center.y)
}
}
get {
return super.frame
}
}
If you're using a custom view from a nib, be sure to disable auto-layout on the nib file.
I created a custom UINavigationController that after dropping in, the only thing you have to do is call showNavBarTitle(title:font:) when you want to show and removeNavBarTitle() when you want to hide:
class NavigationController: UINavigationController {
private static var mTitleFont = UIFont(name: <your desired font (String)> , size: <your desired font size -- however, font size will automatically adjust so the text fits in the label>)!
private static var mNavBarLabel: UILabel = {
let x: CGFloat = 60
let y: CGFloat = 7
let label = UILabel(frame: CGRect(x: x, y: y, width: UIScreen.main.bounds.size.width - 2 * x, height: 44 - 2 * y))
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.5
label.font = NavigationController.mTitleFont
label.numberOfLines = 0
label.textAlignment = .center
return label
}()
func showNavBarLabel(title: String, font: UIFont = mTitleFont) {
NavigationController.mNavBarLabel.text = title
NavigationController.mNavBarLabel.font = font
navigationBar.addSubview(NavigationController.mNavBarLabel)
}
func removeNavBarLabel() {
NavigationController.mNavBarLabel.removeFromSuperview()
}
}
I find the best place to call showNavBarTitle(title:font:) and removeNavBarTitle() are in the view controller's viewWillAppear() and viewWillDisappear() methods, respectively:
class YourViewController: UIViewController {
func viewWillAppear() {
(navigationController as! NavigationController).showNavBarLabel(title: "Your Title")
}
func viewWillDisappear() {
(navigationController as! NavigationController).removeNavBarLabel()
}
}
1) You can try setting your image as UINavigationBar's background image by calling
[self.navigationController.navigationBar setBackgroundImage:[UIImage imageNamed:#"color.png"] forBarMetrics:UIBarMetricsDefault];
inside the viewDidLoad method.
That way it will be always centered, but if you have back button with long title as left navigation item, it can appear on top of your logo. And you should probably at first create another image with the same size as the navigation bar, then draw your image at its center, and after that set it as the background image.
2) Or instead of setting your image view as titleView, you can try simply adding at as a subview, so it won't have the constraints related to right and left bar button items.
In Swift, this is what worked for me however it´s not the best solution (basically, add it up to navigationBar):
let titleIV = UIImageView(image: UIImage(named:"some"))
titleIV.contentMode = .scaleAspectFit
titleIV.translatesAutoresizingMaskIntoConstraints = false
if let navigationController = self.navigationController{
navigationController.navigationBar.addSubview(titleIV)
titleIV.centerXAnchor.constraint(equalTo:
navigationController.navigationBar.centerXAnchor).isActive = true
titleIV.centerYAnchor.constraint(equalTo: navigationController.navigationBar.centerYAnchor).isActive = true
}
else{
view.addSubview(titleIV)
titleIV.topAnchor.constraint(equalTo: view.topAnchor, constant: UIApplication.shared.statusBarFrame.height).isActive = true
titleIV.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
Extending Darren's answer, a fix for me was to return a sizeThatFits with the UILabel size. It turns out that this is called after layoutSubViews so the label has a size.
override func sizeThatFits(_ size: CGSize) -> CGSize {
return CGSize(width: titleLabel.frame.width + titleInset*2, height: titleLabel.frame.height)
}
Also note that I have + titleInset*2 because Im setting the horizontal constraints like so:
titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: titleInset),
titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -titleInset)

Custom inputView with dynamic height in iOS 8

I have some trouble with my custom inputView for UITextFields. Depending on the text the user needs to input in a UITextField, the inputView displays only the needed letters. That means for short texts, an inputView with only one line of letters is sufficient, longer texts may require 2 or even 3 lines so the height of the inputView is variabel.
Since I was expecting better performance, there exists only one inputView instance that is used by every textField. That way the creation must only happen once and it made the sometimes needed direct access to the inputView easier. The inputView is set up in - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField, sets its required height and will be shown.
That works perfectly, but not on iOS8. There some system view containing the inputView will not update its frame to match the inputView's bounds when they are changed (first time works).
I know that can be fixed by using one instance of my inputView per textField. But I'm asking if there is a recommended/better way to adjust the frame or to report its change to the containing view. Maybe it is an iOS8 bug that could be fixed until release?
Here's some example code to reproduce the issue:
CustomInputView
#implementation CustomInputView
+ (CustomInputView*)sharedInputView{
static CustomInputView *sharedInstance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[CustomInputView alloc] init];
});
return sharedInstance;
}
- (id)init
{
self = [super init];
if (self) {
self.backgroundColor = [UIColor greenColor];
}
return self;
}
- (void)setupForTextField:(UITextField*)textField{
CGFloat height;
if(textField.tag == 1){
height = 100;
}else height = 50;
self.frame = CGRectMake(0, 0, 320, height);
}
#end
TestViewController code
- (void)viewDidLoad
{
[super viewDidLoad];
UITextField *tf = [[UITextField alloc] initWithFrame:CGRectMake(15, 50, 290, 30)];
tf.text = #"bigKeyboard";
tf.inputView = [CustomInputView sharedInputView];
tf.layer.borderWidth = 1;
tf.layer.borderColor = [UIColor lightGrayColor].CGColor;
tf.delegate = self;
tf.tag = 1;
[self.view addSubview:tf];
tf = [[UITextField alloc] initWithFrame:CGRectMake(15, 100, 290, 30)];
tf.text = #"smallKeyboard";
tf.inputView = [CustomInputView sharedInputView];
tf.layer.borderWidth = 1;
tf.layer.borderColor = [UIColor lightGrayColor].CGColor;
tf.delegate = self;
tf.tag = 2;
[self.view addSubview:tf];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:#"dismissKeyboard" forState:UIControlStateNormal];
[button addTarget:self action:#selector(endEditing) forControlEvents:UIControlEventTouchUpInside];
button.frame = CGRectMake(15, 150, 290, 30);
[self.view addSubview:button];
}
- (void)endEditing{
[self.view endEditing:YES];
}
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField{
[[CustomInputView sharedInputView] setupForTextField:textField];
return YES;
}
I had similar issues with sizing a custom keyboard from iOS 8 to iOS 10. I believe the proper solution is to have the input view provide a proper intrinsicContentSize and change (and invalidate!) that value when you want to change the view's height. Sample code:
class CustomInputView: UIInputView {
var intrinsicHeight: CGFloat = 200 {
didSet {
self.invalidateIntrinsicContentSize()
}
}
init() {
super.init(frame: CGRect(), inputViewStyle: .keyboard)
self.translatesAutoresizingMaskIntoConstraints = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.translatesAutoresizingMaskIntoConstraints = false
}
override var intrinsicContentSize: CGSize {
return CGSize(width: UIViewNoIntrinsicMetric, height: self.intrinsicHeight)
}
}
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
textView.becomeFirstResponder()
let inputView = CustomInputView()
// To make the view's size more clear.
inputView.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0.5, alpha: 1)
textView.inputView = inputView
// To demonstrate a change to the view's intrinsic height.
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) {
inputView.intrinsicHeight = 400
}
}
}
See also https://stackoverflow.com/a/40359382/153354.
Another thing which I found critical for resizing an inputView on iOS 9 and up is setting allowsSelfSizing to true:
if #available(iOS 9.0, *) {
self.inputView?.allowsSelfSizing = true
}

iOS Floating Video Window like Youtube App

Does anyone know of any existing library, or any techniques on how to get the same effect as is found on the Youtube App.
The video can be "minimised" and hovers at the bottom of the screen - which can then be swiped to close or touched to re-maximised.
See:
Video Playing Normally: https://www.dropbox.com/s/o8c1ntfkkp4pc4q/2014-06-07%2001.19.20.png
Video Minimized: https://www.dropbox.com/s/w0syp3infu21g08/2014-06-07%2001.19.27.png
(Notice how the video is now in a small floating window on the bottom right of the screen).
Anyone have any idea how this was achieved, and if there are any existing tutorials or libraries that can be used to get this same effect?
It sounded fun, so I looked at youtube. The video looks like it plays in a 16:9 box at the top, with a "see also" list below. When user minimizes the video, the player drops to the lower right corner along with the "see also" view. At the same time, that "see also" view fades to transparent.
1) Setup the views like that and created outlets. Here's what it looks like in IB. (Note that the two containers are siblings)
2) Give the video view a swipe up and swipe down gesture recognizer:
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UIView *tallMpContainer;
#property (weak, nonatomic) IBOutlet UIView *mpContainer;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UISwipeGestureRecognizer *swipeDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipeDown:)];
UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipeUp:)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;
swipeDown.direction = UISwipeGestureRecognizerDirectionDown;
[self.mpContainer addGestureRecognizer:swipeUp];
[self.mpContainer addGestureRecognizer:swipeDown];
}
- (void)swipeDown:(UIGestureRecognizer *)gr {
[self minimizeMp:YES animated:YES];
}
- (void)swipeUp:(UIGestureRecognizer *)gr {
[self minimizeMp:NO animated:YES];
}
3) And then a method to know about the current state, and change the current state.
- (BOOL)mpIsMinimized {
return self.tallMpContainer.frame.origin.y > 0;
}
- (void)minimizeMp:(BOOL)minimized animated:(BOOL)animated {
if ([self mpIsMinimized] == minimized) return;
CGRect tallContainerFrame, containerFrame;
CGFloat tallContainerAlpha;
if (minimized) {
CGFloat mpWidth = 160;
CGFloat mpHeight = 90; // 160:90 == 16:9
CGFloat x = 320-mpWidth;
CGFloat y = self.view.bounds.size.height - mpHeight;
tallContainerFrame = CGRectMake(x, y, 320, self.view.bounds.size.height);
containerFrame = CGRectMake(x, y, mpWidth, mpHeight);
tallContainerAlpha = 0.0;
} else {
tallContainerFrame = self.view.bounds;
containerFrame = CGRectMake(0, 0, 320, 180);
tallContainerAlpha = 1.0;
}
NSTimeInterval duration = (animated)? 0.5 : 0.0;
[UIView animateWithDuration:duration animations:^{
self.tallMpContainer.frame = tallContainerFrame;
self.mpContainer.frame = containerFrame;
self.tallMpContainer.alpha = tallContainerAlpha;
}];
}
I didn't add video to this project, but it should just drop in. Make the mpContainer the parent view of the MPMoviePlayerController's view and it should look pretty cool.
Use TFSwipeShrink and customize code for your project.
hope to help you.
Update new framwork FWDraggableSwipePlayer for drag uiview like YouTube app.
hope to help you.
This is a swift 3 version for the answer #danh had provided earlier.
https://stackoverflow.com/a/24107949/1211470
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tallMpContainer: UIView!
#IBOutlet weak var mpContainer: UIView!
var swipeDown: UISwipeGestureRecognizer?
var swipeUp: UISwipeGestureRecognizer?
override func viewDidLoad() {
super.viewDidLoad()
swipeDown = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownAction))
swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpAction))
swipeDown?.direction = .down
swipeUp?.direction = .up
self.mpContainer.addGestureRecognizer(swipeDown!)
self.mpContainer.addGestureRecognizer(swipeUp!)
}
#objc func swipeDownAction() {
minimizeWindow(minimized: true, animated: true)
}
#objc func swipeUpAction() {
minimizeWindow(minimized: false, animated: true)
}
func isMinimized() -> Bool {
return CGFloat((self.tallMpContainer?.frame.origin.y)!) > CGFloat(20)
}
func minimizeWindow(minimized: Bool, animated: Bool) {
if isMinimized() == minimized {
return
}
var tallContainerFrame: CGRect
var containerFrame: CGRect
var tallContainerAlpha: CGFloat
if minimized == true {
let mpWidth: CGFloat = 160
let mpHeight: CGFloat = 90
let x: CGFloat = 320-mpWidth
let y: CGFloat = self.view.bounds.size.height - mpHeight;
tallContainerFrame = CGRect(x: x, y: y, width: 320, height: self.view.bounds.size.height)
containerFrame = CGRect(x: x, y: y, width: mpWidth, height: mpHeight)
tallContainerAlpha = 0.0
} else {
tallContainerFrame = self.view.bounds
containerFrame = CGRect(x: 0, y: 0, width: 320, height: 180)
tallContainerAlpha = 1.0
}
let duration: TimeInterval = (animated) ? 0.5 : 0.0
UIView.animate(withDuration: duration, animations: {
self.tallMpContainer.frame = tallContainerFrame
self.mpContainer.frame = containerFrame
self.tallMpContainer.alpha = tallContainerAlpha
})
}
}

Resources