Glitchy UIScrollView Infinite Scroll - ios

Im trying to implement an infinitely paging UIScrollView based on the Advanced ScrollView Techniques from WWDC 2011. The problem that Im facing is that as I scroll the screen keeps on Jumping backwards instead of advancing forward in the array. Is there any way to create this effect. Below is the code I have implemented thus far.
import Foundation
import UIKit
class CustomScrollView:UIScrollView{
var label1:CustomLabel!
var label2:CustomLabel!
var label3:CustomLabel!
var labels:[UILabel]!
var visibleLabels:[UILabel]!
var recycledPages:Set<CustomLabel>!
var visiblePages:Set<CustomLabel>!
override init(frame: CGRect) {
super.init(frame: frame)
indicatorStyle = .white
recycledPages = Set<CustomLabel>()
visiblePages = Set<CustomLabel>()
var firstScreenPostion:CGRect = CGRect(origin: CGPoint(x: 0 * bounds.width, y: 0), size: bounds.size)
var secondeScreenPosition:CGRect = CGRect(origin: CGPoint(x: 1 * bounds.width, y: 0), size: bounds.size)
var thirdScreenPosition:CGRect = CGRect(origin: CGPoint(x: 2 * bounds.width, y: 0), size: bounds.size)
label1 = CustomLabel(frame: firstScreenPostion)
label1.backgroundColor = .red
label1.text = "1"
label2 = CustomLabel(frame: secondeScreenPosition)
label2.backgroundColor = .green
label2.text = "2"
label3 = CustomLabel(frame: thirdScreenPosition)
label3.backgroundColor = .blue
label3.text = "3"
visibleLabels = [label1,label2,label3]
labels = [label1,label2,label3]
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func recenterIfNecessary(){
let currentOffset = contentOffset
let contentWidth = contentSize.width
let centerOffset = (contentWidth - bounds.width)/2
let distanceFromCenter = abs(currentOffset.x - centerOffset)
if distanceFromCenter > contentWidth/4{
self.contentOffset = CGPoint(x: centerOffset, y: 0)
for label in visibleLabels{
var center = label.center
center.x += centerOffset - currentOffset.x
label.center = center
}
}
}
override func layoutSubviews() {
recenterIfNecessary()
let visibleBounds = bounds
let minimumVisibleX = bounds.minX
let maximumVisbleX = bounds.maxX
tilePages(minimumVisibleX: minimumVisibleX, toMaxX: maximumVisbleX)
// tileLabelsFromMinX(minimumVisibleX: minimumVisibleX, toMaxX: maximumVisbleX)
}
func insertLabel()->UILabel{
let recycledLabels = visibleLabels.filter { (label) -> Bool in
return label.superview == nil
}
let label = recycledLabels.last ?? UILabel(frame: bounds)
self.addSubview(label)
return label
}
func placeNewLabelOnRight(rightEdge: CGFloat)->CGFloat{
let label = self.insertLabel()
visibleLabels.append(label) // add rightmost label at the end of the array
label.frame.origin.x = rightEdge;
label.frame.origin.y = 0
label.text = labels.last?.text
return label.frame.maxX
}
func placeNewLabelOnLeft(leftEdge:CGFloat)->CGFloat{
let label = self.insertLabel()
self.visibleLabels.insert(label, at: 0) // add leftmost label at the beginning of the array
label.frame.origin.x = leftEdge - frame.size.width;
label.frame.origin.y = bounds.size.height - frame.size.height;
label.text = labels[0].text
return label.frame.minX
}
//function used in video
// func tileLabelsFromMinX(minimumVisibleX:CGFloat, toMaxX maximumVisibleX:CGFloat){
// // the upcoming tiling logic depends on there already being at least one label in the visibleLabels array, so
// // to kick off the tiling we need to make sure there's at least one label
// if (self.visibleLabels.count == 0)
// {
// self.placeNewLabelOnRight(rightEdge: minimumVisibleX);
// }
// print("visible labels.count: \(visibleLabels.count)")
//
// // add labels that are missing on right side
// // UILabel *lastLabel = [self.visibleLabels lastObject];
// var lastLabel = visibleLabels.last!
// var rightEdge = lastLabel.frame.maxX
// while (rightEdge < maximumVisibleX){
// rightEdge = self.placeNewLabelOnRight(rightEdge: rightEdge)
// }
//
// // add labels that are missing on left side
// var firstLabel = self.visibleLabels[0]
// var leftEdge = firstLabel.frame.minX
// while (leftEdge > minimumVisibleX){
// leftEdge = self.placeNewLabelOnLeft(leftEdge:leftEdge)
// }
//
// // remove labels that have fallen off right edge
// // lastLabel = [self.visibleLabels lastObject];
//
// while (lastLabel.frame.origin.x > maximumVisibleX){
// lastLabel.removeFromSuperview()
// self.visibleLabels.removeLast()
// lastLabel = self.visibleLabels.last!
// }
//
// // remove labels that have fallen off left edge
// firstLabel = self.visibleLabels[0];
// while (firstLabel.frame.maxX < minimumVisibleX){
// firstLabel.removeFromSuperview()
// self.visibleLabels.removeFirst()
// firstLabel = self.visibleLabels[0];
// }
// }
func tilePages(minimumVisibleX:CGFloat, toMaxX maximumVisibleX:CGFloat){
let visibleBounds = bounds
var firstNeededPageIndex:Int = Int(floorf(Float(minimumVisibleX/visibleBounds.width)))
var lastNeededPageIndex:Int = Int(floorf(Float((maximumVisibleX - 1)/visibleBounds.width)))
firstNeededPageIndex = max(firstNeededPageIndex, 0)
lastNeededPageIndex = min(lastNeededPageIndex, labels.count - 1)
//Recycle no-longer needed pages
for page in visiblePages{
if page.index < Int(firstNeededPageIndex) || page.index > Int(lastNeededPageIndex){
recycledPages.insert(page)
page.removeFromSuperview()
}
}
visiblePages.subtract(recycledPages)
//add missing pages
for i in firstNeededPageIndex...lastNeededPageIndex{
if !isDisplaying(pageForIndex: i){
let page:CustomLabel = dequeueRecycledPage() ?? CustomLabel()
print("index i: \(i)")
self.configurePage(page: page, forIndex: i)
self.addSubview(page)
visiblePages.insert(page)
}
}
}
func isDisplaying(pageForIndex index:Int)->Bool{
for page in visiblePages{
if page.index == index{
return true
}
}
return false
}
func configurePage(page:CustomLabel,forIndex index:Int){
page.index = index
page.text = "current index: \(index)"
let width = bounds.width
let newX:CGFloat = CGFloat(index) * width
page.backgroundColor = labels[index].backgroundColor
page.frame = CGRect(origin: CGPoint(x: newX, y: 0), size: bounds.size)
}
func dequeueRecycledPage()->CustomLabel?{
let page = recycledPages.first
if let page = page{
recycledPages.remove(page)
return page
}
return nil
}
}

Related

UIScrollView Reusable Views

I would like to create a UIScrollView that cycles between 3 paging views infinitely while I change the subviews to hold different reusable view controllers giving the illusion of infinitely many screen while only allocating the resources for 3 screens at a time similar to a UICollectionView.
Below is an attempt I've made at implementing such a view based on this answer Infinite UIScrollView however my project has not been working as expected.
Ive been trying to use 3 views as the basis for infinite scrolling and many labels as the subviews to be added and removed as the user scrolls.
import UIKit
class ViewController: UIViewController, UIScrollViewDelegate {
var scrollView:UIScrollView = {
let scrollView:UIScrollView = UIScrollView()
scrollView.backgroundColor = UIColor.orange
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
var labels:[CustomLabel] = [CustomLabel]()
var pages:[Page] = [Page]()
var visiblePages:Set<Page>!{
didSet{
print("visible pages count: \(recycledPages.count)")
}
}
var recycledPages:Set<Page>!{
didSet{
print("recycled pages count: \(recycledPages.count)")
}
}
var visibleLabels:Set<CustomLabel>!
var recycledLabels:Set<CustomLabel>!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
recycledPages = Set<Page>()
visiblePages = Set<Page>()
recycledLabels = Set<CustomLabel>()
visibleLabels = Set<CustomLabel>()
scrollView.contentSize = CGSize(width: view.bounds.width * 3, height: view.bounds.height)
scrollView.delegate = self
scrollView.isPagingEnabled = true
scrollView.indicatorStyle = .white
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
configureLabels()
setUpPages()
for i in 0..<pages.count{
scrollView.addSubview(pages[i])
}
}
func setUpPages(){
let page1 = Page(),page2 = Page(),page3 = Page()
page1.backgroundColor = .red
page2.backgroundColor = .green
page3.backgroundColor = .blue
page1.index = 0
page2.index = 1
page3.index = 2
pages = [page1,page2,page3]
visiblePages = [page1,page2,page3]
setContentViewFrames()
}
func configureLabels(){
let label1 = CustomLabel(), label2 = CustomLabel(), label3 = CustomLabel()
label1.text = "label1"
label2.text = "label2"
label3.text = "label3"
label1.backgroundColor = .red
label2.backgroundColor = .green
label3.backgroundColor = .blue
labels = [label1,label2,label3]
setContentViewFrames()
}
// func dequeueRecycledPage()->CustomLabel?{
// let page = recycledPages.first
// if let page = page{
// recycledPages.remove(page)
// return page
// }
// return nil
// }
var currentIndex = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.x < 0) {
let newOffset = CGPoint(x: scrollView.bounds.width + scrollView.contentOffset.x, y: 0)
scrollView.contentOffset.x = newOffset.x
rotateViewsRight()
}else if(scrollView.contentOffset.x > scrollView.bounds.size.width*2){
let newOffset = CGPoint(x: scrollView.contentOffset.x - scrollView.bounds.width, y: 0)
scrollView.contentOffset.x = newOffset.x
rotateViewsLeft()
}
print("current index: \(currentIndex)")
}
lazy var previousOffset:CGPoint = CGPoint()
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
print("currentIndex called")
if previousOffset.x < scrollView.contentOffset.x{
currentIndex += 1
}else if previousOffset.x > scrollView.contentOffset.x{
currentIndex -= 1
}
previousOffset = scrollView.contentOffset
}
func rotateViewsRight(){
let endView = pages.removeLast()
pages.insert(endView, at: 0)
setContentViewFrames()
}
func rotateViewsLeft(){
let endView = pages.removeFirst()
pages.append(endView)
setContentViewFrames()
}
func setContentViewFrames(){
for i in 0..<pages.count{
// let adjustedIndex = i % pages.count
// let view = pages[i]
// view.frame = CGRect(origin: CGPoint(x: view.bounds.width * CGFloat(i), y: 0), size: view.bounds.size)
pages[i].frame = CGRect(x: CGFloat(i)*view.bounds.width, y: 0, width: view.bounds.width, height: view.bounds.height)
let label = labels[currentIndex%labels.count]
label.text = "current label: \(currentIndex)"
label.frame = pages[i].frame
pages[i].addSubview(label)
}
}
func configurePage(forIndex index:Int){
}
func dequeueRecycledPage()->Page?{
let page = recycledPages.first
if let page = page{
recycledPages.remove(page)
return page
}
return nil
}
}

