Customize dot with image of UIPageControl at index 0 of UIPageControl - iphone

I'm a new learner of ios programming. I have tried to search with another example and more questions at stackoverflow but it's not my goal. I want to set an image of dot at index 0 of UIPageControl as similar as iPhone search homescreen. Have any way to do it ? Please explain me with some code or another useful link.
Thanks in advance

I have found a solution for this problem. I know it is not the way but it will work till iOS 8 will be launched in the market.
Reason for Crash:
in iOS 7 [self.subViews objectAtIndex: i] returns UIView Instead of UIImageView and setImage is not the property of UIView and the app crashes. I solve my problem using this following code.
Check Whether the subview is UIView(for iOS7) or UIImageView(for iOS6 or earlier). And If it is UIView I am going to add UIImageView as subview on that view and voila its working and not crash..!!
-(void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView * dot = [self imageViewForSubview: [self.subviews objectAtIndex: i]];
if (i == self.currentPage) dot.image = activeImage;
else dot.image = inactiveImage;
}
}
- (UIImageView *) imageViewForSubview: (UIView *) view
{
UIImageView * dot = nil;
if ([view isKindOfClass: [UIView class]])
{
for (UIView* subview in view.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
{
dot = (UIImageView *)subview;
break;
}
}
if (dot == nil)
{
dot = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, view.frame.size.width, view.frame.size.height)];
[view addSubview:dot];
}
}
else
{
dot = (UIImageView *) view;
}
return dot;
}
Also, to clear the images that are already there, set the tint colors for the existing indicators to transparent:
- (void)awakeFromNib
{
self.pageIndicatorTintColor = [UIColor clearColor];
self.currentPageIndicatorTintColor = [UIColor clearColor];
}
Hope this will solve ur issue for iOS 7.
Happy coding

Try this link:-
Answer with GrayPageControl:-
Is there a way to change page indicator dots color
It is really good and reliable.I also have used this code.
You might have to do some more customization as
-(void) updateDots
{
for (int i = 0; i < [self.subviews count]; i++)
{
UIImageView* dot = [self.subviews objectAtIndex:i];
if (i == self.currentPage) {
if(i==0) {
dot.image = [UIImage imageNamed:#"activesearch.png"];
} else {
dot.image = activeImage;
}
} else {
if(i==0) {
dot.image = [UIImage imageNamed:#"inactivesearch.png"];
} else {
dot.image = inactiveImage;
}
}
}
}

Simply change the UIPageControl page indicator Color with pattern Image self.pageControl.currentPageIndicatorTintColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"circle"]];

The best compilation of the code for Swift 3 to replace the first icon of UIPageControl by a location marker:
import UIKit
class LocationPageControl: UIPageControl {
let locationArrow: UIImage = UIImage(named: "locationArrow")!
let pageCircle: UIImage = UIImage(named: "pageCircle")!
override var numberOfPages: Int {
didSet {
updateDots()
}
}
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = UIColor.clear
self.currentPageIndicatorTintColor = UIColor.clear
self.clipsToBounds = false
}
func updateDots() {
var i = 0
for view in self.subviews {
var imageView = self.imageView(forSubview: view)
if imageView == nil {
if i == 0 {
imageView = UIImageView(image: locationArrow)
} else {
imageView = UIImageView(image: pageCircle)
}
imageView!.center = view.center
view.addSubview(imageView!)
view.clipsToBounds = false
}
if i == self.currentPage {
imageView!.alpha = 1.0
} else {
imageView!.alpha = 0.5
}
i += 1
}
}
fileprivate func imageView(forSubview view: UIView) -> UIImageView? {
var dot: UIImageView?
if let dotImageView = view as? UIImageView {
dot = dotImageView
} else {
for foundView in view.subviews {
if let imageView = foundView as? UIImageView {
dot = imageView
break
}
}
}
return dot
}
}
Attached images:

I've created a custom page controller that should function in mostly the same way without hacking into the internals of a UIPageControl or having a whole library for one small widget.
Just place an empty UIStackView in your storyboard and make its custom class this class below, and use numberOfPages and currentPage just like a normal UIPageControl. Set the spacing on the UIStackView to change how much space there is between the views.
Swift 4.2
/**
If adding via storyboard, you should not need to set a width and height constraint for this view,
just set a placeholder for each so autolayout doesnt complain and this view will size itself once its populated with pages at runtime
*/
class PageControl: UIStackView {
#IBInspectable var currentPageImage: UIImage = UIImage(named: "whiteCircleFilled")!
#IBInspectable var pageImage: UIImage = UIImage(named: "whiteCircleOutlined")!
/**
Sets how many page indicators will show
*/
var numberOfPages = 3 {
didSet {
layoutIndicators()
}
}
/**
Sets which page indicator will be highlighted with the **currentPageImage**
*/
var currentPage = 0 {
didSet {
setCurrentPageIndicator()
}
}
override func awakeFromNib() {
super.awakeFromNib()
axis = .horizontal
distribution = .equalSpacing
alignment = .center
layoutIndicators()
}
private func layoutIndicators() {
for i in 0..<numberOfPages {
let imageView: UIImageView
if i < arrangedSubviews.count {
imageView = arrangedSubviews[i] as! UIImageView // reuse subview if possible
} else {
imageView = UIImageView()
addArrangedSubview(imageView)
}
if i == currentPage {
imageView.image = currentPageImage
} else {
imageView.image = pageImage
}
}
// remove excess subviews if any
let subviewCount = arrangedSubviews.count
if numberOfPages < subviewCount {
for _ in numberOfPages..<subviewCount {
arrangedSubviews.last?.removeFromSuperview()
}
}
}
private func setCurrentPageIndicator() {
for i in 0..<arrangedSubviews.count {
let imageView = arrangedSubviews[i] as! UIImageView
if i == currentPage {
imageView.image = currentPageImage
} else {
imageView.image = pageImage
}
}
}
}
Works for my purposes but I make no guarantees

Update for Swift 3.0 ... you know if you are OK with accepting stated risk: "Modifying the subviews of an existing control is fragile".
import UIKit
class CustomImagePageControl: UIPageControl {
let activeImage:UIImage = UIImage(named: "SelectedPage")!
let inactiveImage:UIImage = UIImage(named: "UnselectedPage")!
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = UIColor.clear
self.currentPageIndicatorTintColor = UIColor.clear
self.clipsToBounds = false
}
func updateDots() {
var i = 0
for view in self.subviews {
if let imageView = self.imageForSubview(view) {
if i == self.currentPage {
imageView.image = self.activeImage
} else {
imageView.image = self.inactiveImage
}
i = i + 1
} else {
var dotImage = self.inactiveImage
if i == self.currentPage {
dotImage = self.activeImage
}
view.clipsToBounds = false
view.addSubview(UIImageView(image:dotImage))
i = i + 1
}
}
}
fileprivate func imageForSubview(_ view:UIView) -> UIImageView? {
var dot:UIImageView?
if let dotImageView = view as? UIImageView {
dot = dotImageView
} else {
for foundView in view.subviews {
if let imageView = foundView as? UIImageView {
dot = imageView
break
}
}
}
return dot
}
}

