I want to start addArrangedSubview from bottom alignment as like as attached 2nd screenshot(3rd is my actual working screenshot). But every time it arranged from top to bottom. But I need it from bottom to top arrange. I want to create this design using UIStackView inside UIScrollView. I'm trying it with UIScrollView because of landscapes orientation support. And UIStackView for better efficiency with native view.
https://github.com/amitcse6/BottomAlignStackView
import UIKit
import SnapKit
class ViewController: UIViewController {
private var storeBack: UIView?
private var myImageView: UIImageView?
private var myScrollView: UIScrollView?
private var myStackView: UIStackView?
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor=UIColor.white
self.loadStoreBack()
self.loadBackgroundImage()
self.loadScrollView()
self.loadStackView()
self.loadUI()
}
func loadStoreBack() {
self.storeBack=UIView()
self.view.addSubview(self.storeBack!)
self.storeBack?.backgroundColor=UIColor.white
self.storeBack?.snp.remakeConstraints { (make) in
make.top.equalTo(self.view!.snp.top)
make.left.equalTo(self.view.snp.left)
make.right.equalTo(self.view.snp.right)
make.bottom.equalTo(self.view.snp.bottom)
}
}
func loadBackgroundImage() -> Void {
self.myImageView = UIImageView()
self.storeBack?.addSubview(self.myImageView!)
self.myImageView?.contentMode = .scaleAspectFill
self.myImageView?.image=UIImage(named: "welcome-background")
self.myImageView?.snp.remakeConstraints { (make) in
make.top.equalTo(self.storeBack!.snp.top)
make.left.equalTo(self.storeBack!.snp.left)
make.right.equalTo(self.storeBack!.snp.right)
make.bottom.equalTo(self.storeBack!.snp.bottom)
}
}
func loadScrollView() {
self.myScrollView = UIScrollView()
self.storeBack?.addSubview(self.myScrollView!)
self.myScrollView?.backgroundColor=UIColor.clear
self.myScrollView?.showsHorizontalScrollIndicator = false
self.myScrollView?.showsVerticalScrollIndicator = false
self.myScrollView?.bounces=false
self.myScrollView?.isScrollEnabled=true
self.myScrollView?.snp.remakeConstraints { (make) in
make.top.equalTo(self.storeBack!.snp.top)
make.left.equalTo(self.storeBack!.snp.left)
make.right.equalTo(self.storeBack!.snp.right)
make.bottom.equalTo(self.storeBack!.snp.bottom)
make.width.equalTo(self.storeBack!)
make.height.equalTo(self.storeBack!)
}
}
func loadStackView() {
self.myStackView = UIStackView()
self.myScrollView?.addSubview(self.myStackView!)
self.myStackView?.backgroundColor=UIColor.clear
self.myStackView?.axis = .vertical
self.myStackView?.spacing = 0
//self.myStackView?.alignment = .bottom
self.myStackView?.snp.remakeConstraints { (make) in
make.top.equalTo(self.myScrollView!.snp.top)
make.left.equalTo(self.myScrollView!.snp.left)
make.right.equalTo(self.myScrollView!.snp.right)
make.bottom.equalTo(self.myScrollView!.snp.bottom)
make.width.equalTo(self.myScrollView!)
make.height.equalTo(self.myScrollView!).priority(250)
}
}
func loadUI() {
for n in 0..<5 {
if n%2 == 0 {
loadBuddyLogoImageView1()
}else{
loadBuddyLogoImageView2()
}
}
}
func loadBuddyLogoImageView1() {
let subBackView=UIView()
self.myStackView?.addArrangedSubview(subBackView)
subBackView.backgroundColor=UIColor.clear
let backgroundView=UIView()
subBackView.addSubview(backgroundView)
backgroundView.backgroundColor=UIColor.red
backgroundView.snp.remakeConstraints { (make) in
make.top.equalTo(subBackView.snp.top)
make.left.equalTo(subBackView.snp.left)
make.right.equalTo(subBackView.snp.right)
make.bottom.equalTo(subBackView.snp.bottom)
make.height.equalTo(100)
}
}
func loadBuddyLogoImageView2() {
let subBackView=UIView()
self.myStackView?.addArrangedSubview(subBackView)
subBackView.backgroundColor=UIColor.clear
let backgroundView=UIView()
subBackView.addSubview(backgroundView)
backgroundView.backgroundColor=UIColor.green
backgroundView.snp.remakeConstraints { (make) in
make.top.equalTo(subBackView.snp.top)
make.left.equalTo(subBackView.snp.left)
make.right.equalTo(subBackView.snp.right)
make.bottom.equalTo(subBackView.snp.bottom)
make.height.equalTo(100)
}
}
}
Related
I have a question about the UICollection view list's separatorLayoutGuide. I saw this article and understood I need to override the function updateConstraints() in order to update the separator layout guide.
like this...
override func updateConstraints() {
super.updateConstraints()
separatorLayoutGuide.leadingAnchor.constraint(equalTo: otherView.leadingAnchor, constant: 0.0).isActive = true
}
I can see the tiny space between the cell's leading anchor and seprateguide's leading anchor like the image below and I want to fix it. (like the left side of the cell)
The problem is, however, I created a custom collection view list cell using this article and cannot change the separatorLayoutGuide leading to the custom view's leading.
I added the customListCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true in order to position the leading of separatorLayoutGuide to the customView's leading, and I get the
"UILayoutGuide:0x2822d8b60'UICollectionViewListCellSeparatorLayoutGuide'.leading"> and <NSLayoutXAxisAnchor:0x280e9cac0 "ContentView:0x15960db90.leading"> because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.'
error.
After I've done the research, I figured I didn't addSubview for the separatorLayoutGuide, but even if I add a subview to the custom view, the app crashes. Is there a way to change the separator guide's leading anchor when using the custom UIView?
class CustomListCell: UICollectionViewListCell {
var item: TestItem?
override func updateConfiguration(using state: UICellConfigurationState) {
// Create new configuration object
var newConfiguration = ContentConfiguration().updated(for: state)
newConfiguration.name = item.name
newConfiguration.state = item.state
// Set content configuration
contentConfiguration = newConfiguration
}
}
struct ContentConfiguration: UIContentConfiguration, Hashable {
var name: String?
var state: String?
func makeContentView() -> UIView & UIContentView {
return ContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Self {
guard let state = state as? UICellConfigurationState else {
return self
}
// Updater self based on the current state
let updatedConfiguration = self
if state.isSelected {
print("is selected")
} else {
print("is deselected")
}
return updatedConfiguration
}
}
class ContentView: UIView, UIContentView {
let contentsView = UIView()
let customListCell = CustomListCell()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = ""
return label
}()
lazy var statusLabel: UILabel = {
let label = UILabel()
label.text = ""
return label
}()
lazy var symbolImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
init(configuration: ContentConfiguration) {
// Custom initializer implementation here.
super.init(frame: .zero)
setupAllViews()
apply(configuration: configuration)
}
override func updateConstraints() {
super.updateConstraints()
customListCell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var currentConfiguration: ContentConfiguration!
var configuration: UIContentConfiguration {
get {
currentConfiguration
}
set {
guard let newConfiguration = newValue as? ContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
func setupAllViews() {
// add subviews and add constraints
}
func apply(configuration: ContentConfiguration) {
}
}
by overriding updateConstraints in a UICollectionViewListCell subclass
In your code you have a customListCell instance variable which is not necessary. Also you should not add separatorLayoutGuide as a subview anywhere.
Try to access the cell's contentView:
override func updateConstraints() {
super.updateConstraints()
if let customView = cell.contentView as? ContentView {
separatorLayoutGuide.leadingAnchor.constraint(equalTo: customView.leadingAnchor, constant: 0.0).isActive = true
}
}
Like this you can access any subview in your ContentView.
Another question is: Is it necessary to create a new constraint every time updateConstraints is called? Are constraints from previous calls still there? According to the documentation of updateConstraints:
Your implementation must be as efficient as possible. Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed. During each update pass, you must ensure that you have the appropriate constraints for the app’s current state.
Therefore I suggest this approach:
class ContentView: UIView, UIContentView {
var separatorConstraint: NSLayoutConstraint?
func updateForCell(_ cell: CustomListCell) {
if separatorConstraint == nil {
separatorConstraint = cell.separatorLayoutGuide.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 30)
}
separatorConstraint?.isActive = true
}
}
class CustomListCell: UICollectionViewListCell {
...
override func updateConstraints() {
super.updateConstraints()
(contentView as? AlbumContentView)?.updateForCell(self)
}
}
I am using SnapKit for autolaoyut.
This is my view:
private func makeIconUI() {
contentView.addSubview(iconView)
iconView.snp.makeConstraints { (make) in
make.leading.equalToSuperview().inset(Dimesion.sidePadding)
make.height.width.equalTo(19)
make.top.equalToSuperview().inset(24)
}
}
private func makeNumberUI() {
contentView.addSubview(numberLabel)
numberLabel.backgroundColor = .yellow
numberLabel.snp.makeConstraints { (make) in
make.leading.equalTo(iconView.snp.trailing)
make.top.equalToSuperview().inset(24)
make.bottom.equalToSuperview()
}
}
private func makeTitleUI() {
contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { (make) in
make.leading.equalTo(numberLabel.snp.trailing)
make.trailing.equalTo(self.snp.trailing)
make.top.equalToSuperview()
}
}
The problem is:
Yellow label does not fit fully. The second label pushing on it, but I can fix it.
Am confused.Yellow doesn't fit fully like top of both labels dont match? then remove inset
private func makeNumberUI() {
contentView.addSubview(numberLabel)
numberLabel.backgroundColor = .yellow
numberLabel.snp.makeConstraints { (make) in
make.leading.equalTo(iconView.snp.trailing)
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
}
if logic is horizontally then give it a width like:
view.addSubview(numberLabel)
numberLabel.backgroundColor = .yellow
numberLabel.snp.makeConstraints { (make) in
make.leading.equalTo(iconView.snp.trailing)
make.width.equalTo(30)
make.top.equalToSuperview()
make.bottom.equalToSuperview()
}
I tend to hide the status bar, animated in the following way.
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
extension ViewController: SideMenuNavigationControllerDelegate {
func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = true
}
func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = false
}
}
However, I would also like to preserve the space occupied by status bar, so that when status bar appears, the entire app will not be "pushed up"
May I know how I can achieve so?
Thank you.
You can use additionalSafeAreaInsets to add a placeholder height, substituting the status bar.
But for devices with a notch like the iPhone 12, the space is automatically preserved, so you don't need to add any additional height.
class ViewController: UIViewController {
var statusBarHidden: Bool = false /// no more computed property, otherwise reading safe area would be too late
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func showButtonPressed(_ sender: Any) {
statusBarHidden.toggle()
if statusBarHidden {
sideMenuWillAppear()
} else {
sideMenuWillDisappear()
}
}
lazy var overlayViewController: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "OverlayViewController")
}()
var additionalHeight: CGFloat {
if view.window?.safeAreaInsets.top ?? 0 > 20 { /// is iPhone X or other device with notch
return 0 /// add 0 height
} else {
/// the height of the status bar
return view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
}
}
}
extension ViewController {
/// add placeholder height to substitute status bar
func addAdditionalHeight(_ add: Bool) {
if add {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = additionalHeight
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = additionalHeight
}
} else {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = 0
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = 0
}
}
}
func sideMenuWillAppear() {
addChild(overlayViewController)
view.addSubview(overlayViewController.view)
overlayViewController.view.frame = view.bounds
overlayViewController.view.frame.origin.x = -400
overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlayViewController.didMove(toParent: self)
addAdditionalHeight(true) /// add placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -100
self.setNeedsStatusBarAppearanceUpdate() /// hide status bar
}
}
func sideMenuDidAppear() {}
func sideMenuWillDisappear() {
addAdditionalHeight(false) /// remove placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -400
self.setNeedsStatusBarAppearanceUpdate() /// show status bar
} completion: { _ in
self.overlayViewController.willMove(toParent: nil)
self.overlayViewController.view.removeFromSuperview()
self.overlayViewController.removeFromParent()
}
}
func sideMenuDidDisappear() {}
}
Result (Tested on iPhone 12, iPhone 8, iPad Pro 4th gen):
iPhone 12 (notch)
iPhone 8 (no notch)
iPhone 12 + navigation bar
iPhone 8 + navigation bar
Demo GitHub repo
First of all, it is not currently possible to make UINavigationController behave this way. However you can wrap your UINavigationController instance in a Container View Controller.
This will give you control over managing the top space from where the UINavigationController view layout starts. Inside this container class, you could manage it like following -
class ContainerViewController: UIViewController {
private lazy var statusBarBackgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var statusBarBackgroundViewHeightConstraint: NSLayoutConstraint = {
statusBarBackgroundView.heightAnchor.constraint(equalToConstant: 0)
}()
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
guard let statusBarMananger = self.view.window?.windowScene?.statusBarManager
else { return 0 }
return statusBarMananger.statusBarFrame.height
} else {
return UIApplication.shared.statusBarFrame.height
}
}
var statusBarHidden: Bool = false {
didSet {
self.statusBarBackgroundViewHeightConstraint.constant = self.statusBarHidden ? self.lastKnownStatusBarHeight : 0
self.view.layoutIfNeeded()
}
}
private var lastKnownStatusBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let topView = self.statusBarBackgroundView
self.view.addSubview(topView)
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
statusBarBackgroundViewHeightConstraint,
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = self.statusBarHeight
if height > 0 {
self.lastKnownStatusBarHeight = height
}
}
func setUpNavigationController(_ navCtrl: UINavigationController) {
self.addChild(navCtrl)
navCtrl.didMove(toParent: self)
self.view.addSubview(navCtrl.view)
navCtrl.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navCtrl.view.topAnchor.constraint(equalTo: statusBarBackgroundView.bottomAnchor),
navCtrl.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
navCtrl.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
navCtrl.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
Now from your call site, you can do following -
class ViewController: UIViewController {
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
/// Forward the call to ContainerViewController to act on this update
(self.navigationController?.parent as? ContainerViewController)?.statusBarHidden = self.statusBarHidden
/// Keep doing whatever you are doing now
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}
I tend to hide the status bar, animated in the following way.
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
extension ViewController: SideMenuNavigationControllerDelegate {
func sideMenuWillAppear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = true
}
func sideMenuDidAppear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuWillDisappear(menu: SideMenuNavigationController, animated: Bool) {
}
func sideMenuDidDisappear(menu: SideMenuNavigationController, animated: Bool) {
statusBarHidden = false
}
}
However, I would also like to preserve the space occupied by status bar, so that when status bar appears, the entire app will not be "pushed up"
May I know how I can achieve so?
Thank you.
You can use additionalSafeAreaInsets to add a placeholder height, substituting the status bar.
But for devices with a notch like the iPhone 12, the space is automatically preserved, so you don't need to add any additional height.
class ViewController: UIViewController {
var statusBarHidden: Bool = false /// no more computed property, otherwise reading safe area would be too late
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation{
return .slide
}
#IBAction func showButtonPressed(_ sender: Any) {
statusBarHidden.toggle()
if statusBarHidden {
sideMenuWillAppear()
} else {
sideMenuWillDisappear()
}
}
lazy var overlayViewController: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: "OverlayViewController")
}()
var additionalHeight: CGFloat {
if view.window?.safeAreaInsets.top ?? 0 > 20 { /// is iPhone X or other device with notch
return 0 /// add 0 height
} else {
/// the height of the status bar
return view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0.0
}
}
}
extension ViewController {
/// add placeholder height to substitute status bar
func addAdditionalHeight(_ add: Bool) {
if add {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = additionalHeight
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = additionalHeight
}
} else {
if let navigationController = self.navigationController {
/// set insets of navigation controller if you're using navigation controller
navigationController.additionalSafeAreaInsets.top = 0
} else {
/// set insets of self if not using navigation controller
self.additionalSafeAreaInsets.top = 0
}
}
}
func sideMenuWillAppear() {
addChild(overlayViewController)
view.addSubview(overlayViewController.view)
overlayViewController.view.frame = view.bounds
overlayViewController.view.frame.origin.x = -400
overlayViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
overlayViewController.didMove(toParent: self)
addAdditionalHeight(true) /// add placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -100
self.setNeedsStatusBarAppearanceUpdate() /// hide status bar
}
}
func sideMenuDidAppear() {}
func sideMenuWillDisappear() {
addAdditionalHeight(false) /// remove placeholder height
UIView.animate(withDuration: 1) {
self.overlayViewController.view.frame.origin.x = -400
self.setNeedsStatusBarAppearanceUpdate() /// show status bar
} completion: { _ in
self.overlayViewController.willMove(toParent: nil)
self.overlayViewController.view.removeFromSuperview()
self.overlayViewController.removeFromParent()
}
}
func sideMenuDidDisappear() {}
}
Result (Tested on iPhone 12, iPhone 8, iPad Pro 4th gen):
iPhone 12 (notch)
iPhone 8 (no notch)
iPhone 12 + navigation bar
iPhone 8 + navigation bar
Demo GitHub repo
First of all, it is not currently possible to make UINavigationController behave this way. However you can wrap your UINavigationController instance in a Container View Controller.
This will give you control over managing the top space from where the UINavigationController view layout starts. Inside this container class, you could manage it like following -
class ContainerViewController: UIViewController {
private lazy var statusBarBackgroundView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .clear
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var statusBarBackgroundViewHeightConstraint: NSLayoutConstraint = {
statusBarBackgroundView.heightAnchor.constraint(equalToConstant: 0)
}()
var statusBarHeight: CGFloat {
if #available(iOS 13.0, *) {
guard let statusBarMananger = self.view.window?.windowScene?.statusBarManager
else { return 0 }
return statusBarMananger.statusBarFrame.height
} else {
return UIApplication.shared.statusBarFrame.height
}
}
var statusBarHidden: Bool = false {
didSet {
self.statusBarBackgroundViewHeightConstraint.constant = self.statusBarHidden ? self.lastKnownStatusBarHeight : 0
self.view.layoutIfNeeded()
}
}
private var lastKnownStatusBarHeight: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
let topView = self.statusBarBackgroundView
self.view.addSubview(topView)
NSLayoutConstraint.activate([
topView.topAnchor.constraint(equalTo: self.view.topAnchor),
topView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
statusBarBackgroundViewHeightConstraint,
topView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let height = self.statusBarHeight
if height > 0 {
self.lastKnownStatusBarHeight = height
}
}
func setUpNavigationController(_ navCtrl: UINavigationController) {
self.addChild(navCtrl)
navCtrl.didMove(toParent: self)
self.view.addSubview(navCtrl.view)
navCtrl.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
navCtrl.view.topAnchor.constraint(equalTo: statusBarBackgroundView.bottomAnchor),
navCtrl.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
navCtrl.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
navCtrl.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
])
self.view.layoutIfNeeded()
}
}
Now from your call site, you can do following -
class ViewController: UIViewController {
var statusBarHidden: Bool = false {
didSet {
UIView.animate(withDuration: Constants.config_shortAnimTime) { () -> Void in
/// Forward the call to ContainerViewController to act on this update
(self.navigationController?.parent as? ContainerViewController)?.statusBarHidden = self.statusBarHidden
/// Keep doing whatever you are doing now
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
}
In the past, I customized the images of indicators of Page Control using some functions like the following code provided by #Politta.
class CustomPageControl: UIPageControl {
#IBInspectable var currentPageImage: UIImage?
#IBInspectable var otherPagesImage: UIImage?
override var numberOfPages: Int {
didSet {
updateDots()
}
}
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
pageIndicatorTintColor = .clear
currentPageIndicatorTintColor = .clear
clipsToBounds = false
}
private func updateDots() {
for (index, subview) in subviews.enumerated() {
let imageView: UIImageView
if let existingImageview = getImageView(forSubview: subview) {
imageView = existingImageview
} else {
imageView = UIImageView(image: otherPagesImage)
// Modify image size
imageView.frame = ....
imageView.center = subview.center
subview.addSubview(imageView)
subview.clipsToBounds = false
}
imageView.image = currentPage == index ? currentPageImage : otherPagesImage
}
}
private func getImageView(forSubview view: UIView) -> UIImageView? {
if let imageView = view as? UIImageView {
return imageView
} else {
let view = view.subviews.first { (view) -> Bool in
return view is UIImageView
} as? UIImageView
return view
}
}
}
Now I found that Subviews count is not working on iOS 14 as Apple had introduced some new APIs for UIPageControll. Now when I try to use a function setIndicatorImage(image, index) provided by #Soumen, the image shows abnormally big. Modifying the size of page control doesn't help me. In the past, since I add image view to current view of page control, I can define its frame, but now the function setIndicatorImage() just takes image as its parameter. How do I solve the issue?
class CustomPageControl: UIPageControl {
#IBInspectable var currentPageImage: UIImage?
#IBInspectable var otherPagesImage: UIImage?
override var numberOfPages: Int {
didSet {
updateDots()
}
}
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
if #available(iOS 14.0, *) {
defaultConfigurationForiOS14AndAbove()
} else {
pageIndicatorTintColor = .clear
currentPageIndicatorTintColor = .clear
clipsToBounds = false
}
}
private func defaultConfigurationForiOS14AndAbove() {
if #available(iOS 14.0, *) {
for index in 0..<numberOfPages {
let image = index == currentPage ? currentPageImage : otherPagesImage
setIndicatorImage(image, forPage: index)
}
// give the same color as "otherPagesImage" color.
pageIndicatorTintColor = .gray
// give the same color as "currentPageImage" color.
currentPageIndicatorTintColor = .red
/*
Note: If Tint color set to default, Indicator image is not showing. So, give the same tint color based on your Custome Image.
*/
}
}
private func updateDots() {
if #available(iOS 14.0, *) {
defaultConfigurationForiOS14AndAbove()
} else {
for (index, subview) in subviews.enumerated() {
let imageView: UIImageView
if let existingImageview = getImageView(forSubview: subview) {
imageView = existingImageview
} else {
imageView = UIImageView(image: otherPagesImage)
// Modify image size
imageView.frame = ....
imageView.center = subview.center
subview.addSubview(imageView)
subview.clipsToBounds = false
}
imageView.image = currentPage == index ? currentPageImage : otherPagesImage
}
}
}
private func getImageView(forSubview view: UIView) -> UIImageView? {
if let imageView = view as? UIImageView {
return imageView
} else {
let view = view.subviews.first { (view) -> Bool in
return view is UIImageView
} as? UIImageView
return view
}
}
}
For iOS 14, the hierarchy of Views has changed, so we cannot get subviews count of UIPageControl like we did before (in iOS < 14). To get them like before, you need to change your accessing method of dot subviews like below.
For accessing them in iOS 14,
Before:
for (index, subview) in subviews.enumerated() {
//Your rest of the code
}
After:
var dotViews: [UIView] = subviews
if #available(iOS 14, *) {
let pageControl = dotViews[0]
let dotContainerView = pageControl.subviews[0]
dotViews = dotContainerView.subviews
}
for (index, subview) in dotViews.enumerated() {
//Your rest of the code
}
Your full code may look like this after modification:
class CustomPageControl: UIPageControl {
#IBInspectable var currentPageImage: UIImage?
#IBInspectable var otherPagesImage: UIImage?
override var numberOfPages: Int {
didSet {
updateDots()
}
}
override var currentPage: Int {
didSet {
updateDots()
}
}
override func awakeFromNib() {
super.awakeFromNib()
pageIndicatorTintColor = .clear
currentPageIndicatorTintColor = .clear
clipsToBounds = false
}
private func updateDots() {
var dotViews: [UIView] = subviews
if #available(iOS 14, *) {
let pageControl = dotViews[0]
let dotContainerView = pageControl.subviews[0]
dotViews = dotContainerView.subviews
}
for (index, subview) in dotViews.enumerated() {
let imageView: UIImageView
if let existingImageview = getImageView(forSubview: subview) {
imageView = existingImageview
} else {
imageView = UIImageView(image: otherPagesImage)
// Modify image size
imageView.frame = ....
imageView.center = subview.center
subview.addSubview(imageView)
subview.clipsToBounds = false
}
imageView.image = currentPage == index ? currentPageImage : otherPagesImage
}
}
private func getImageView(forSubview view: UIView) -> UIImageView? {
if let imageView = view as? UIImageView {
return imageView
} else {
let view = view.subviews.first { (view) -> Bool in
return view is UIImageView
} as? UIImageView
return view
}
}
}
In this way, you can access your dot views and proceed code like before (Customising the images of indicators, change background color etc.)
For iOS 14.0 you have to access pageControl.subviews[0].subviews[0].subviews in order to get the dots views of the pageControl. Instead, for iOS < 14.0 you'll get the dots views accessing pageControl.subviews
private func updatePageControlDots() {
var currentDot = UIView()
if #available(iOS 14, *) {
let pageControlContent = pageControl.subviews[0]
let dotContainerView = pageControlContent.subviews[0]
currentDot = dotContainerView.subviews[currentPage]
} else {
currentDot = pageControl.subviews[currentPage]
}
}