UITapGestureRecognizer doesn't work properly

I made the function updateItems() which create, from an array, many UIView's in a UIScrollView :
Here is the file where this function is :
class MainViewController: UIViewController {
#IBOutlet weak var body: UIScrollView!
#IBOutlet weak var edit: UIButton!
var _title: String = "Title"
var _isEditing: Bool = false
var firstItems: [UISectionView] = []
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.prefersLargeTitles = true
navigationController?.navigationBar.topItem?.title = self._title
navigationController?.navigationItem.largeTitleDisplayMode = .automatic
body.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height + 100)
self.updateItems(self.firstItems)
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
public func updateItems(_ s: [UISectionView]) {
let topMargin = 10
let rightMargin = 10
let leftMargin = 10
let space = 5
let heightItem = 60
var b = topMargin
for i in body.subviews {
i.removeFromSuperview()
}
for t in s {
if t.isHidden == true {
continue
}
if t.title != nil {
let f = UIFont(name: "Helvetica", size: 20)
let l = UILabel(frame: CGRect(x: rightMargin, y : b, width: Int(UIScreen.main.bounds.width) - (rightMargin + leftMargin), height: Int(f!.lineHeight)))
l.font = f
l.text = t.title
body.addSubview(l)
b = b + Int(f!.lineHeight) + space
}
for i in t.items{
body.addSubview(i.getView(frame: CGRect(x: rightMargin, y: b, width: Int(UIScreen.main.bounds.width) - (rightMargin + leftMargin), height: heightItem), view: self))
b = b + heightItem + space
}
}
}
}
TIPS : UISectionView is an object which contains an array of UIItemView
The object UIItemView looks like :
class UIItemView {
var icon: UIImage = UIImage();
var line1: rString = rString("")!;
var line2: rString = rString("")!;
var leftline: Any = String();
var background: String = "white"
var onItemTap: (_ sender: UITapGestureRecognizer?) -> () = {sender in }
var onItemLongPress: (_ sender: UILongPressGestureRecognizer?) -> () = {sender in }
var id: String
init?(_ id: String) {
self.id = id
}
public func getView(frame: CGRect, view: UIViewController) -> UIView {
let width = Int(frame.width)
let height = Int(frame.height)
let rightMargin = 20
let leftMargin = 10
let topMargin = 10
let bottomMargin = 10
let iconSide = height - (topMargin + bottomMargin)
let marginLine = leftMargin + iconSide + 10
let v = UIView(frame: frame)
//Background & shape
if self.background == "white" {
v.backgroundColor = UIColor.white;
} else if self.background == "blur" {
let bEV = UIVisualEffectView(effect: UIBlurEffect(style: UIBlurEffectStyle.extraLight))
bEV.frame = v.bounds
bEV.autoresizingMask = [.flexibleWidth, .flexibleHeight]
v.addSubview(bEV)
}
v.layer.cornerRadius = 10.0
//Icon
let i = UIImageView()
i.image = self.icon;
i.frame = CGRect(x: leftMargin, y: topMargin, width: iconSide, height: iconSide)
v.addSubview(i)
//First Line
let l1 = self.line1.getLabel()
l1.frame = CGRect(x: marginLine, y: topMargin, width: width - (marginLine + leftMargin), height: Int(self.line1.getFont().lineHeight))
v.addSubview(l1)
//Seconde Line
let l2 = self.line2.getLabel()
l2.frame = CGRect(x: marginLine, y: height - (bottomMargin + Int(self.line1.getFont().lineHeight)), width: width - (marginLine + leftMargin), height: Int(self.line1.getFont().lineHeight))
v.addSubview(l2)
//Left Line
if type(of: self.leftline) == type(of: SpinnerView()) {
let sv = (self.leftline as! SpinnerView)
sv.frame = CGRect(x: width - (rightMargin + iconSide), y: height/2 - iconSide/2, width: iconSide, height: iconSide)
v.addSubview(sv)
} else if type(of: self.leftline) == type(of: rString("")) {
let rS = (self.leftline as! rString)
if rS.text != "" {
rS.fontName = "HelveticaNeue-Bold"
rS.size = 15
rS.color = UIColor(red:0.01, green:0.48, blue:1.00, alpha:1.0)
let l3 = rS.getLabel()
l3.frame = CGRect(x: width - (rightMargin + Int(rS.getFont().lineWidth(rS.text)) + 15), y: height/2 - (Int(rS.getFont().lineHeight) + 10)/2, width: Int(rS.getFont().lineWidth(rS.text)) + 15, height: Int(rS.getFont().lineHeight) + 10)
l3.backgroundColor = UIColor(red:0.94, green:0.94, blue:0.97, alpha:1.0)
l3.layer.masksToBounds = true
l3.layer.borderWidth = 2
l3.layer.borderColor = UIColor(red:0.94, green:0.94, blue:0.97, alpha:1.0).cgColor
l3.layer.cornerRadius = rS.getFont().lineHeight/1.2
l3.textAlignment = .center
v.addSubview(l3)
}
}
//GestureRecognizer
v.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.oIT(_:))))
v.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.oILP(_:))))
v.restorationIdentifier = self.id
return v;
}
#objc func oIT(_ sender: UITapGestureRecognizer) {
print("Tap")
self.onItemTap(sender)
}
#objc func oILP(_ sender: UILongPressGestureRecognizer) {
print("LongPress")
self.onItemLongPress(sender)
}
static func ==(lhs: UIItemView, rhs: UIItemView) -> Bool {
return lhs === rhs
}
}
TIPS : UIItemView contains the function getView() which returns a specific UIView
The problem :
Everything work properly, when I load the ViewController (where there is the UIScrollView) every UIView's are build like I want, and I can interact with the UIView by the UITapGestureRecognizer or the UILongPressGestureRecognizer (the function is called as expected)
BUT
When I call the function updateItems() a second time, without reload the ViewController, the items change as expected but the UITapGestureRecognizer and the UILongPressGestureRecognizer don't work any more.
I hope you can help me :D
If information are missing for you to understand the problem, please let me know ;)