You just need do it like this:
((UIImageView *)[[yourPageControl subviews] objectAtIndex:0]).image=[UIImage imageNamed:#"search.png"];

I think for this you need to customize whole UIPageControl. Please find more out at below links
How can i change the color of pagination dots of UIPageControl?
http://iphoneappcode.blogspot.in/2012/03/custom-uipagecontrol.html

From iOS 14 you can get and set the indicator image with these methods:
#available(iOS 14.0, *)
open func indicatorImage(forPage page: Int) -> UIImage?
#available(iOS 14.0, *)
open func setIndicatorImage(_ image: UIImage?, forPage page: Int)

From iProgrammer's answer
In case you want to hide the original dot
- (UIImageView *)imageViewForSubview:(UIView *)view {
UIImageView * dot = nil;
[view setBackgroundColor:[UIColor clearColor]]; << add this line

Following the previous answers I came up with the solution below.
Keep in mind that I had to add valueChanged listener that calls updateDots() as well in controller to handle taps made on UIPageControl
import UIKit
class PageControl: UIPageControl {
private struct Constants {
static let activeColor: UIColor = .white
static let inactiveColor: UIColor = .black
static let locationImage: UIImage = UIImage(named: "Location")!
}
// Update dots when currentPage changes
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.pageIndicatorTintColor = .clear
self.currentPageIndicatorTintColor = .clear
}
func updateDots() {
for (index, view) in self.subviews.enumerated() {
// Layers will be redrawn, remove old.
view.layer.sublayers?.removeAll()
if index == 0 {
drawImage(view: view)
} else {
drawDot(index: index, view: view)
}
}
}
private func drawDot(index: Int, view: UIView) {
let dotLayer = CAShapeLayer()
dotLayer.path = UIBezierPath(ovalIn: view.bounds).cgPath
dotLayer.fillColor = getColor(index: index)
view.layer.addSublayer(dotLayer)
}
private func drawImage(view: UIView) {
let height = view.bounds.height * 2
let width = view.bounds.width * 2
let topMargin: CGFloat = -3.5
let maskLayer = CALayer()
maskLayer.frame = CGRect(x: 0, y: 0, width: width, height: height)
maskLayer.contents = Constants.locationImage.cgImage
maskLayer.contentsGravity = .resizeAspect
let imageLayer = CALayer()
imageLayer.frame = CGRect(x:0, y: topMargin, width: width, height: height)
imageLayer.mask = maskLayer
imageLayer.backgroundColor = getColor()
view.backgroundColor = .clear // Otherwise black background
view.layer.addSublayer(imageLayer)
}
private func getColor(index: Int? = 0) -> CGColor {
return currentPage == index ? Constants.activeColor.cgColor : Constants.inactiveColor.cgColor
}
}

Related

UILabel preferredMaxLayout breaking self-sizing cells

I found some really annoying problem with UILabel not working with AutoLayout.
I found multiple threads about this, but none of solutions worked for me.
class AudiosHeaderCell: CollectionViewCell<AudiosHeaderItemViewModel> {
var label: UILabelPreferedWidth? {
didSet {
self.label?.textAlignment = .center
self.label?.numberOfLines = 0
self.label?.lineBreakMode = .byWordWrapping
self.label?.font = Font.Standard.size14
self.label?.textColor = UIColor(netHex: 0x185B97)
}
}
let labelLeftRightMargin = CGFloat(16)
override func setupViews() {
self.backgroundColor = UIColor.white
self.label = UILabelPreferedWidth()
self.contentView.addSubview(self.label!)
}
override func setupConstraints() {
self.label?.snp.makeConstraints { (make) in
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 8, left: labelLeftRightMargin, bottom: 8, right: labelLeftRightMargin))
}
}
override func bindViewModel(viewModel: AudiosHeaderItemViewModel) {
self.label?.text = viewModel.text
}
}
class UILabelPreferedWidth : UILabel {
override var bounds: CGRect {
didSet {
print("SET BOUNDS", bounds)
if (bounds.size.width != oldValue.size.width) {
self.setNeedsUpdateConstraints()
}
}
}
override func updateConstraints() {
print("updateConstraints", preferredMaxLayoutWidth, bounds)
if(preferredMaxLayoutWidth != bounds.size.width) {
preferredMaxLayoutWidth = bounds.size.width
}
super.updateConstraints()
}
}
I use a method to calculate the size of the cell like this:
func sizeForCellWithViewModel(_ viewModel: IReusableViewModel, fittingSize: CGSize) -> CGSize {
let cell = self.classRegistry.instances[viewModel.reuseIdentifier]!
(cell as! ICollectionViewCell).setViewModel(viewModel)
cell.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.translatesAutoresizingMaskIntoConstraints = false
cell.frame = CGRect(x: 0, y: 0, width: fittingSize.width, height: fittingSize.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
print("SIZE FOR ", cell, "FITTING ", fittingSize, "IS", cell.systemLayoutSizeFitting(fittingSize))
return cell.systemLayoutSizeFitting(fittingSize)
}
It works for multiple cells that has some images and other content, but it fails on such a simple problem like scaling to content of UILabel.
Problem I have is that systemLayoutSizeFitting.width returns size that is larger than fittingSize.width parameter I pass.
I've been debugging this long time and I found out that preferredMaxLayoutWidth is not updating properly, as bounds for this UILabel are going beyond cell frame - despite the constraints I use there.
Does anyone have a good solution for that ?
The only one I found is to use this on CollectionViewCell:
override var frame: CGRect {
didSet {
self.label?.preferredMaxLayoutWidth = self.frame.size.width - 32
}
}
But I hate it because it forces me to synchronise that with constraints and it will be required to all other use-cases in my application to remember to copy that.
What I'm looking for is AutoLayout, Constraint only solution.
Ok problem solved by adding width constraint to the Cell's contentView:
func sizeForCellWithViewModel(_ viewModel: IReusableViewModel, fittingSize: CGSize) -> CGSize {
let cell = self.classRegistry.instances[viewModel.reuseIdentifier]!
(cell as! ICollectionViewCell).setViewModel(viewModel)
cell.contentView.frame = CGRect(x: 0, y: 0, width: fittingSize.width, height: fittingSize.height)
cell.contentView.snp.removeConstraints()
if fittingSize.width != 0 {
cell.contentView.snp.makeConstraints { (make) in
make.width.lessThanOrEqualTo(fittingSize.width)
}
}
if fittingSize.height != 0 {
cell.contentView.snp.makeConstraints({ (make) in
make.height.lessThanOrEqualTo(fittingSize.height)
})
}
cell.contentView.setNeedsLayout()
cell.contentView.layoutIfNeeded()
return cell.contentView.systemLayoutSizeFitting(fittingSize)
}
Seems that this somehow makes UILabel works and preferredWidth not going crazy.

How to edit the UIBlurEffect intensity?

I don't want my background image to be too blury. Isn't there a property to adjust the blur intensity?
let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.Light)
blurEffect.???
let effectView = UIVisualEffectView(effect: blurEffect)
effectView.frame = backgroundAlbumCover.bounds
backgroundAlbumCover.addSubview(effectView)
You can do that in super elegant way with animator
(reducing UIVisualEffectView alpha will not affect blur intensity, so we must use animator)
Usage as simple as:
let blurEffectView = BlurEffectView()
view.addSubview(blurEffectView)
BlurEffectView realisation:
class BlurEffectView: UIVisualEffectView {
var animator = UIViewPropertyAnimator(duration: 1, curve: .linear)
override func didMoveToSuperview() {
guard let superview = superview else { return }
backgroundColor = .clear
frame = superview.bounds //Or setup constraints instead
setupBlur()
}
private func setupBlur() {
animator.stopAnimation(true)
effect = nil
animator.addAnimations { [weak self] in
self?.effect = UIBlurEffect(style: .dark)
}
animator.fractionComplete = 0.1 //This is your blur intensity in range 0 - 1
}
deinit {
animator.stopAnimation(true)
}
}
Adjusting the blur itself is not possible... But, you can adjust how visible the blur view is. This can be done in a number of ways, only three of which I can think of at the moment:
1st Option: Adjust the alpha of your UIVisualEffectView instance e.g:
effectView.alpha = 0.4f;
2nd Option: Add a UIView instance to effectView at Index 0 and adjust the alpha of this UIView instance. e.g:
UIView *blurDilutionView = [[UIView alloc] initWithFrame: effectView.frame];
blurDilutionView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent: 0.5];
blurDilutionView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin|UIViewAutoresizingFlexibleBottomMargin|UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;//if not using AutoLayout
[effectView insertSubview:blurDilutionView atIndex:0];
3rd Option: use multiple UIVisualEffectView instances (I have not tried this yet, more of an idea). Apply an alpha of 0.1f on each. The more UIVisualEffectView views you have the more blurry the overall look. Once again, I have not tried this option yet!
Update:
As Axeva mentioned in the comments, Apple advises against adjusting the alpha to change the blur. So use these suggestions at your own potential peril.
Once I ran into a problem to create a blur effect that is darker than .light and lighter than .dark UIBlurEffect style.
To achieve that, put a view on the back with the color and alpha you need:
let pictureImageView = // Image that you need to blur
let backView = UIView(frame: pictureImageView.bounds)
backView.backgroundColor = UIColor(red: 100/255, green: 100/255, blue: 100/255, alpha: 0.3)
pictureImageView.addSubview(backView)
let blurEffect = UIBlurEffect(style: .light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = pictureImageView.bounds
pictureImageView.addSubview(blurEffectView)
How the result looks like:
For more details, check out this article.
UPDATE: apparently there is another nice (maybe even nicer) way to implement the Blur using CIFilter(name: "CIGaussianBlur").
It allows the make “opacity” and blur’s strengths much lower than UIBlurEffect.
Use Private API if you want. Tested on iOS 13.7, 14.8, 15.5, 16.0. Does not work with Mac Catalyst.
Sample
UIVisualEffectView+Intensity.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#interface UIVisualEffectView (Intensity)
#property (nonatomic) CGFloat intensity;
#end
NS_ASSUME_NONNULL_END
UIVisualEffectView+Intensity.m
#import "UIVisualEffectView+Intensity.h"
#import <objc/message.h>
#interface UIVisualEffectView (Intensity)
#property (readonly) id backgroundHost; // _UIVisualEffectHost
#property (readonly) __kindof UIView *backdropView; // _UIVisualEffectBackdropView
#end
#implementation UIVisualEffectView (Intensity)
- (id)backgroundHost {
id backgroundHost = ((id (*)(id, SEL))objc_msgSend)(self, NSSelectorFromString(#"_backgroundHost")); // _UIVisualEffectHost
return backgroundHost;
}
- (__kindof UIView * _Nullable)backdropView {
__kindof UIView *backdropView = ((__kindof UIView * (*)(id, SEL))objc_msgSend)(self.backgroundHost, NSSelectorFromString(#"contentView")); // _UIVisualEffectBackdropView
return backdropView;
}
- (CGFloat)intensity {
__kindof UIView *backdropView = self.backdropView; // _UIVisualEffectBackdropView
__kindof CALayer *backdropLayer = ((__kindof CALayer * (*)(id, SEL))objc_msgSend)(backdropView, NSSelectorFromString(#"backdropLayer")); // UICABackdropLayer
NSArray *filters = backdropLayer.filters;
id _Nullable __block gaussianBlur = nil; // CAFilter
[filters enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj respondsToSelector:NSSelectorFromString(#"type")]) return;
NSString *type = ((NSString * (*)(id, SEL))objc_msgSend)(obj, NSSelectorFromString(#"type"));
if (![type isKindOfClass:[NSString class]]) return;
if ([type isEqualToString:#"gaussianBlur"]) {
gaussianBlur = obj;
*stop = YES;
}
}];
if (gaussianBlur == nil) return 0.0f;
NSNumber * _Nullable inputRadius = [gaussianBlur valueForKeyPath:#"inputRadius"];
if ((inputRadius == nil) || (![inputRadius isKindOfClass:[NSNumber class]])) return 0.0f;
return [inputRadius floatValue];
}
- (void)setIntensity:(CGFloat)intensity {
id descriptor = ((id (*)(id, SEL, id, BOOL))objc_msgSend)(self, NSSelectorFromString(#"_effectDescriptorForEffects:usage:"), #[self.effect], YES); // _UIVisualEffectDescriptor
NSArray *filterEntries = ((NSArray * (*)(id, SEL))objc_msgSend)(descriptor, NSSelectorFromString(#"filterEntries")); // NSArray<_UIVisualEffectFilterEntry *>
id _Nullable __block gaussianBlur = nil; // _UIVisualEffectFilterEntry
[filterEntries enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *filterType = ((NSString * (*)(id, SEL))objc_msgSend)(obj, NSSelectorFromString(#"filterType"));
if ([filterType isEqualToString:#"gaussianBlur"]) {
gaussianBlur = obj;
*stop = YES;
}
}];
if (gaussianBlur == nil) return;
NSMutableDictionary *requestedValues = [((NSDictionary * (*)(id, SEL))objc_msgSend)(gaussianBlur, NSSelectorFromString(#"requestedValues")) mutableCopy];
if (![requestedValues.allKeys containsObject:#"inputRadius"]) {
NSLog(#"Not supported effect.");
return;
}
requestedValues[#"inputRadius"] = [NSNumber numberWithFloat:intensity];
((void (*)(id, SEL, NSDictionary *))objc_msgSend)(gaussianBlur, NSSelectorFromString(#"setRequestedValues:"), requestedValues);
((void (*)(id, SEL, id))objc_msgSend)(self.backgroundHost, NSSelectorFromString(#"setCurrentEffectDescriptor:"), descriptor);
((void (*)(id, SEL))objc_msgSend)(self.backdropView, NSSelectorFromString(#"applyRequestedFilterEffects"));
}
#end
Usage
let firstBlurView: UIVisualEffectView = .init(effect: UIBlurEffect(style: .dark))
// setter
firstBlurView.intensity = 7
// getter
print(firstBlurView.intensity) // 7.0
UIBlurEffect doesn't provide such a property. If you want another intensity, you will have to make a BlurEffect by yourself.
Here is the BlurEffectView class with public intensity setter as well as with conformance to Apple's UIView.animation functions (you can animate intensity by UIKit's animations)
BlurEffectView.swift
import UIKit
public class BlurEffectView: UIView {
public override class var layerClass: AnyClass {
return BlurIntensityLayer.self
}
#objc
#IBInspectable
public dynamic var intensity: CGFloat {
set { self.blurIntensityLayer.intensity = newValue }
get { return self.blurIntensityLayer.intensity }
}
#IBInspectable
public var effect = UIBlurEffect(style: .dark) {
didSet {
self.setupPropertyAnimator()
}
}
private let visualEffectView = UIVisualEffectView(effect: nil)
private var propertyAnimator: UIViewPropertyAnimator!
private var blurIntensityLayer: BlurIntensityLayer {
return self.layer as! BlurIntensityLayer
}
public override init(frame: CGRect) {
super.init(frame: frame)
self.setupView()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupView()
}
deinit {
self.propertyAnimator.stopAnimation(true)
}
private func setupPropertyAnimator() {
self.propertyAnimator?.stopAnimation(true)
self.propertyAnimator = UIViewPropertyAnimator(duration: 1, curve: .linear)
self.propertyAnimator.addAnimations { [weak self] in
self?.visualEffectView.effect = self?.effect
}
self.propertyAnimator.pausesOnCompletion = true
}
private func setupView() {
self.backgroundColor = .clear
self.isUserInteractionEnabled = false
self.addSubview(self.visualEffectView)
self.visualEffectView.fill(view: self)
self.setupPropertyAnimator()
}
public override func display(_ layer: CALayer) {
guard let presentationLayer = layer.presentation() as? BlurIntensityLayer else {
return
}
let clampedIntensity = max(0.0, min(1.0, presentationLayer.intensity))
self.propertyAnimator.fractionComplete = clampedIntensity
}
}
BlurIntensityLayer.swift
import QuartzCore
class BlurIntensityLayer: CALayer {
#NSManaged var intensity: CGFloat
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? BlurIntensityLayer {
self.intensity = layer.intensity
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override class func needsDisplay(forKey key: String) -> Bool {
key == #keyPath(intensity) ? true : super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
guard event == #keyPath(intensity) else {
return super.action(forKey: event)
}
let animation = CABasicAnimation(keyPath: event)
animation.toValue = nil
animation.fromValue = (self.presentation() ?? self).intensity
return animation
}
}

UIImagePicker allowsEditing stuck in center

I have a UIImagePicker that works perfect for a type of UIImagePickerControllerSourceTypePhotoLibrary, but when I use UIImagePickerControllerSourceTypeCamera, the editing box cannot move from the center of the image. So if the image is say taller than it is wide, the user cannot move the editing box to the top square of the image.
Anyone know why this would be the case? It only happens when the source is from the camera, not the library.
Edit: Some CODE!!!
if (actionSheet.tag == 2) {
if (buttonIndex == 0) { // Camera
// Check for camera
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] == YES) {
// Create image picker controller
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
// Set source to the camera
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
imagePicker.allowsEditing = YES;
// Delegate is self
imagePicker.delegate = self;
// Show image picker
[self presentViewController:imagePicker
animated:YES
completion:^(void) {
}];
}
}
else if (buttonIndex == 1) { // Photo Library
if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypePhotoLibrary] == YES) {
// Create image picker controller
UIImagePickerController *imagePicker = [[UIImagePickerController alloc] init];
// Set source to the camera
imagePicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
imagePicker.allowsEditing = YES;
// Delegate is self
imagePicker.delegate = self;
// Show image picker
[self presentViewController:imagePicker
animated:YES
completion:^(void) {
}];
}
}
So as you can see, I display them the exact same, but the camera edit acts differently than the photo library edit.
Looks like this behavior is just a bug in iOS 6... Basically you cannot move the editing box, it always bounces back to the middle unless you zoom in a bit. Hopefully they fix that soon.
Thanks yycking. This extension works. Except I added the method call inside viewDidLayoutSubviews so that I don't have to call it every time I want to open image picker.
Here's the full extenstion
extension UIImagePickerController {
open override var childForStatusBarHidden: UIViewController? {
return nil
}
open override var prefersStatusBarHidden: Bool {
return true
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
fixCannotMoveEditingBox()
}
func fixCannotMoveEditingBox() {
if let cropView = cropView,
let scrollView = scrollView,
scrollView.contentOffset.y == 0 {
var top: CGFloat = 0.0
if #available(iOS 11.0, *) {
top = cropView.frame.minY + self.view.safeAreaInsets.top
} else {
// Fallback on earlier versions
top = cropView.frame.minY
}
let bottom = scrollView.frame.height - cropView.frame.height - top
scrollView.contentInset = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
var offset: CGFloat = 0
if scrollView.contentSize.height > scrollView.contentSize.width {
offset = 0.5 * (scrollView.contentSize.height - scrollView.contentSize.width)
}
scrollView.contentOffset = CGPoint(x: 0, y: -top + offset)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.fixCannotMoveEditingBox()
}
}
var cropView: UIView? {
return findCropView(from: self.view)
}
var scrollView: UIScrollView? {
return findScrollView(from: self.view)
}
func findCropView(from view: UIView) -> UIView? {
let width = UIScreen.main.bounds.width
let size = view.bounds.size
if width == size.height, width == size.height {
return view
}
for view in view.subviews {
if let cropView = findCropView(from: view) {
return cropView
}
}
return nil
}
func findScrollView(from view: UIView) -> UIScrollView? {
if let scrollView = view as? UIScrollView {
return scrollView
}
for view in view.subviews {
if let scrollView = findScrollView(from: view) {
return scrollView
}
}
return nil
}
}
Reset contentInset of scrollview:
extension UIImagePickerController {
func fixCannotMoveEditingBox() {
if let cropView = cropView,
let scrollView = scrollView,
scrollView.contentOffset.y == 0 {
let top = cropView.frame.minY + self.view.safeAreaInsets.top
let bottom = scrollView.frame.height - cropView.frame.height - top
scrollView.contentInset = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
var offset: CGFloat = 0
if scrollView.contentSize.height > scrollView.contentSize.width {
offset = 0.5 * (scrollView.contentSize.height - scrollView.contentSize.width)
}
scrollView.contentOffset = CGPoint(x: 0, y: -top + offset)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.fixCannotMoveEditingBox()
}
}
var cropView: UIView? {
return findCropView(from: self.view)
}
var scrollView: UIScrollView? {
return findScrollView(from: self.view)
}
func findCropView(from view: UIView) -> UIView? {
let width = UIScreen.main.bounds.width
let size = view.bounds.size
if width == size.height, width == size.height {
return view
}
for view in view.subviews {
if let cropView = findCropView(from: view) {
return cropView
}
}
return nil
}
func findScrollView(from view: UIView) -> UIScrollView? {
if let scrollView = view as? UIScrollView {
return scrollView
}
for view in view.subviews {
if let scrollView = findScrollView(from: view) {
return scrollView
}
}
return nil
}
}
then call it
imagePickercontroller.fixCannotMoveEditingBox()
Here is an extension I end up using that works fine on both notch and non-notch devices. And works perfectly on iOS 15!
extension UIImagePickerController {
open override var childForStatusBarHidden: UIViewController? {
return nil
}
open override var prefersStatusBarHidden: Bool {
return true
}
open override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
fixCannotMoveEditingBox()
}
private func fixCannotMoveEditingBox() {
if let cropView = cropView, let scrollView = scrollView, scrollView.contentOffset.y == 0 {
let top: CGFloat = cropView.frame.minY + self.view.frame.minY
let bottom = scrollView.frame.height - cropView.frame.height - top
scrollView.contentInset = UIEdgeInsets(top: top, left: 0, bottom: bottom, right: 0)
var offset: CGFloat = 0
if scrollView.contentSize.height > scrollView.contentSize.width {
offset = 0.5 * (scrollView.contentSize.height - scrollView.contentSize.width)
}
scrollView.contentOffset = CGPoint(x: 0, y: -top + offset)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
self?.fixCannotMoveEditingBox()
}
}
private var cropView: UIView? {
return findCropView(from: self.view)
}
private var scrollView: UIScrollView? {
return findScrollView(from: self.view)
}
private func findCropView(from view: UIView) -> UIView? {
let width = UIScreen.main.bounds.width
let size = view.bounds.size
if width == size.height, width == size.height {
return view
}
for view in view.subviews {
if let cropView = findCropView(from: view) {
return cropView
}
}
return nil
}
private func findScrollView(from view: UIView) -> UIScrollView? {
if let scrollView = view as? UIScrollView {
return scrollView
}
for view in view.subviews {
if let scrollView = findScrollView(from: view) {
return scrollView
}
}
return nil
}
}
p.s.: the only changes from other similar answers are:
dropping support for an old iOS version (like iOS 11);
a bit different way of calculating contentInset top property;
This is the default behavior the Image Picker Controller, you can not change it. The only other option is to create your own cropping utility. Check out the link below for an example:
https://github.com/ardalahmet/SSPhotoCropperViewController
I know, this is not a good solution, but it works.
I tested on iOS8+iPhone5, iOS9+iPhone6sPlus, iOS10+iPhone6, iOS10+iPhone6sPlus.
CAUTION: PLImageScrollView and PLCropOverlayCropView are UNDOCUMENTED classes.
- (void)showImagePickerControllerWithSourceType:(UIImagePickerControllerSourceType)sourceType {
UIImagePickerController *imagePickerController = [UIImagePickerController new];
imagePickerController.sourceType = sourceType;
imagePickerController.mediaTypes = #[(NSString *)kUTTypeImage];
imagePickerController.allowsEditing = YES;
imagePickerController.delegate = self;
[self presentViewController:imagePickerController animated:YES completion:^{
[self fxxxImagePickerController:imagePickerController];
}];
}
- (void)fxxxImagePickerController:(UIImagePickerController *)imagePickerController {
if (!imagePickerController
|| !imagePickerController.allowsEditing
|| imagePickerController.sourceType != UIImagePickerControllerSourceTypeCamera) {
return;
}
// !!!: UNDOCUMENTED CLASS
Class ScrollViewClass = NSClassFromString(#"PLImageScrollView");
Class CropViewClass = NSClassFromString(#"PLCropOverlayCropView");
[imagePickerController.view eachSubview:^BOOL(UIView *subview, NSInteger depth) {
if ([subview isKindOfClass:CropViewClass]) {
// 0. crop rect position
subview.frame = subview.superview.bounds;
}
else if ([subview isKindOfClass:[UIScrollView class]]
&& [subview isKindOfClass:ScrollViewClass]) {
BOOL isNewImageScrollView = !self->_imageScrollView;
self->_imageScrollView = (UIScrollView *)subview;
// 1. enable scrolling
CGSize size = self->_imageScrollView.frame.size;
CGFloat inset = ABS(size.width - size.height) / 2;
self->_imageScrollView.contentInset = UIEdgeInsetsMake(inset, 0, inset, 0);
// 2. centering image by default
if (isNewImageScrollView) {
CGSize contentSize = self->_imageScrollView.contentSize;
if (contentSize.height > contentSize.width) {
CGFloat offset = round((contentSize.height - contentSize.width) / 2 - inset);
self->_imageScrollView.contentOffset = CGPointMake(self->_imageScrollView.contentOffset.x, offset);
}
}
}
return YES;
}];
// prevent re-layout, maybe not necessary
#weakify(self, imagePickerController);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
#strongify(self, imagePickerController);
[self fxxxImagePickerController:imagePickerController];
});
}
EDIT: The eachSubview: method traverses all the subviews tree.
If you have set "View controller-based status bar appearance" to NO in info.plist and set status bar appearance as light using
UIApplication.shared.statusBarStyle = .lightContent
or using any other method , Then simply set the style as .default before presenting the image picker. for Eg:
imagePicker.allowsEditing = true
imagePicker.sourceType = .photoLibrary
UIApplication.shared.statusBarStyle = .default
present(imagePicker, animated: true, completion: nil)
Change the source type according to your need either as photoLibrary or camera and in completion block of your didFinishPickingMediaWithInfo add the following to completion block.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
//let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage
var pickedImage : UIImage?
if let img = info[UIImagePickerControllerEditedImage] as? UIImage
{
pickedImage = img
}
else if let img = info[UIImagePickerControllerOriginalImage] as? UIImage
{
pickedImage = img
}
dismiss(animated: true, completion: {
UIApplication.shared.statusBarStyle = .lightContent
})}
Apparently this is a workaround for the same.Hope this helps.
A workaround that solved it is to add an entry in info.plist with "View controller-based status bar appearance" set to NO

How to mask UITableViewCells underneath a UITableView Transparent Header

I want the header to mask the cells, but not the background.
I have a UITableView with transparent headers and cells similar to Apple's Notification Center (when you swipe down on the status bar on your iPhone). I can't figure out how to mask the cells so they don't show up underneath the header when it scrolls.
I've tried changing the contentInsets of the tableview, and I've tried changing the frame of the header View to a negative origin.
Try to make a subclass of UITableviewCell and add these methods
- (void)maskCellFromTop:(CGFloat)margin {
self.layer.mask = [self visibilityMaskWithLocation:margin/self.frame.size.height];
self.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskWithLocation:(CGFloat)location {
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = self.bounds;
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}
and add this delegate method in UITableView
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (iNotifyTableViewCell *cell in self.visibleCells) {
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + [iNotifyHeaderView height] - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
[cell maskCellFromTop:hiddenFrameHeight];
}
}
}
*Note that [iNotifyHeaderView height] is the height of the HeaderView. and use #import <QuartzCore/QuartzCore.h> for the custom cell.
A little edit on Alex Markman's answer, where you could skip creating a subclass for an UITableViewCell. Benefit of this approach is that you can use it for multiple different UITableViewCells.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
for (UITableViewCell *cell in self.tableView.visibleCells) {
CGFloat hiddenFrameHeight = scrollView.contentOffset.y + self.navigationController.navigationBar.frame.size.height - cell.frame.origin.y;
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
[self maskCell:cell fromTopWithMargin:hiddenFrameHeight];
}
}
}
- (void)maskCell:(UITableViewCell *)cell fromTopWithMargin:(CGFloat)margin
{
cell.layer.mask = [self visibilityMaskForCell:cell withLocation:margin/cell.frame.size.height];
cell.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskForCell:(UITableViewCell *)cell withLocation:(CGFloat)location
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = cell.bounds;
mask.colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:1 alpha:0] CGColor], (id)[[UIColor colorWithWhite:1 alpha:1] CGColor], nil];
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location], [NSNumber numberWithFloat:location], nil];
return mask;
}
#Alex Markman - your answer is great and was very usefull for me, but I found that when you're scrolling on retina devices, the cells do not scroll smoothly. I found that it is caused by layer's locations parameter during the rendering process:
mask.locations = [NSArray arrayWithObjects:[NSNumber numberWithFloat:location];
I slightly modified your code. Maybe someone will find it useful:
- (void)maskCellFromTop:(CGFloat)margin
{
self.layer.mask = [self visibilityMaskFromLocation:margin];
self.layer.masksToBounds = YES;
}
- (CAGradientLayer *)visibilityMaskFromLocation:(CGFloat)location
{
CAGradientLayer *mask = [CAGradientLayer layer];
mask.frame = CGRectMake(
self.bounds.origin.x,
location+self.bounds.origin.y,
self.bounds.size.width,
self.bounds.size.height-location);
mask.colors = #[
(id)[[UIColor colorWithWhite:1 alpha:1] CGColor],
(id)[[UIColor colorWithWhite:1 alpha:1] CGColor]
];
return mask;
}
Swift version
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let hiddenFrameHeight = scrollView.contentOffset.y + navigationController!.navigationBar.frame.size.height - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
maskCell(cell: cell, margin: Float(hiddenFrameHeight))
}
}
}
func maskCell(cell: UITableViewCell, margin: Float) {
cell.layer.mask = visibilityMaskForCell(cell: cell, location: (margin / Float(cell.frame.size.height) ))
cell.layer.masksToBounds = true
}
func visibilityMaskForCell(cell: UITableViewCell, location: Float) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = cell.bounds
mask.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor(white: 1, alpha: 1).cgColor]
mask.locations = [NSNumber(value: location), NSNumber(value: location)]
return mask;
}
Clean Swift 3 Version:
extension YourViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let hiddenFrameHeight = scrollView.contentOffset.y + navigationController!.navigationBar.frame.size.height - cell.frame.origin.y
if (hiddenFrameHeight >= 0 || hiddenFrameHeight <= cell.frame.size.height) {
if let customCell = cell as? CustomCell {
customCell.maskCell(fromTop: hiddenFrameHeight)
}
}
}
}
}
class CustomCell: UITableViewCell {
public func maskCell(fromTop margin: CGFloat) {
layer.mask = visibilityMask(withLocation: margin / frame.size.height)
layer.masksToBounds = true
}
private func visibilityMask(withLocation location: CGFloat) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = bounds
mask.colors = [UIColor.white.withAlphaComponent(0).cgColor, UIColor.white.cgColor]
let num = location as NSNumber
mask.locations = [num, num]
return mask
}
}
I just used this and it works like a charm. I want to thank everyone in the post! Up votes all around!
I have two possible solutions, no code - just the idea:
not generic, should work with the settup/design apple uses at the Notification Center.
Make the Section-Header opaque, 'clone' the background-pattern of the table as background of the section-Header. Position the background-pattern depending on the section-header offset.
genereic, but probably more performance problems. Should work fine with few cells.
Add a Alpha Mask to all cell-layers. Move the Alpha Mask depending on the cell-position.
(use scrollViewDidScroll delegate method to maintain the background-pattern / Alpha-Mask offset).
I ended up setting the height of the section header to its minimum, and overriding UITableView's layoutSubviews to place the header on the tableView's superview, adjusting the frame's origin upward by its height.
The Swift 3 version didn't work for me because I added the UITableViewController as a subview. So I had to make some changes in the extension of the scrollview.
This should also work with UITableViewController that have been pushed from another ViewController (Note: not tested)
extension NavNotitionTableViewController {
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
let calculatedY = cell.frame.origin.y - scrollView.contentOffset.y;
if let customCell = cell as? NavNotitionTableViewCell {
if(calculatedY < 44 && calculatedY > 0){
let hideAmount = 44 - calculatedY;
if let customCell = cell as? NavNotitionTableViewCell {
customCell.maskCell(fromTop: hideAmount)
}
}else if (calculatedY > 0){
//All other cells
customCell.maskCell(fromTop: 0)
}else if (calculatedY < 0){
customCell.maskCell(fromTop: cell.frame.height);
}
}
}
}
}
In this example, I first get the frame Y origin of the cell and distract the scollViews contentOffsetY.
The height of my custom section is 44. So I define the hideAmount value for the mask.
The Cell functions are untouched:
public func maskCell(fromTop margin: CGFloat) {
layer.mask = visibilityMask(withLocation: margin / frame.size.height)
layer.masksToBounds = true
}
private func visibilityMask(withLocation location: CGFloat) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = bounds
mask.colors = [UIColor.white.withAlphaComponent(0).cgColor, UIColor.white.cgColor]
let num = location as NSNumber
mask.locations = [num, num]
return mask
}
This wouldn't work if you wanted to show content behind your table view, but, since I'm only trying to create rounded headers with a plain solid colour background behind them, what solved it for me was mimicking transparency by setting the background colour of the header's background view to the background colour of the table view (or the first parent view with an opaque background).
class YourViewController {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
for cell in tableView.visibleCells {
// if tableView is under navigationbar, set `systemTopInset`
cell.sectionHeaderMask(delegate: self, systemTopInset: self.navigationController?.navigationBar.frame.height ?? 0)
}
}
}
public extension UITableViewCell {
func sectionHeaderMask<T: UITableViewDelegate>(delegate: T, systemTopInset: CGFloat = 0) {
guard let tableView = self.superview as? UITableView else { return }
guard let indexPath = tableView.indexPath(for: self) else { return }
guard let heightForHeader = delegate.tableView?(tableView, heightForHeaderInSection: indexPath.section) else { return }
let hiddenFrameHeight = tableView.contentOffset.y - self.frame.origin.y + heightForHeader + tableView.contentInset.top + systemTopInset
if hiddenFrameHeight >= 0 || hiddenFrameHeight <= self.frame.size.height {
mask(margin: Float(hiddenFrameHeight))
}
}
private func mask(margin: Float) {
layer.mask = visibilityMask(location: (margin / Float(frame.size.height) ))
layer.masksToBounds = true
}
private func visibilityMask(location: Float) -> CAGradientLayer {
let mask = CAGradientLayer()
mask.frame = self.bounds
mask.colors = [UIColor(white: 1, alpha: 0).cgColor, UIColor(white: 1, alpha: 1).cgColor]
mask.locations = [NSNumber(value: location), NSNumber(value: location)]
return mask
}
}
Or, if all you need is for your UI to look nice,
you could
change your table view to not have floating section headers
In two quick and easy steps (iOS 6):
Change your UITableView style to UITableViewStyleGrouped. (You can do this from Storyboard/NIB, or via code.)
Next, set your tableview's background view to a empty view like so [in either a method such as viewDidAppear or even in the cellForRow method (though I would prefer the former)].
yourTableView.backgroundView = [[UIView alloc] initWithFrame:listTableView.bounds];
Voila, now you have your table view - but without the floating section headers. Your section headers now scroll along with the cells and your messy UI problems are solved!
Do try this out and let me know how it goes.
Happy coding :)
EDIT: for iOS 7, simply change the table view style to 'UITableViewStyleGrouped' and change the view's tint color to 'clear color'.