SwiftPages updateUI Does Not Work with Swift 3

I'm using Swiftpages. When app is opened it looks like first picture.
But app goes to background and opened different app on device, after open again my app it looks like second picture.
I updated to Swift 3, but I can't figure out the issue, I write about it on Github but no reply from them.
public class SwiftPages: UIView {
private lazy var token = 0
var containerVieww: UIView!
private var scrollView: UIScrollView!
private var topBar: UIView!
var animatedBar: UIView!
var viewControllerIDs = [String]()
private var buttonTitles = [String]()
private var buttonImages = [UIImage]()
private var pageViews = [UIViewController?]()
private var currentPage: Int = 0
// Container view position variables
private var xOrigin: CGFloat = 0
private var yOrigin: CGFloat = 64
private var distanceToBottom: CGFloat = 0
// Color variables
private var animatedBarColor = UIColor(red: 28/255, green: 95/255, blue: 185/255, alpha: 1)
private var topBarBackground = UIColor.white
private var buttonsTextColor = UIColor.gray
private var containerViewBackground = UIColor.white
// Item size variables
private var topBarHeight: CGFloat = 52
private var animatedBarHeight: CGFloat = 3
// Bar item variables
private var aeroEffectInTopBar = false //This gives the top bap a blurred effect, also overlayes the it over the VC's
private var buttonsWithImages = false
var barShadow = true
private var shadowView : UIView!
private var shadowViewGradient = CAGradientLayer()
private var buttonsTextFontAndSize = UIFont(name: "HelveticaNeue-Light", size: 20)!
private var blurView : UIVisualEffectView!
private var barButtons = [UIButton?]()
// MARK: - Positions Of The Container View API -
public func setOriginX (origin : CGFloat) { xOrigin = origin }
public func setOriginY (origin : CGFloat) { yOrigin = origin }
public func setDistanceToBottom (distance : CGFloat) { distanceToBottom = distance }
// MARK: - API's -
public func setAnimatedBarColor (color : UIColor) { animatedBarColor = color }
public func setTopBarBackground (color : UIColor) { topBarBackground = color }
public func setButtonsTextColor (color : UIColor) { buttonsTextColor = color }
public func setContainerViewBackground (color : UIColor) { containerViewBackground = color }
public func setTopBarHeight (pointSize : CGFloat) { topBarHeight = pointSize}
public func setAnimatedBarHeight (pointSize : CGFloat) { animatedBarHeight = pointSize}
public func setButtonsTextFontAndSize (fontAndSize : UIFont) { buttonsTextFontAndSize = fontAndSize}
public func enableAeroEffectInTopBar (boolValue : Bool) { aeroEffectInTopBar = boolValue}
public func enableButtonsWithImages (boolValue : Bool) { buttonsWithImages = boolValue}
public func enableBarShadow (boolValue : Bool) { barShadow = boolValue}
override public func draw(_ rect: CGRect) {
DispatchQueue.main.async {
let pagesContainerHeight = self.frame.height - self.yOrigin - self.distanceToBottom
let pagesContainerWidth = self.frame.width
// Set the notifications for an orientation change & BG mode
let defaultNotificationCenter = NotificationCenter.default
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.applicationWillEnterBackground), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.orientationWillChange), name: NSNotification.Name.UIApplicationWillChangeStatusBarOrientation, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.orientationDidChange), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
defaultNotificationCenter.addObserver(self, selector: #selector(SwiftPages.applicationWillEnterForeground), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
// Set the containerView, every item is constructed relative to this view
self.containerVieww = UIView(frame: CGRect(x: self.xOrigin, y: self.yOrigin, width: pagesContainerWidth, height: pagesContainerHeight))
self.containerVieww.backgroundColor = self.containerViewBackground
self.containerVieww.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.containerVieww)
//Add the constraints to the containerView.
if #available(iOS 9.0, *) {
let horizontalConstraint = self.containerVieww.centerXAnchor.constraint(equalTo: self.centerXAnchor)
let verticalConstraint = self.containerVieww.centerYAnchor.constraint(equalTo: self.centerYAnchor)
let widthConstraint = self.containerVieww.widthAnchor.constraint(equalTo: self.widthAnchor)
let heightConstraint = self.containerVieww.heightAnchor.constraint(equalTo: self.heightAnchor)
NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint, widthConstraint, heightConstraint])
}
// Set the scrollview
if self.aeroEffectInTopBar {
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: self.containerVieww.frame.size.width, height: self.containerVieww.frame.size.height))
} else {
self.scrollView = UIScrollView(frame: CGRect(x: 0, y: self.topBarHeight, width: self.containerVieww.frame.size.width, height: self.containerVieww.frame.size.height - self.topBarHeight))
}
self.scrollView.isPagingEnabled = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.showsVerticalScrollIndicator = false
self.scrollView.delegate = self
self.scrollView.backgroundColor = UIColor.clear
self.scrollView.contentOffset = CGPoint(x: 0, y: 0)
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.scrollView.isScrollEnabled = false
self.containerVieww.addSubview(self.scrollView)
// Add the constraints to the scrollview.
if #available(iOS 9.0, *) {
let leadingConstraint = self.scrollView.leadingAnchor.constraint(equalTo: self.containerVieww.leadingAnchor)
let trailingConstraint = self.scrollView.trailingAnchor.constraint(equalTo: self.containerVieww.trailingAnchor)
let topConstraint = self.scrollView.topAnchor.constraint(equalTo: self.containerVieww.topAnchor)
let bottomConstraint = self.scrollView.bottomAnchor.constraint(equalTo: self.containerVieww.bottomAnchor)
NSLayoutConstraint.activate([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
}
// Set the top bar
self.topBar = UIView(frame: CGRect(x: 0, y: 0, width: self.containerVieww.frame.size.width, height: self.topBarHeight))
self.topBar.backgroundColor = self.topBarBackground
if self.aeroEffectInTopBar {
// Create the blurred visual effect
// You can choose between ExtraLight, Light and Dark
self.topBar.backgroundColor = UIColor.clear
let blurEffect: UIBlurEffect = UIBlurEffect(style: .light)
self.blurView = UIVisualEffectView(effect: blurEffect)
self.blurView.frame = self.topBar.bounds
self.blurView.translatesAutoresizingMaskIntoConstraints = false
self.topBar.addSubview(self.blurView)
}
self.topBar.translatesAutoresizingMaskIntoConstraints = false
self.containerVieww.addSubview(self.topBar)
// Set the top bar buttons
// Check to see if the top bar will be created with images ot text
if self.buttonsWithImages {
var buttonsXPosition: CGFloat = 0
for (index, image) in self.buttonImages.enumerated() {
let frame = CGRect(x: buttonsXPosition, y: 0, width: self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count), height: self.topBarHeight)
let barButton = UIButton(frame: frame)
barButton.backgroundColor = UIColor.clear
barButton.imageView?.contentMode = .scaleAspectFit
barButton.setImage(image, for: .normal)
barButton.tag = index
barButton.addTarget(self, action: #selector(SwiftPages.barButtonAction), for: .touchUpInside)
self.topBar.addSubview(barButton)
self.barButtons.append(barButton)
buttonsXPosition += self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count)
}
} else {
var buttonsXPosition: CGFloat = 0
for (index, title) in self.buttonTitles.enumerated() {
let frame = CGRect(x: buttonsXPosition, y: 0, width: self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count), height: self.topBarHeight)
let barButton = UIButton(frame: frame)
barButton.backgroundColor = UIColor.clear
barButton.titleLabel!.font = self.buttonsTextFontAndSize
barButton.setTitle(title, for: .normal)
barButton.setTitleColor(self.buttonsTextColor, for: .normal)
barButton.tag = index
barButton.addTarget(self, action: #selector(SwiftPages.barButtonAction), for: .touchUpInside)
self.topBar.addSubview(barButton)
self.barButtons.append(barButton)
buttonsXPosition += self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count)
}
}
// Set up the animated UIView
self.animatedBar = UIView(frame: CGRect(x: 0, y: self.topBarHeight - self.animatedBarHeight + 1, width: (self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count)) * 0.8, height: self.animatedBarHeight))
self.animatedBar.center.x = self.containerVieww.frame.size.width / CGFloat(self.viewControllerIDs.count << 1)
self.animatedBar.backgroundColor = self.animatedBarColor
self.containerVieww.addSubview(self.animatedBar)
// Add the bar shadow (set to true or false with the barShadow var)
if self.barShadow {
self.shadowView = UIView(frame: CGRect(x: 0, y: self.topBarHeight, width: self.containerVieww.frame.size.width, height: 4))
self.shadowViewGradient.frame = self.shadowView.bounds
self.shadowViewGradient.colors = [UIColor(red: 150/255, green: 150/255, blue: 150/255, alpha: 0.28).cgColor, UIColor.clear.cgColor]
self.shadowView.layer.insertSublayer(self.shadowViewGradient, at: 0)
self.containerVieww.addSubview(self.shadowView)
}
let pageCount = self.viewControllerIDs.count
// Fill the array containing the VC instances with nil objects as placeholders
for _ in 0..<pageCount {
self.pageViews.append(nil)
}
// Defining the content size of the scrollview
let pagesScrollViewSize = self.scrollView.frame.size
self.scrollView.contentSize = CGSize(width: pagesScrollViewSize.width * CGFloat(pageCount), height: pagesScrollViewSize.height)
// Load the pages to show initially
self.loadVisiblePages()
// Do the initial alignment of the subViews
self.alignSubviews()
}
}
// MARK: - Initialization Functions -
public func initializeWithVCIDsArrayAndButtonTitlesArray (VCIDsArray: [String], buttonTitlesArray: [String]) {
// Important - Titles Array must Have The Same Number Of Items As The viewControllerIDs Array
if VCIDsArray.count == buttonTitlesArray.count {
viewControllerIDs = VCIDsArray
buttonTitles = buttonTitlesArray
buttonsWithImages = false
} else {
print("Initilization failed, the VC ID array count does not match the button titles array count.")
}
}
public func initializeWithVCIDsArrayAndButtonImagesArray (VCIDsArray: [String], buttonImagesArray: [UIImage]) {
// Important - Images Array must Have The Same Number Of Items As The viewControllerIDs Array
if VCIDsArray.count == buttonImagesArray.count {
viewControllerIDs = VCIDsArray
buttonImages = buttonImagesArray
buttonsWithImages = true
} else {
print("Initilization failed, the VC ID array count does not match the button images array count.")
}
}
public func loadPage(page: Int) {
// If it's outside the range of what you have to display, then do nothing
guard page >= 0 && page < viewControllerIDs.count else { return }
// Do nothing if the view is already loaded.
guard pageViews[page] == nil else { return }
print("Loading Page \(page)")
// The pageView instance is nil, create the page
var frame = scrollView.bounds
frame.origin.x = frame.size.width * CGFloat(page)
frame.origin.y = 0.0
// Look for the VC by its identifier in the storyboard and add it to the scrollview
let newPageView = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: viewControllerIDs[page])
newPageView.view.frame = frame
scrollView.addSubview(newPageView.view)
// Replace the nil in the pageViews array with the VC just created
pageViews[page] = newPageView
}
public func loadVisiblePages() {
// First, determine which page is currently visible
let pageWidth = scrollView.frame.size.width
let page = Int(floor((scrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))
// Work out which pages you want to load
let firstPage = page - 1
let lastPage = page + 1
// Load pages in our range
for index in firstPage...lastPage {
loadPage(page: index)
}
}
public func barButtonAction(sender: UIButton?) {
let index = sender!.tag
let pagesScrollViewSize = scrollView.frame.size
scrollView.setContentOffset(CGPoint(x: pagesScrollViewSize.width * CGFloat(index), y: 0), animated: true)
currentPage = index
}
// MARK: - Orientation Handling Functions -
public func alignSubviews() {
let pageCount = viewControllerIDs.count
// Setup the new frames
scrollView.contentSize = CGSize(width: CGFloat(pageCount) * scrollView.bounds.size.width, height: scrollView.bounds.size.height)
topBar.frame = CGRect(x: 0, y: 0, width: containerVieww.frame.size.width, height: topBarHeight)
blurView?.frame = topBar.bounds
animatedBar.frame.size = CGSize(width: (containerVieww.frame.size.width / (CGFloat)(viewControllerIDs.count)) * 0.8, height: animatedBarHeight)
if barShadow {
shadowView.frame.size = CGSize(width: containerVieww.frame.size.width, height: 4)
shadowViewGradient.frame = shadowView.bounds
}
// Set the new frame of the scrollview contents
for (index, controller) in pageViews.enumerated() {
controller?.view.frame = CGRect(x: CGFloat(index) * scrollView.bounds.size.width, y: 0, width: scrollView.bounds.size.width, height: scrollView.bounds.size.height)
}
// Set the new frame for the top bar buttons
var buttonsXPosition: CGFloat = 0
for button in barButtons {
button?.frame = CGRect(x: buttonsXPosition, y: 0, width: containerVieww.frame.size.width / CGFloat(viewControllerIDs.count), height: topBarHeight)
buttonsXPosition += containerVieww.frame.size.width / CGFloat(viewControllerIDs.count)
}
}
func applicationWillEnterBackground() {
//Save the current page
currentPage = Int(scrollView.contentOffset.x / scrollView.bounds.size.width)
print("Haydar")
}
func orientationWillChange() {
//Save the current page
currentPage = Int(scrollView.contentOffset.x / scrollView.bounds.size.width)
}
func orientationDidChange() {
//Update the view
alignSubviews()
scrollView.contentOffset = CGPoint(x: CGFloat(currentPage) * scrollView.frame.size.width, y: 0)
}
func applicationWillEnterForeground() {
alignSubviews()
scrollView.contentOffset = CGPoint(x: CGFloat(currentPage) * scrollView.frame.size.width, y: 0)
initializeWithVCIDsArrayAndButtonTitlesArray(VCIDsArray: buttonTitles, buttonTitlesArray: buttonTitles)
print("ForegroundHound")
}
public func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let previousPage : NSInteger = currentPage
let pageWidth : CGFloat = scrollView.frame.size.width
let fractionalPage = scrollView.contentOffset.x / pageWidth
let page : NSInteger = Int(round(fractionalPage))
if (previousPage != page) {
currentPage = page;
}
}
deinit {
NotificationCenter.default.removeObserver(self)
print("deinittta")
}
}
extension SwiftPages: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Load the pages that are now on screen
loadVisiblePages()
// The calculations for the animated bar's movements
// The offset addition is based on the width of the animated bar (button width times 0.8)
let offsetAddition = (containerVieww.frame.size.width / CGFloat(viewControllerIDs.count)) * 0.1
animatedBar.frame = CGRect(x: (offsetAddition + (scrollView.contentOffset.x / CGFloat(viewControllerIDs.count))), y: animatedBar.frame.origin.y, width: animatedBar.frame.size.width, height: animatedBar.frame.size.height)
}
}

Force custom view to redraw (or invalidate) using a timer

I've created a custom widget, which is animated. Now my problem is that I can't redraw the view, when the corresponding data gets updated.
Just don't hold anything against me in the code. This is my first piece of code in swift and I haven't worked with neither swift nor with Objective-C :-D
And also I've read the following questions, but they didn't help me:
How to force a view to render itself?
what-is-the-most-robust-way-to-force-a-uiview-to-redraw
p.s. : I can see the output of print(digit.phase) in the console.
p.s.s: I've also used performSelectorOnMainThread for calling the setNeedsDisplay function
The code:
import UIKit
struct Digit {
var targetDigit: Int
var currentDigit: Int
var phase: Float
}
#IBDesignable class RollerCounter: UIView {
var view: UIView!
var viewRect: CGRect!
var intNumber: Int
var digits = [Digit]()
let baseY = 20
var timer: NSTimer?
#IBInspectable var number: Int {
get {
return intNumber
}
set(number) {
intNumber = number
digits = []
var tempNumber:Int = intNumber
while tempNumber > 0 {
digits.append(Digit(targetDigit: tempNumber % 10, currentDigit: Int(rand()) % 10, phase: 0.0))
tempNumber /= 10
}
}
}
//init
override init(frame: CGRect) {
// set properties:
intNumber = 1111
super.init(frame: frame)
// setup the thing!
setup()
}
required init?(coder aDecoder: NSCoder) {
intNumber = 1111
super.init(coder: aDecoder)
// setup the thing
setup()
}
// Inital setup
func setup() {
let viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
view = UIView(frame: viewRect)
view.frame = bounds
view.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
addSubview(view)
self.setNeedsDisplay()
backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.0)
}
func animate() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.016, target: self, selector: Selector("tick"), userInfo: nil, repeats: true)
}
func tick() {
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
setNeedsDisplay()
//TEST: Also tested this
// if let rect = viewRect {
// drawRect(rect)
// } else {
// viewRect = CGRect(x: 0, y: 0, width: 280, height: 40)
// drawRect(viewRect
// }
}
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
// Drawing code
var tempNumber: Int = number
let strTempNumber = String(tempNumber)
var index: Int = 1
let width = Float(rect.width)
let charWidth: Float = Float(rect.width) / Float(strTempNumber.characters.count)
let charHeight: CGFloat = 36
let color = UIColor.blackColor()
let font: UIFont = UIFont(name: "Helvetica Neue", size: charHeight)!
let paraStyle = NSMutableParagraphStyle()
paraStyle.lineSpacing = 6.0
let skew = 0.1
let textAttribs = [
NSForegroundColorAttributeName: color,
NSParagraphStyleAttributeName: paraStyle,
NSObliquenessAttributeName: skew,
NSFontAttributeName: font
]
for digit in digits {
let strCurrentDigit: NSString = String(digit.currentDigit) as NSString
let strNextDigit: NSString = String(digit.currentDigit - 1) as NSString
let xPos = width - Float(index) * charWidth
let yPos = Float(baseY) + Float(charHeight) * digit.phase
let point: CGPoint = CGPoint(x: Int(xPos), y: Int(yPos))
strCurrentDigit.drawAtPoint(point, withAttributes: textAttribs)
let nextDigitYPos = yPos - Float(charHeight) * 1.2
let nextDigitPoint: CGPoint = CGPoint(x: Int(xPos), y: Int(nextDigitYPos))
strNextDigit.drawAtPoint(nextDigitPoint, withAttributes: textAttribs)
index++
tempNumber /= 10
}
}
}
Sorry folks. My bad :-(
There's nothing wrong with the invalidation system. Here's what's wrong:
for var digit in digits {
digit.phase += Float(rand() % 100) / 100
print(digit.phase)
}
As it turns out, the changes to phase only get reflected in the local digit instance inside the for loop
But just to be clear, the setNeedsDisplay() call inside the tick method is crucial for the view to be updated.

How to add small red dot in UITabBarItem

How to add red dot on the top right side of the UITabBarItem.
I have searched a while and some guys said this can be done setting Badge Value of the UITabBarItem.But when I give it a try and set badge value to empty space " ",the red dot is somewhat big.How can I get a proper one?Big thanks.
If you want to avoid traversing subviews & potentially dangerous hacks in general, what I've done is set the badge's background colour to clear and used a styled bullet point to appear as a badge:
tabBarItem.badgeValue = "●"
tabBarItem.badgeColor = .clear
tabBarItem.setBadgeTextAttributes([NSAttributedStringKey.foregroundColor.rawValue: UIColor.red], for: .normal)
This seems more future-proof than the other answers.
you can try this method:
func addRedDotAtTabBarItemIndex(index: Int) {
for subview in tabBarController!.tabBar.subviews {
if let subview = subview as? UIView {
if subview.tag == 1314 {
subview.removeFromSuperview()
break
}
}
}
let RedDotRadius: CGFloat = 5
let RedDotDiameter = RedDotRadius * 2
let TopMargin:CGFloat = 5
let TabBarItemCount = CGFloat(self.tabBarController!.tabBar.items!.count)
let HalfItemWidth = CGRectGetWidth(view.bounds) / (TabBarItemCount * 2)
let xOffset = HalfItemWidth * CGFloat(index * 2 + 1)
let imageHalfWidth: CGFloat = (self.tabBarController!.tabBar.items![index] as! UITabBarItem).selectedImage.size.width / 2
let redDot = UIView(frame: CGRect(x: xOffset + imageHalfWidth, y: TopMargin, width: RedDotDiameter, height: RedDotDiameter))
redDot.tag = 1314
redDot.backgroundColor = UIColor.redColor()
redDot.layer.cornerRadius = RedDotRadius
self.tabBarController?.tabBar.addSubview(redDot)
}
set the badgeValue for your desired UITabBarItem as follow:
// for first tab
(tabBarController!.tabBar.items!.first! as! UITabBarItem).badgeValue = "1"
//for second tab
(tabBarController!.tabBar.items![1] as! UITabBarItem).badgeValue = "2"
// for last tab
(tabBarController!.tabBar.items!.last! as! UITabBarItem).badgeValue = "final"
for remove a badge from the UITabBarItem just assign nil
(tabBarController!.tabBar.items!.first! as! UITabBarItem).badgeValue = nil
you can get the output Like
for additional information please ref this link
Choice --2
var lbl : UILabel = UILabel(frame: CGRectMake(225, 5, 20, 20))
lbl.layer.borderColor = UIColor.whiteColor().CGColor
lbl.layer.borderWidth = 2
lbl.layer.cornerRadius = lbl.bounds.size.height/2
lbl.textAlignment = NSTextAlignment.Center
lbl.layer.masksToBounds = true
lbl.font = UIFont(name: hereaddyourFontName, size: 13)
lbl.textColor = UIColor.whiteColor()
lbl.backgroundColor = UIColor.redColor()
lbl.text = "1" //if you no need remove this
// add subview to tabBarController?.tabBar
self.tabBarController?.tabBar.addSubview(lbl)
the output is
That is very simple in current iOS versions
tabBarItem.badgeValue = " "
it shows the red dot on the top of the tabbar item
Swift 5+
This goes into the controller that belongs to the tab
alt. you just need to grab the right tabBarItem
func updateTabBarBadge(showDot: Bool) {
guard let tbi = tabBarItem else {
return
}
if showDot {
tbi.setBadgeTextAttributes([.font: UIFont.systemFont(ofSize: 6), .foregroundColor:UIColor(named: "Primary")!], for: .normal)
tbi.badgeValue = "⬤"
tbi.badgeColor = UIColor.clear
} else {
tbi.badgeValue = nil
}
}
I have figured out a hack solution.
func addRedDotAtTabBarItemIndex(index: Int,dotRadius: CGFloat) {
var tabBarButtons = [UIView]()
// find the UITabBarButton instance.
for subview in tabBarController!.tabBar.subviews.reverse() {
if subview.isKindOfClass(NSClassFromString("UITabBarButton")) {
tabBarButtons.append(subview as! UIView)
}
}
if index >= tabBarButtons.count {
println("out of bounds")
return
}
let tabBar = tabBarButtons[index]
var selectedImageWidth: CGFloat!
var topMargin: CGFloat!
for subview in tabBar.subviews {
if subview.isKindOfClass(NSClassFromString("UITabBarSwappableImageView")) {
selectedImageWidth = (subview as! UIView).frame.size.width
topMargin = (subview as! UIView).frame.origin.y
}
}
// remove existing red dot.
for subview in tabBar.subviews {
if subview.tag == 999 {
subview.removeFromSuperview()
}
}
let redDot = UIView(frame: CGRect(x: CGRectGetMidX(tabBar.bounds) + selectedImageWidth / 2 + dotRadius, y: topMargin, width: dotRadius * 2, height: dotRadius * 2))
redDot.backgroundColor = UIColor.redColor()
redDot.layer.cornerRadius = dotRadius // half of the view's height.
redDot.tag = 999
tabBar.addSubview(redDot)
}
Works both for iPad and iPhone.
Be able to hide and calculate index automatically.
Call self.setTabBarDotVisible(visible:true) if self is not an UITabBarController.
Call self.setTabBarDotVisible(visible:true, index:2) if self is an UITabBarController.
import UIKit
public extension UIViewController {
func setTabBarDotVisible(visible:Bool,index: Int? = nil) {
let tabBarController:UITabBarController!
if self is UITabBarController
{
tabBarController = self as! UITabBarController
}
else
{
if self.tabBarController == nil
{
return
}
tabBarController = self.tabBarController!
}
let indexFinal:Int
if (index != nil)
{
indexFinal = index!
}
else
{
let index3 = tabBarController.viewControllers?.index(of: self)
if index3 == nil
{
return;
}
else
{
indexFinal = index3!
}
}
guard let barItems = tabBarController.tabBar.items else
{
return
}
//
let tag = 8888
var tabBarItemView:UIView?
for subview in tabBarController.tabBar.subviews {
let className = String(describing: type(of: subview))
guard className == "UITabBarButton" else {
continue
}
var label:UILabel?
var dotView:UIView?
for subview2 in subview.subviews {
if subview2.tag == tag {
dotView = subview2;
}
else if (subview2 is UILabel)
{
label = subview2 as? UILabel
}
}
if label?.text == barItems[indexFinal].title
{
dotView?.removeFromSuperview()
tabBarItemView = subview;
break;
}
}
if (tabBarItemView == nil || !visible)
{
return
}
let barItemWidth = tabBarItemView!.bounds.width
let x = barItemWidth * 0.5 + (barItems[indexFinal].selectedImage?.size.width ?? barItemWidth) / 2
let y:CGFloat = 5
let size:CGFloat = 10;
let redDot = UIView(frame: CGRect(x: x, y: y, width: size, height: size))
redDot.tag = tag
redDot.backgroundColor = UIColor.red
redDot.layer.cornerRadius = size/2
tabBarItemView!.addSubview(redDot)
}
}
i test this question's answer. but not work on iPad.
now i found that, when u add this on iPhone, tabBarItem left and right margin is 2, and each items margin is 4. Code as below:
NSInteger barItemCount = self.tabBar.items.count;
UITabBarItem *barItem = (UITabBarItem *)self.tabBar.items[index];
CGFloat imageHalfWidth = barItem.image.size.width / 2.0;
CGFloat barItemWidth = (BXS_WINDOW_WIDTH - barItemCount * 4) / barItemCount;
CGFloat barItemMargin = 4;
CGFloat redDotXOffset = barItemMargin / 2 + barItemMargin * index + barItemWidth * (index + 0.5);
and iPad as below:
barItemWidth = 76;
barItemMargin = 34;
redDotXOffset = (BXS_WINDOW_WIDTH - 76 * barItemCount - 34 * (barItemCount - 1)) / 2.0 + 76 * (index + 0.5) + 34 * index;
Hope this is useful.
This it Swift 4 solution:
1) Add BaseTabBar custom class to your project:
import UIKit
class BaseTabBar: UITabBar {
static var dotColor: UIColor = UIColor.red
static var dotSize: CGFloat = 4
static var dotPositionX: CGFloat = 0.8
static var dotPositionY: CGFloat = 0.2
var dotMap = [Int: Bool]()
func resetDots() {
dotMap.removeAll()
}
func addDot(tabIndex: Int) {
dotMap[tabIndex] = true
}
func removeDot(tabIndex: Int) {
dotMap[tabIndex] = false
}
override func draw(_ rect: CGRect) {
super.draw(rect)
if let items = items {
for i in 0..<items.count {
let item = items[i]
if let view = item.value(forKey: "view") as? UIView, let dotBoolean = dotMap[i], dotBoolean == true {
let x = view.frame.origin.x + view.frame.width * BaseTabBar.dotPositionX
let y = view.frame.origin.y + view.frame.height * BaseTabBar.dotPositionY
let dotPath = UIBezierPath(ovalIn: CGRect(x: x, y: y, width: BaseTabBar.dotSize, height: BaseTabBar.dotSize))
BaseTabBar.dotColor.setFill()
dotPath.fill()
}
}
}
}
}
2) Change the custom class of UITabBar inside your UITabBarController to BaseTabBar.
3) Manage the dots in the place where you can access the tabBarController
func updateNotificationCount(count: Int) {
if let tabBar = navigationController?.tabBarController?.tabBar as? BaseTabBar {
if count > 0 {
tabBar.addDot(tabIndex: 0)
} else {
tabBar.removeDot(tabIndex: 0)
}
tabBar.setNeedsDisplay()
}
}
I added 5 tab bar indexes and add the dot points according to the notification occurs. First, create Dots view array.
var Dots = [UIView](repeating: UIView(), count: 5)
func addRedDotAtTabBarItemIndex(index: Int) {
if self.Dots[index].tag != index {
let RedDotRadius: CGFloat = 7
let RedDotDiameter = RedDotRadius
let TopMargin:CGFloat = 2
let tabSize = self.tabBarController.view.frame.width / CGFloat(5)
let xPosition = tabSize * CGFloat(index - 1)
let tabHalfWidth: CGFloat = tabSize / 2
self.Dots[index] = UIView(frame: CGRect(x: xPosition + tabHalfWidth - 2 , y: TopMargin, width: RedDotDiameter, height: RedDotDiameter))
self.Dots[index].tag = index
self.Dots[index].backgroundColor = UIColor.red
self.Dots[index].layer.cornerRadius = RedDotRadius
self.tabBarController.tabBar.addSubview(self.Dots[index])
}
}
If you want to remove the dot from selected index, use this code:
func removeRedDotAtTabBarItemIndex(index: Int) {
self.Dots[index].removeFromSuperview()
self.Dots[index].tag = 0
}
simple solution
set space in storyboard tabbaritem badge value.
if we add space below output you can get:
In Swift 5:
tabBarItem.badgeValue = "1"
to change from default color use:
tabBarItem.badgeColor = UIColor.systemBlue
From iOS 13, use UITabBarAppearance and UITabBarItemAppearance
let appearance = UITabBarAppearance()
let itemAppearance = UITabBarItemAppearance(style: .stacked)
itemAppearance.normal.badgeBackgroundColor = .clear
itemAppearance.normal.badgeTextAttributes = [.foregroundColor: UIColor.red]
profileViewController.tabBarItem.badgeValue = "●"

Resources