How do I auto size a UIScrollView to fit its content

Is there a way to make a UIScrollView auto-adjust to the height (or width) of the content it's scrolling?
Something like:
[scrollView setContentSize:(CGSizeMake(320, content.height))];
The best method I've ever come across to update the content size of a UIScrollView based on its contained subviews:
Objective-C
CGRect contentRect = CGRectZero;
for (UIView *view in self.scrollView.subviews) {
contentRect = CGRectUnion(contentRect, view.frame);
}
self.scrollView.contentSize = contentRect.size;
Swift
let contentRect: CGRect = scrollView.subviews.reduce(into: .zero) { rect, view in
rect = rect.union(view.frame)
}
scrollView.contentSize = contentRect.size
UIScrollView doesn't know the height of its content automatically. You must calculate the height and width for yourself
Do it with something like
CGFloat scrollViewHeight = 0.0f;
for (UIView* view in scrollView.subviews)
{
scrollViewHeight += view.frame.size.height;
}
[scrollView setContentSize:(CGSizeMake(320, scrollViewHeight))];
But this only work if the views are one below the other. If you have a view next to each other you only have to add the height of one if you don't want to set the content of the scroller larger than it really is.
Solution if you're using auto layout:
Set translatesAutoresizingMaskIntoConstraints to NO on all views involved.
Position and size your scroll view with constraints external to the scroll view.
Use constraints to lay out the subviews within the scroll view, being sure that the constraints tie to all four edges of the scroll view and do not rely on the scroll view to get their size.
Source:
https://developer.apple.com/library/ios/technotes/tn2154/_index.html
I added this to Espuz and JCC's answer. It uses the y position of the subviews and doesn't include the scroll bars. Edit Uses the bottom of the lowest sub view that is visible.
+ (CGFloat) bottomOfLowestContent:(UIView*) view
{
CGFloat lowestPoint = 0.0;
BOOL restoreHorizontal = NO;
BOOL restoreVertical = NO;
if ([view respondsToSelector:#selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:#selector(setShowsVerticalScrollIndicator:)])
{
if ([(UIScrollView*)view showsHorizontalScrollIndicator])
{
restoreHorizontal = YES;
[(UIScrollView*)view setShowsHorizontalScrollIndicator:NO];
}
if ([(UIScrollView*)view showsVerticalScrollIndicator])
{
restoreVertical = YES;
[(UIScrollView*)view setShowsVerticalScrollIndicator:NO];
}
}
for (UIView *subView in view.subviews)
{
if (!subView.hidden)
{
CGFloat maxY = CGRectGetMaxY(subView.frame);
if (maxY > lowestPoint)
{
lowestPoint = maxY;
}
}
}
if ([view respondsToSelector:#selector(setShowsHorizontalScrollIndicator:)] && [view respondsToSelector:#selector(setShowsVerticalScrollIndicator:)])
{
if (restoreHorizontal)
{
[(UIScrollView*)view setShowsHorizontalScrollIndicator:YES];
}
if (restoreVertical)
{
[(UIScrollView*)view setShowsVerticalScrollIndicator:YES];
}
}
return lowestPoint;
}
Here is the accepted answer in swift for anyone who is too lazy to convert it :)
var contentRect = CGRectZero
for view in self.scrollView.subviews {
contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size
Here's a Swift 3 adaptation of #leviatan's answer :
EXTENSION
import UIKit
extension UIScrollView {
func resizeScrollViewContentSize() {
var contentRect = CGRect.zero
for view in self.subviews {
contentRect = contentRect.union(view.frame)
}
self.contentSize = contentRect.size
}
}
USAGE
scrollView.resizeScrollViewContentSize()
Very easy to use !
Following extension would be helpful in Swift.
extension UIScrollView{
func setContentViewSize(offset:CGFloat = 0.0) {
// dont show scroll indicators
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
var maxHeight : CGFloat = 0
for view in subviews {
if view.isHidden {
continue
}
let newHeight = view.frame.origin.y + view.frame.height
if newHeight > maxHeight {
maxHeight = newHeight
}
}
// set content size
contentSize = CGSize(width: contentSize.width, height: maxHeight + offset)
// show scroll indicators
showsHorizontalScrollIndicator = true
showsVerticalScrollIndicator = true
}
}
Logic is the same with the given answers. However, It omits hidden views within UIScrollView and calculation is performed after scroll indicators set hidden.
Also, there is an optional function parameter and you're able to add an offset value by passing parameter to function.
Great & best solution from #leviathan. Just translating to swift using FP (functional programming) approach.
self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect(), {
CGRectUnion($0, $1.frame)
}.size
You can get height of the content inside UIScrollView by calculate which child "reaches furthers". To calculate this you have to take in consideration origin Y (start) and item height.
float maxHeight = 0;
for (UIView *child in scrollView.subviews) {
float childHeight = child.frame.origin.y + child.frame.size.height;
//if child spans more than current maxHeight then make it a new maxHeight
if (childHeight > maxHeight)
maxHeight = childHeight;
}
//set content size
[scrollView setContentSize:(CGSizeMake(320, maxHeight))];
By doing things this way items (subviews) don't have to be stacked directly one under another.
I came up with another solution based on #emenegro's solution
NSInteger maxY = 0;
for (UIView* subview in scrollView.subviews)
{
if (CGRectGetMaxY(subview.frame) > maxY)
{
maxY = CGRectGetMaxY(subview.frame);
}
}
maxY += 10;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, maxY)];
Basically, we figure out which element is furthest down in the view and adds a 10px padding to the bottom
Or just do:
int y = CGRectGetMaxY(((UIView*)[_scrollView.subviews lastObject]).frame); [_scrollView setContentSize:(CGSizeMake(CGRectGetWidth(_scrollView.frame), y))];
(This solution was added by me as a comment in this page. After getting 19 up-votes for this comment, I've decided to add this solution as a formal answer for the benefit of the community!)
Because a scrollView can have other scrollViews or different inDepth subViews tree, run in depth recursively is preferable.
Swift 2
extension UIScrollView {
//it will block the mainThread
func recalculateVerticalContentSize_synchronous () {
let unionCalculatedTotalRect = recursiveUnionInDepthFor(self)
self.contentSize = CGRectMake(0, 0, self.frame.width, unionCalculatedTotalRect.height).size;
}
private func recursiveUnionInDepthFor (view: UIView) -> CGRect {
var totalRect = CGRectZero
//calculate recursevly for every subView
for subView in view.subviews {
totalRect = CGRectUnion(totalRect, recursiveUnionInDepthFor(subView))
}
//return the totalCalculated for all in depth subViews.
return CGRectUnion(totalRect, view.frame)
}
}
Usage
scrollView.recalculateVerticalContentSize_synchronous()
For swift4 using reduce:
self.scrollView.contentSize = self.scrollView.subviews.reduce(CGRect.zero, {
return $0.union($1.frame)
}).size
The size depends on the content loaded inside of it, and the clipping options. If its a textview, then it also depends on the wrapping, how many lines of text, the font size, and so on and on. Nearly impossible for you to compute yourself. The good news is, it is computed after the view is loaded and in the viewWillAppear. Before that, it's all unknown and and content size will be the same as frame size. But, in the viewWillAppear method and after (such as the viewDidAppear) the content size will be the actual.
Wrapping Richy's code I created a custom UIScrollView class that automates
content resizing completely!
SBScrollView.h
#interface SBScrollView : UIScrollView
#end
SBScrollView.m:
#implementation SBScrollView
- (void) layoutSubviews
{
CGFloat scrollViewHeight = 0.0f;
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
for (UIView* view in self.subviews)
{
if (!view.hidden)
{
CGFloat y = view.frame.origin.y;
CGFloat h = view.frame.size.height;
if (y + h > scrollViewHeight)
{
scrollViewHeight = h + y;
}
}
}
self.showsHorizontalScrollIndicator = YES;
self.showsVerticalScrollIndicator = YES;
[self setContentSize:(CGSizeMake(self.frame.size.width, scrollViewHeight))];
}
#end
How to use:
Simply import the .h file to your view controller and
declare a SBScrollView instance instead of the normal UIScrollView one.
why not single line of code??
_yourScrollView.contentSize = CGSizeMake(0, _lastView.frame.origin.y + _lastView.frame.size.height);
I created a subclass of ScrollView to handle the intrinsicContentSize and it worked perfectly for me
public final class ContentSizedScrollView: UIScrollView {
override public var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override public var intrinsicContentSize: CGSize {
layoutIfNeeded()
return self.contentSize
}
}
Now you can create a scrollview with this class and set constraints on all sides
Make sure that the subviews are tied up to all fours edges of the scrollView
it depends on the content really : content.frame.height might give you what you want ? Depends if content is a single thing, or a collection of things.
I also found leviathan's answer to work the best. However, it was calculating a strange height. When looping through the subviews, if the scrollview is set to show scroll indicators, those will be in the array of subviews. In this case, the solution is to temporarily disable the scroll indicators before looping, then re-establish their previous visibility setting.
-(void)adjustContentSizeToFit is a public method on a custom subclass of UIScrollView.
-(void)awakeFromNib {
dispatch_async(dispatch_get_main_queue(), ^{
[self adjustContentSizeToFit];
});
}
-(void)adjustContentSizeToFit {
BOOL showsVerticalScrollIndicator = self.showsVerticalScrollIndicator;
BOOL showsHorizontalScrollIndicator = self.showsHorizontalScrollIndicator;
self.showsVerticalScrollIndicator = NO;
self.showsHorizontalScrollIndicator = NO;
CGRect contentRect = CGRectZero;
for (UIView *view in self.subviews) {
contentRect = CGRectUnion(contentRect, view.frame);
}
self.contentSize = contentRect.size;
self.showsVerticalScrollIndicator = showsVerticalScrollIndicator;
self.showsHorizontalScrollIndicator = showsHorizontalScrollIndicator;
}
I think this can be a neat way of updating UIScrollView's content view size.
extension UIScrollView {
func updateContentViewSize() {
var newHeight: CGFloat = 0
for view in subviews {
let ref = view.frame.origin.y + view.frame.height
if ref > newHeight {
newHeight = ref
}
}
let oldSize = contentSize
let newSize = CGSize(width: oldSize.width, height: newHeight + 20)
contentSize = newSize
}
}
Set dynamic content size like this.
self.scroll_view.contentSize = CGSizeMake(screen_width,CGRectGetMaxY(self.controlname.frame)+20);
import UIKit
class DynamicSizeScrollView: UIScrollView {
var maxHeight: CGFloat = UIScreen.main.bounds.size.height
var maxWidth: CGFloat = UIScreen.main.bounds.size.width
override func layoutSubviews() {
super.layoutSubviews()
if !__CGSizeEqualToSize(bounds.size,self.intrinsicContentSize){
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
let height = min(contentSize.height, maxHeight)
let width = min(contentSize.height, maxWidth)
return CGSize(width: width, height: height)
}
}
If you using Auto layout, just set the border element's edge equal to your scroll view.
For example, I wanna my horizontal scroll view auto fit my horizontal contents:
Swift
let bottomConstrint = NSLayoutConstraint.init(item: (bottommost UI element),
attribute: .bottom,
relatedBy: .equal,
toItem: (your UIScrollView),
attribute: .bottom,
multiplier: 1.0,
constant: 0)
bottomConstrint.isActive = true
If you using Snapkit like me, just:
scrollView.addSubview( (bottommost element) )
(bottommost element).snp.makeConstraints { make in
/*other constraints*/
make.bottom.equalToSuperview()
}
I would create a subclass of UIScrollView with the following:
class ContentSizedScrollView: UIScrollView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
This will resize automatically based on the height of the content.
class ContentSizedScrollView: UIScrollView {
override var contentSize:CGSize {
didSet {
invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
layoutIfNeeded()
return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height)
}
}
// In UIViewController
import SnapKit
...
var scrollView: ContentSizedScrollView!
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scrollView.contentSize = .init(width: view.bounds.width, height: stackView.bounds.height)
}
// Here some example of content composing inside of UIStackView
func setupContent() {
scrollView = ContentSizedScrollView()
blockView.addSubview(scrollView)
scrollView.snp.makeConstraints { make in
make.top.equalTo(19)
make.left.equalToSuperview()
make.right.equalToSuperview()
make.bottom.equalTo(-20)
}
scrollView.contentInset = .init(top: 0, left: 0, bottom: 26, right: 0)
scrollView.showsVerticalScrollIndicator = false
scrollView.clipsToBounds = true
scrollView.layer.cornerRadius = blockView.layer.cornerRadius / 2
stackView = UIStackView()
scrollView.addSubview(stackView)
stackView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.centerX.equalToSuperview()
make.width.equalToSuperview().offset(-10)
}
stackView.axis = .vertical
stackView.alignment = .center
textTitleLabel = Label()
stackView.addArrangedSubview(textTitleLabel)
textTitleLabel.snp.makeConstraints { make in
make.width.equalToSuperview().offset(-30)
}
textTitleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)
textTitleLabel.textColor = Color.Blue.oxfordBlue
textTitleLabel.textAlignment = .center
textTitleLabel.numberOfLines = 0
stackView.setCustomSpacing(10, after: textTitleLabel)
}